diff --git a/src/drivers/mod.rs b/src/drivers/mod.rs index 18d5631..150ca49 100644 --- a/src/drivers/mod.rs +++ b/src/drivers/mod.rs @@ -1,5 +1,3 @@ -use std::process::Command; - use gtk::{ prelude::BoxExt, Align, Button, ListBox, Orientation, ScrolledWindow, SearchBar, SearchEntry, }; diff --git a/src/main.rs b/src/main.rs index d9e7457..f6e96fc 100644 --- a/src/main.rs +++ b/src/main.rs @@ -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!(); - } -} diff --git a/src/packages/browse.rs b/src/packages/browse.rs index 9dd8aaf..44be840 100644 --- a/src/packages/browse.rs +++ b/src/packages/browse.rs @@ -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(); diff --git a/src/packages/mod.rs b/src/packages/mod.rs index 4dba313..5461aed 100644 --- a/src/packages/mod.rs +++ b/src/packages/mod.rs @@ -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(); - let install_btn = Button::builder().label("Install").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. - */ - install_btn.connect_clicked(move |_| { - Command::new("pkexec") - .args(["dnf", "install", pkg.as_str()]) - .spawn() - .unwrap(); - }); + /* + 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(&install_btn); + 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); diff --git a/src/packages/updates.rs b/src/packages/updates.rs new file mode 100644 index 0000000..a873265 --- /dev/null +++ b/src/packages/updates.rs @@ -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 = vec![]; +pub static mut PACKAGES_LIST: Option = None; +pub static mut PKG_LIST: Vec = vec![]; +pub static mut PKG_LIST_INDEX: usize = 0; +pub static mut PKG_LISTBOX: Option = 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::>(); + + /* + 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())); +} diff --git a/src/style.css b/src/style.css index 28a53f1..1c1c7dd 100644 --- a/src/style.css +++ b/src/style.css @@ -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; +}