Added functionality for browsing packages and documented it.

This commit is contained in:
Hellx2 2024-08-22 16:47:44 +10:00
parent af8365675d
commit 941f66662e
6 changed files with 259 additions and 9 deletions

View file

@ -1,3 +1,4 @@
cargo
gtk4-devel
docker
fuse-overlayfs

View file

@ -1,4 +1,4 @@
use std::collections::BTreeMap;
use std::{collections::BTreeMap, str::FromStr};
use serde::{Deserialize, Serialize};
@ -284,3 +284,51 @@ pub fn change_keys(json: String) -> String {
value
}
/**
Struct for handling the output of the `docker search`
command.
*/
pub struct DockerSearchImage {
pub name: String,
pub description: String,
pub stars: i32,
pub official: bool,
}
impl FromStr for DockerSearchImage {
type Err = String;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let a: Vec<String> = s.split_whitespace().map(|x| x.to_owned()).collect();
if a.last().expect("Failed to get last item of Vec.").trim() == "[OK]" {
Ok(Self {
name: a[0].clone(),
description: a[1..(a.len() - 2)].join(" "),
stars: match a[a.len() - 2].parse() {
Ok(b) => b,
Err(a) => {
return Err("Failed to convert to i32: ".to_owned() + a.to_string().as_str())
}
},
official: true,
})
} else {
Ok(Self {
name: a[0].clone(),
description: a[1..(a.len() - 1)].join(" "),
stars: match a[a.len() - 1].parse() {
Ok(b) => b,
Err(b) => {
dbg!(a);
return Err(
"Failed to convert to i32: ".to_owned() + b.to_string().as_str()
);
}
},
official: false,
})
}
}
}

162
src/containers/browse.rs Normal file
View file

@ -0,0 +1,162 @@
use gtk::{
glib::Object,
prelude::{BoxExt, ButtonExt, Cast, EditableExt, ListModelExtManual, WidgetExt},
Button, CheckButton, Entry, Label, ListBox, ListBoxRow, ScrolledWindow,
};
use std::{
sync::{Arc, Mutex},
thread::spawn,
};
use crate::pkexec_a;
use super::{api::DockerSearchImage, images::gen_box};
/// A page for browsing the docker hub for images.
pub fn browse() -> gtk::Box {
let browse_box = gtk::Box::builder()
.orientation(gtk::Orientation::Vertical)
.build();
let entry = Entry::builder()
.css_classes(["docker-search"])
.halign(gtk::Align::Center)
.valign(gtk::Align::Start)
.build();
let images_list = ListBox::builder()
// .css_classes(["images-list"])
.halign(gtk::Align::Center)
.valign(gtk::Align::Center)
.hexpand(true)
.vexpand(true)
.build();
let search_btn = Button::builder()
.label("Search")
.css_classes(["search-btn"])
.halign(gtk::Align::Center)
.valign(gtk::Align::Start)
.build();
let image_list_scroll = ScrolledWindow::builder()
.child(&images_list)
.height_request(600)
.width_request(800)
.css_classes(["images-list"])
.build();
browse_box.append(&entry);
browse_box.append(&search_btn);
browse_box.append(&image_list_scroll);
search_btn.connect_clicked(move |_| {
populate_list(&images_list, entry.text().to_string());
});
browse_box
}
/**
Function to (re)initialize all of the items
in the `list` ListBox, re-running the docker
command and clearing the previous list.
*/
pub fn populate_list(list: &ListBox, query: String) {
let mut a = vec![];
list.observe_children().iter::<Object>().for_each(|f| {
a.push(
f.expect("Failed to get child")
.downcast::<ListBoxRow>()
.expect("Failed to get child"),
);
});
for i in a {
list.remove(&i);
}
/*
An Arc of a Mutex for passing across threads, hasn't been
implemented yet, due to a large amount of difficulty that
comes with trying to use multithreaded GTK.
*/
let m: Arc<Mutex<Vec<DockerSearchImage>>> = Arc::new(Mutex::new(vec![]));
/*
An attempt to run the docker command in the background,
yet failed because of a lack of a way to run the task
without blocking the main thread once again.
NOTE: Try using a GMutex to pass control of GTK across threads.
TODO: Figure out a way to make permissions to the docker daemon persistent.
*/
let handle = {
let m = Arc::clone(&m);
spawn(move || {
let mut a = m.lock().unwrap();
*a = pkexec_a(
vec!["docker", "search", "--no-trunc", query.as_str()],
false,
)
.expect("Failed to run 'docker search'.")
.lines()
.skip(1)
.map(|x| x.parse().expect("Failed to parse into API readable."))
.collect();
})
};
// Join the threads, should be replaced with a non-blocking alternative.
handle.join().unwrap();
// Get the lock for the Mutex.
let a = m.lock().unwrap();
/*
Loop through each of the images and create list items for each of them.
NOTE: &(*a) may seem redundant but is simply how you access the Mutex
in this instance.
*/
for image in &(*a) {
let image_box = gtk::Box::builder()
.orientation(gtk::Orientation::Vertical)
.build();
let is_official = gtk::Box::builder()
.orientation(gtk::Orientation::Horizontal)
.build();
is_official.append(
&Label::builder()
.css_classes(["box-label"])
.label("Official: ")
.build(),
);
/*
Use a non-interactable checkbox instead of text to display whether
the image is marked as official.
*/
is_official.append(
&CheckButton::builder()
.active(true)
.focusable(false)
.can_focus(false)
.build(),
);
// Generate a box for each property of the image.
image_box.append(&gen_box("Name", &image.name));
image_box.append(&gen_box("Description", &image.description));
image_box.append(&gen_box("Stars", &image.stars.to_string()));
image_box.append(&is_official);
let btn = Button::builder().child(&image_box).build();
list.append(&btn);
}
}

View file

@ -1,9 +1,12 @@
use gtk::{prelude::BoxExt, Stack, StackSidebar};
use images::images;
pub mod api;
pub mod browse;
pub mod images;
use browse::browse;
use images::images;
/**
Page for managing Docker containers and images.
@ -11,6 +14,15 @@ Page for managing Docker containers and images.
- Add the following pages: "Containers", "Images", "Browse"
*/
pub fn containers() -> gtk::Box {
/*
Start the docker daemon if it isn't running.
TODO: Figure out how to run a rootless docker daemon.
*/
if std::fs::metadata("/var/run/docker.pid").is_err() {
crate::pkexec("dockerd".to_owned(), true);
}
// Initialize the box that will be used as a page.
let containers_box = gtk::Box::builder()
.orientation(gtk::Orientation::Horizontal)
@ -34,11 +46,7 @@ pub fn containers() -> gtk::Box {
// Add each of the pages to the stack.
containers_stack.add_titled(&images(), Some("images"), "Images");
// Start the docker daemon if it isn't running.
if std::fs::metadata("/var/run/docker.pid").is_err() {
crate::pkexec("dockerd".to_owned(), true);
}
containers_stack.add_titled(&browse(), Some("browse"), "Browse");
/*
Append the page switcher sidebar and the stack, in that order.

View file

@ -1,5 +1,4 @@
use gtk::{
glib,
prelude::{ApplicationExt, ApplicationExtManual, BoxExt, GtkWindowExt},
style_context_add_provider_for_display, Application, ApplicationWindow,
Orientation::{Horizontal, Vertical},
@ -18,7 +17,7 @@ Initializes the application.
## Returns
Returns a `glib::ExitCode`, as given by the `app.run()` function.
*/
fn main() -> glib::ExitCode {
fn main() -> gtk::glib::ExitCode {
let app = Application::builder().application_id(APP_ID).build();
app.connect_startup(on_startup);
@ -118,6 +117,27 @@ pub fn pkexec(cmd: String, spawn: bool) -> Option<String> {
}
}
pub fn pkexec_a(cmd: Vec<&str>, spawn: bool) -> Option<String> {
if spawn {
std::process::Command::new("pkexec")
.args(cmd)
.spawn()
.expect("Failed to spawn child process!");
None
} else {
Some(
String::from_utf8(
std::process::Command::new("pkexec")
.args(cmd)
.output()
.expect("Failed to get output of command.")
.stdout,
)
.expect("Failed to convert command stdout to string!"),
)
}
}
/// Function to link `style.css` to the application.
fn on_startup(_: &Application) {
let css_provider = gtk::CssProvider::new();

View file

@ -46,3 +46,14 @@
.box-label {
font-weight: bold;
}
.images-list {
padding: 10px;
margin: 20px;
border-radius: 10px;
}
.search-btn {
padding: 5px 10px;
margin: 10px;
}