bos/bos-settings/src/ui/views/packages.rs
Breadway 8682698402 Fix prod-readiness issues flagged in audit
- Fix XDG config dir logic in config/mod.rs (was double-nesting and had /home/user hardcode)
- Replace /home/user hardcodes in breadbar.rs and hyprland.rs with config::config_dir()
- Fix /home/user hardcode in packages.rs (uses /root fallback for .local/state path)
- Remove eprintln! from GTK callback in packages.rs (no stderr at runtime)
- Fix YAML parse error in branding.desc (missing space after sidebarTextHighlight key)
- Add .gitignore (Rust target/, ISO artifacts, editor/OS junk, secrets)
- Delete state.rs (dead code — never mod'd in main.rs)
- Add brightnessctl, grim, slurp to packages.x86_64 (used by keybinds)
- Rename can-you-begin-a-composed-beacon.md → DESIGN.md

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-13 11:29:53 +08:00

187 lines
5.7 KiB
Rust

use async_channel;
use gtk4::prelude::*;
use gtk4::{
Box as GBox, Button, Label, ListBox, ListBoxRow, Orientation, ScrolledWindow, TextView,
};
use std::collections::HashMap;
use std::io::{BufRead, BufReader};
use std::process::{Command, Stdio};
fn read_installed() -> HashMap<String, String> {
let home = std::env::var("HOME").unwrap_or_else(|_| "/root".to_string());
let path = std::path::Path::new(&home)
.join(".local/state/bakery/installed.json");
let Ok(text) = std::fs::read_to_string(&path) else {
return HashMap::new();
};
let Ok(parsed) = serde_json::from_str::<HashMap<String, serde_json::Value>>(&text) else {
return HashMap::new();
};
parsed
.into_iter()
.filter_map(|(name, val)| {
let version = val
.get("version")
.and_then(|v| v.as_str())
.unwrap_or("unknown")
.to_string();
Some((name, version))
})
.collect()
}
fn stream_command(args: &[&str], log_buf: gtk4::TextBuffer) {
let (sender, receiver) = async_channel::bounded::<String>(256);
let args: Vec<String> = args.iter().map(|s| s.to_string()).collect();
std::thread::spawn(move || {
let mut child = match Command::new(&args[0])
.args(&args[1..])
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()
{
Ok(c) => c,
Err(e) => {
let _ = sender.send_blocking(format!("Error: {e}"));
return;
}
};
// Merge stderr into the channel too
let stdout = child.stdout.take().unwrap();
let stderr = child.stderr.take().unwrap();
let tx2 = sender.clone();
std::thread::spawn(move || {
for line in BufReader::new(stderr).lines().flatten() {
let _ = tx2.send_blocking(line);
}
});
for line in BufReader::new(stdout).lines().flatten() {
let _ = sender.send_blocking(line);
}
let _ = child.wait();
});
glib::spawn_future_local(async move {
while let Ok(line) = receiver.recv().await {
let mut end = log_buf.end_iter();
log_buf.insert(&mut end, &format!("{line}\n"));
}
});
}
pub fn build() -> GBox {
let vbox = GBox::new(Orientation::Vertical, 0);
vbox.add_css_class("view-content");
let title = Label::new(Some("Packages"));
title.add_css_class("title");
title.set_xalign(0.0);
vbox.append(&title);
let subtitle = Label::new(Some("Bread ecosystem packages installed via bakery."));
subtitle.set_xalign(0.0);
subtitle.set_margin_bottom(16);
vbox.append(&subtitle);
let list = ListBox::new();
list.set_selection_mode(gtk4::SelectionMode::None);
let packages = read_installed();
if packages.is_empty() {
let row = ListBoxRow::new();
row.set_selectable(false);
let lbl = Label::new(Some(
"No bakery packages found (~/.local/state/bakery/installed.json)",
));
lbl.set_margin_top(8);
lbl.set_margin_bottom(8);
lbl.set_margin_start(8);
row.set_child(Some(&lbl));
list.append(&row);
} else {
let mut names: Vec<_> = packages.iter().collect();
names.sort_by_key(|(k, _)| k.as_str());
for (name, version) in names {
let row = ListBoxRow::new();
row.set_selectable(false);
let hbox = GBox::new(Orientation::Horizontal, 16);
hbox.set_margin_top(6);
hbox.set_margin_bottom(6);
hbox.set_margin_start(8);
hbox.set_margin_end(8);
let name_lbl = Label::new(Some(name));
name_lbl.set_hexpand(true);
name_lbl.set_xalign(0.0);
let ver_lbl = Label::new(Some(version));
ver_lbl.set_xalign(1.0);
// Spawn a thread to reap the child process — no zombies
let pkg_name = name.clone();
let update_btn = Button::with_label("Update");
update_btn.connect_clicked(move |_| {
match Command::new("bakery").args(["update", &pkg_name]).spawn() {
Ok(mut child) => {
std::thread::spawn(move || { let _ = child.wait(); });
}
Err(_) => {} // bakery not found; button is a no-op
}
});
hbox.append(&name_lbl);
hbox.append(&ver_lbl);
hbox.append(&update_btn);
row.set_child(Some(&hbox));
list.append(&row);
}
}
let scroll = ScrolledWindow::new();
scroll.set_vexpand(true);
scroll.set_child(Some(&list));
vbox.append(&scroll);
let log_buf = gtk4::TextBuffer::new(None);
let log_view = TextView::with_buffer(&log_buf);
log_view.set_editable(false);
log_view.set_monospace(true);
log_view.set_height_request(140);
log_view.set_margin_top(8);
let btn_row = GBox::new(Orientation::Horizontal, 8);
btn_row.set_margin_top(12);
let check_btn = Button::with_label("Check for updates");
let update_all_btn = Button::with_label("Update all");
{
let log_buf = log_buf.clone();
check_btn.connect_clicked(move |_| {
log_buf.set_text("");
stream_command(&["bakery", "list"], log_buf.clone());
});
}
{
let log_buf = log_buf.clone();
update_all_btn.connect_clicked(move |_| {
log_buf.set_text("");
stream_command(&["bakery", "update", "--all"], log_buf.clone());
});
}
btn_row.append(&check_btn);
btn_row.append(&update_all_btn);
vbox.append(&btn_row);
vbox.append(&log_view);
vbox
}