Added functionality for browsing packages and documented it.
This commit is contained in:
parent
af8365675d
commit
941f66662e
6 changed files with 259 additions and 9 deletions
|
@ -1,3 +1,4 @@
|
|||
cargo
|
||||
gtk4-devel
|
||||
docker
|
||||
fuse-overlayfs
|
||||
|
|
|
@ -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
162
src/containers/browse.rs
Normal 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);
|
||||
}
|
||||
}
|
|
@ -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.
|
||||
|
|
24
src/main.rs
24
src/main.rs
|
@ -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();
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue