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:
parent
3848c4bf33
commit
b7e9476431
6 changed files with 386 additions and 37 deletions
|
@ -1,5 +1,3 @@
|
|||
use std::process::Command;
|
||||
|
||||
use gtk::{
|
||||
prelude::BoxExt, Align, Button, ListBox, Orientation, ScrolledWindow, SearchBar, SearchEntry,
|
||||
};
|
||||
|
|
19
src/main.rs
19
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!();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
|
259
src/packages/updates.rs
Normal file
259
src/packages/updates.rs
Normal 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()));
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue