Make package info windows work correctly, make them have buttons depending on whether the package is installed and updatable, and made it so that browse packages has deduplication.

This commit is contained in:
Hellx2 2024-08-18 18:10:25 +10:00
parent 3848c4bf33
commit b7e9476431
6 changed files with 386 additions and 37 deletions

View file

@ -1,5 +1,3 @@
use std::process::Command;
use gtk::{
prelude::BoxExt, Align, Button, ListBox, Orientation, ScrolledWindow, SearchBar, SearchEntry,
};

View file

@ -1,9 +1,9 @@
use gtk::{
gdk, glib,
glib,
prelude::{ApplicationExt, ApplicationExtManual, BoxExt, GtkWindowExt},
style_context_add_provider_for_display, Align, Application, ApplicationWindow,
style_context_add_provider_for_display, Application, ApplicationWindow,
Orientation::{Horizontal, Vertical},
SearchBar, SearchEntry, Stack, StackSwitcher,
Stack, StackSwitcher,
};
pub mod drivers;
@ -121,19 +121,8 @@ fn on_startup(_: &Application) {
let css_provider = gtk::CssProvider::new();
css_provider.load_from_data(include_str!("style.css"));
style_context_add_provider_for_display(
gdk::Display::default().as_ref().unwrap(),
gtk::gdk::Display::default().as_ref().unwrap(),
&css_provider,
gtk::STYLE_PROVIDER_PRIORITY_USER,
);
}
#[cfg(test)]
mod tests {
use crate::drivers::get_driver_details;
#[test]
fn test_drivers() {
dbg!(get_driver_details());
panic!();
}
}

View file

@ -2,6 +2,7 @@ use gtk::{
prelude::{BoxExt, ButtonExt, EditableExt},
Button, PositionType,
};
use itertools::Itertools;
use std::process::Command;
use gtk::{Align, ListBox, ScrolledWindow, SearchBar, SearchEntry};
@ -84,6 +85,7 @@ pub fn browse() -> gtk::Box {
&& !x.contains("=")
&& !skip_list.contains(&x.to_lowercase().trim())
})
.dedup()
.collect();
PKG_LIST = FULL_PKG_LIST.clone();

View file

@ -7,6 +7,7 @@ use gtk::{
pub mod browse;
pub mod installed;
pub mod updates;
/**
Provides a box with a stack showing the installed,
@ -30,6 +31,7 @@ pub fn packages() -> gtk::Box {
packages_stack.add_titled(&browse::browse(), Some("browse"), "Browse");
packages_stack.add_titled(&installed::installed(), Some("installed"), "Installed");
packages_stack.add_titled(&updates::updates(), Some("updates"), "Updates");
// TODO: Add "Updates" section
let packages_box = gtk::Box::builder()
@ -61,6 +63,7 @@ fn show_package(pkg: String) {
// Create a window for the package details.
let pkg_window = Window::builder().title(&pkg).build();
let pkg_box = gtk::Box::builder()
.css_classes(["pkg-box"])
.orientation(gtk::Orientation::Vertical)
.build();
@ -74,6 +77,7 @@ fn show_package(pkg: String) {
)
.unwrap();
let mut is_installed = false;
/*
Get the correct info for whether the package
is installed or not and use the data from
@ -85,13 +89,14 @@ fn show_package(pkg: String) {
install button on this menu.
*/
let info = if info.contains("Installed Packages") {
is_installed = true;
let a = info.split_once("Installed Packages").unwrap().1.trim();
a.split_once("Available Packages")
.unwrap_or((a, ""))
.0
.trim()
} else {
info.split_once("Available Packages").unwrap().0.trim()
info.split_once("Available Packages").unwrap().1.trim()
};
// The details of the package, with default values just in case.
@ -141,42 +146,123 @@ fn show_package(pkg: String) {
/*
Append all of the parts to the box.
*/
pkg_box.append(
let description_box = gtk::Box::builder()
.orientation(gtk::Orientation::Horizontal)
.build();
description_box.append(
&Label::builder()
.label("Description: ".to_owned() + description.as_str())
.label("Description: ")
.css_classes(["description-label"])
.build(),
);
pkg_box.append(
description_box.append(&Label::builder().label(description.as_str()).build());
let version_box = gtk::Box::builder()
.orientation(gtk::Orientation::Horizontal)
.build();
version_box.append(
&Label::builder()
.label("Version: ".to_owned() + version)
.label("Version: ")
.css_classes(["version-label"])
.build(),
);
pkg_box.append(&Label::builder().label("Size: ".to_owned() + size).build());
pkg_box.append(
version_box.append(&Label::builder().label(version).build());
let size_box = gtk::Box::builder()
.orientation(gtk::Orientation::Horizontal)
.build();
size_box.append(
&Label::builder()
.label("License: ".to_owned() + license)
.label("Size: ")
.css_classes(["size-label"])
.build(),
);
size_box.append(&Label::builder().label(size).build());
let license_box = gtk::Box::builder()
.orientation(gtk::Orientation::Horizontal)
.build();
license_box.append(
&Label::builder()
.label("License: ")
.css_classes(["license-label"])
.build(),
);
license_box.append(&Label::builder().label(license).build());
pkg_box.append(&description_box);
pkg_box.append(&version_box);
pkg_box.append(&size_box);
pkg_box.append(&license_box);
let buttons_box = gtk::Box::builder()
.orientation(gtk::Orientation::Horizontal)
.spacing(10)
.css_classes(["buttons-box"])
.halign(Align::Center)
.build();
if is_installed {
let uninstall_btn = Button::builder().label("Uninstall").build();
/*
Make it so that when the install button
is clicked the package is installed.
*/
{
let pkg = pkg.clone();
uninstall_btn.connect_clicked(move |_| {
Command::new("pkexec")
.args(["dnf", "remove", pkg.as_str()])
.spawn()
.unwrap();
});
}
buttons_box.append(&uninstall_btn);
} else {
let install_btn = Button::builder().label("Install").build();
/*
Make it so that when the install button
is clicked the package is installed.
*/
{
let pkg = pkg.clone();
install_btn.connect_clicked(move |_| {
Command::new("pkexec")
.args(["dnf", "install", pkg.as_str()])
.spawn()
.unwrap();
});
}
buttons_box.append(&install_btn);
}
if unsafe { updates::FULL_PKG_LIST.contains(&pkg) } {
let update_btn = Button::builder().label("Update").build();
/*
Make it so that when the install button
is clicked the package is installed.
*/
{
let pkg = pkg.clone();
update_btn.connect_clicked(move |_| {
Command::new("pkexec")
.args(["dnf", "upgrade", pkg.as_str()])
.spawn()
.unwrap();
});
}
buttons_box.append(&update_btn);
}
pkg_box.append(&buttons_box);

259
src/packages/updates.rs Normal file
View file

@ -0,0 +1,259 @@
use gtk::{
prelude::{BoxExt, ButtonExt, EditableExt},
Button, PositionType,
};
use std::process::Command;
use itertools::Itertools;
use gtk::{Align, ListBox, ScrolledWindow, SearchBar, SearchEntry};
use super::show_package;
pub static mut FULL_PKG_LIST: Vec<String> = vec![];
pub static mut PACKAGES_LIST: Option<ListBox> = None;
pub static mut PKG_LIST: Vec<String> = vec![];
pub static mut PKG_LIST_INDEX: usize = 0;
pub static mut PKG_LISTBOX: Option<ScrolledWindow> = None;
/**
Function to initialise a box that contains a
list of updatable packages.
*/
pub fn updates() -> gtk::Box {
// The main box that encompasses everything else
let packages_box = gtk::Box::builder()
.orientation(gtk::Orientation::Vertical)
.css_classes(["packages-box"])
.valign(Align::Center)
.halign(Align::Fill)
.hexpand(true)
.build();
// Search bar
let pkgs_search = SearchBar::builder()
.halign(Align::Fill)
.valign(Align::Start)
.css_classes(["search-bar"])
.build();
let pkgs_entry = SearchEntry::builder()
.css_classes(["entries"])
.halign(Align::Center)
.valign(Align::Start)
.width_request(600)
.placeholder_text("Search for a package...")
.sensitive(true)
.hexpand(true)
.build();
/*
List of names to skip when searching, this is to remove the
labels "Available Packages", "Last refreshed at ..." and "Installed Packages"
*/
let skip_list = vec!["available", "installed", "last"];
/*
Initialize the packages list, needs to be a static
mutable variable to allow usage with GTK functions.
*/
unsafe {
PACKAGES_LIST = Some(
ListBox::builder()
.css_classes(["packages-list"])
.valign(Align::Center)
.halign(Align::Fill)
.hexpand(true)
.build(),
);
/*
Gets the list of packages, which also needs to be static
mutable for the same reason as PACKAGES_LIST, and remove
the description and architecture details, and also filter
out the lines that aren't actually packages.
*/
FULL_PKG_LIST = String::from_utf8(
Command::new("dnf")
.args(["check-update"])
.output()
.unwrap()
.stdout,
)
.unwrap()
.lines()
.map(|x| {
let y = x.to_string();
let b = y
.split_whitespace()
.next()
.unwrap_or("")
.split(".")
.next()
.unwrap();
b.to_string()
})
.filter(|x| {
!x.trim().is_empty()
&& !x.contains("=")
&& !skip_list.contains(&x.to_lowercase().trim())
})
.dedup()
.collect::<Vec<String>>();
/*
Add buttons for the first 50 packages in the list, to
keep the RAM usage to a minimum and not have every single
package and a button for it in the memory at the same time,
which has caused as much as 1.4GB of RAM usage during tests.
*/
for pkg in FULL_PKG_LIST[0..(50.min(FULL_PKG_LIST.len()))].to_vec() {
PKG_LIST_INDEX += 1;
let pkg_btn = Button::builder()
.label(pkg)
.css_classes(["package-name"])
.build();
pkg_btn.connect_clicked(|x| {
show_package(x.label().map(|x| x.to_string()).unwrap_or("".to_owned()));
});
PACKAGES_LIST.as_ref().unwrap().append(&pkg_btn)
}
/*
Create a scrollable area for the package list so that it
doesn't just go off the screen and hide all of the list.
*/
PKG_LISTBOX = Some(
ScrolledWindow::builder()
.css_classes(["package-list-scrollable"])
.valign(Align::Center)
.halign(Align::Center)
.width_request(800)
.height_request(600)
.build(),
);
/*
Make it so that on reaching the end of the list it adds
buttons for 50 more packages.
*/
PKG_LISTBOX
.as_ref()
.unwrap()
.connect_edge_reached(|_, edge| {
if edge == PositionType::Bottom {
for i in PKG_LIST_INDEX..((PKG_LIST_INDEX + 50).min(FULL_PKG_LIST.len())) {
let pkg_btn = Button::builder()
.label(FULL_PKG_LIST[i].as_str())
.css_classes(["package-name"])
.build();
pkg_btn.connect_clicked(|x| {
show_package(x.label().map(|x| x.to_string()).unwrap_or("".to_owned()));
});
PACKAGES_LIST.as_ref().unwrap().append(&pkg_btn);
PKG_LIST_INDEX += 1;
}
}
})
};
/*
Run the `search` function when the search is changed.
TODO: Make this asynchronous.
*/
pkgs_entry.connect_search_changed(move |x| {
let f = x.clone();
unsafe { search(&f) }
});
pkgs_search.connect_entry(&pkgs_entry);
// Add all of the parts to the box and return it.
packages_box.append(&pkgs_search);
packages_box.append(&pkgs_entry);
unsafe {
packages_box.append(PKG_LISTBOX.as_ref().unwrap());
PKG_LISTBOX
.as_ref()
.unwrap()
.set_child(Some(PACKAGES_LIST.as_ref().unwrap()));
};
packages_box
}
/**
Function that takes a search entry and updates
the package lists accordingly, skipping the values
stated in `skip_list`
*/
unsafe fn search(f: &SearchEntry) {
/*
Reinitialize PKG_LIST with an extra predicate
in the filter closure to ensure that the list
of packages contains only ones that match the
query
*/
PKG_LIST = FULL_PKG_LIST
.iter()
.filter(|x| {
x.to_lowercase()
.contains(f.text().to_string().to_lowercase().as_str())
})
.map(|x| x.clone())
.collect();
/*
Drop the inner value of PACKAGES_LIST to prevent it
from persisting, since it has a static lifetime.
TODO: Check if this is necessary
*/
std::mem::drop(PACKAGES_LIST.take());
/*
Reinitialize PACKAGES_LIST as a new ListBox.
*/
PACKAGES_LIST = Some(
ListBox::builder()
.css_classes(["packages-list"])
.valign(Align::Center)
.halign(Align::Fill)
.hexpand(true)
.build(),
);
// Reset the list index so that it starts at 0 in the package list
PKG_LIST_INDEX = 0;
/*
Loop through the package list from 0 until
either 50 or the length of the list, whichever
is less, and make buttons in the list for each
*/
for pkg in PKG_LIST[0..(50.min(PKG_LIST.len()))].to_vec() {
PKG_LIST_INDEX += 1;
let pkg_btn = Button::builder()
.label(pkg)
.css_classes(["package-name"])
.build();
pkg_btn.connect_clicked(|x| {
show_package(x.label().map(|x| x.to_string()).unwrap_or("".to_owned()));
});
PACKAGES_LIST.as_ref().unwrap().append(&pkg_btn)
}
/*
Reinitialise PKG_LISTBOX since the value
it points to has now been changed.
*/
PKG_LISTBOX
.as_ref()
.unwrap()
.set_child(Some(PACKAGES_LIST.as_ref().unwrap()));
}

View file

@ -17,3 +17,18 @@
margin: 10px;
border-radius: 10px;
}
.buttons-box {
margin-top: 5px;
}
.pkg-box {
padding: 10px;
}
.license-label,
.version-label,
.size-label,
.description-label {
font-weight: bold;
}