Fix all issues from code/UX review
ISO structural:
- Move post-install.sh → airootfs/etc/calamares/ (it was never in the squashfs)
- Create airootfs/etc/skel/.config/ with all dotfiles (deploy path now works)
- Add iso/pacman.conf with [breadway] custom repo stub for calamares + bakery
- Add Calamares branding component (bos/branding.desc + show.qml)
- Add missing unpackfs.conf and mount.conf modules
- Add live-session autostart: getty autologin → bash_profile → Hyprland → calamares
- Add polkit rule for wheel-group snapper rollback (pkexec path)
- Remove wlroots from packages (bundled with Hyprland); add bakery to package list
- Fix modules-search path in settings.conf
Dotfiles:
- Rename dotfiles/hyprland/ → dotfiles/hypr/ (Hyprland reads ~/.config/hypr/)
- Fix deprecated shadow options: drop_shadow/shadow_range → shadow { } block
bos-settings Rust:
- Replace glib::MainContext::channel (removed in glib 0.19) with async_channel
- Stream bakery update output line-by-line instead of buffering all at once
- Fix zombie processes: per-package update buttons now wait() in a thread
- Fix sidebar/stack mismatch at startup: select snapshots row to match default view
- Replace deprecated MessageDialog with AlertDialog (GTK 4.10+) throughout
- Use pkexec for snapper rollback so polkit handles privilege escalation
- Add confirmation dialog before delete snapshot (was missing, rollback had one)
- Add refresh button + repopulate after delete in snapshots view
- Add "Saved" / "Error: …" status label to every config view save button
- Add "Remove" buttons to breadbox contexts and breadcrumbs profiles
- Remove hardcoded model string from breadpad defaults
- Drop unused state mod; fix config_dir HOME fallback; fix zombie in editor launches
https://claude.ai/code/session_01WszGHvCmxgcyTwNSkfLF9P
This commit is contained in:
parent
0ff3998c84
commit
d5913da277
32 changed files with 720 additions and 288 deletions
|
|
@ -1,31 +1,20 @@
|
|||
use async_channel;
|
||||
use gtk4::prelude::*;
|
||||
use gtk4::{
|
||||
Box as GBox, Button, Label, ListBox, ListBoxRow, Orientation, ScrolledWindow, TextView,
|
||||
};
|
||||
use serde::Deserialize;
|
||||
use std::collections::HashMap;
|
||||
use std::process::Command;
|
||||
|
||||
#[derive(Deserialize, Default)]
|
||||
struct InstalledPackages {
|
||||
#[serde(flatten)]
|
||||
packages: HashMap<String, PackageInfo>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct PackageInfo {
|
||||
version: String,
|
||||
}
|
||||
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 home = std::env::var("HOME").unwrap_or_else(|_| "/home/user".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();
|
||||
};
|
||||
|
|
@ -35,15 +24,57 @@ fn read_installed() -> HashMap<String, String> {
|
|||
.filter_map(|(name, val)| {
|
||||
let version = val
|
||||
.get("version")
|
||||
.or_else(|| val.as_str().map(|_| &val))
|
||||
.and_then(|v| v.as_str())
|
||||
.map(|s| s.to_string())
|
||||
.unwrap_or_else(|| "unknown".to_string());
|
||||
.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");
|
||||
|
|
@ -64,7 +95,10 @@ pub fn build() -> GBox {
|
|||
let packages = read_installed();
|
||||
if packages.is_empty() {
|
||||
let row = ListBoxRow::new();
|
||||
let lbl = Label::new(Some("No bakery packages found (~/.local/state/bakery/installed.json)"));
|
||||
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);
|
||||
|
|
@ -73,8 +107,10 @@ pub fn build() -> GBox {
|
|||
} 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);
|
||||
|
|
@ -88,12 +124,16 @@ pub fn build() -> GBox {
|
|||
let ver_lbl = Label::new(Some(version));
|
||||
ver_lbl.set_xalign(1.0);
|
||||
|
||||
let update_btn = Button::with_label("Update");
|
||||
// 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 |_| {
|
||||
let _ = Command::new("bakery")
|
||||
.args(["update", &pkg_name])
|
||||
.spawn();
|
||||
match Command::new("bakery").args(["update", &pkg_name]).spawn() {
|
||||
Ok(mut child) => {
|
||||
std::thread::spawn(move || { let _ = child.wait(); });
|
||||
}
|
||||
Err(e) => eprintln!("bakery update failed: {e}"),
|
||||
}
|
||||
});
|
||||
|
||||
hbox.append(&name_lbl);
|
||||
|
|
@ -109,56 +149,32 @@ pub fn build() -> GBox {
|
|||
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 = gtk4::TextBuffer::new(None);
|
||||
let log_view = TextView::with_buffer(&log_buf);
|
||||
log_view.set_editable(false);
|
||||
log_view.set_height_request(120);
|
||||
log_view.set_margin_top(8);
|
||||
|
||||
{
|
||||
let log_buf = log_buf.clone();
|
||||
check_btn.connect_clicked(move |_| {
|
||||
log_buf.set_text("Checking for updates...\n");
|
||||
match Command::new("bakery").args(["list"]).output() {
|
||||
Ok(out) => {
|
||||
let text = String::from_utf8_lossy(&out.stdout);
|
||||
log_buf.set_text(&format!("{text}\n"));
|
||||
}
|
||||
Err(e) => {
|
||||
log_buf.set_text(&format!("Error: {e}\n"));
|
||||
}
|
||||
}
|
||||
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("Running bakery update --all...\n");
|
||||
let (sender, receiver) = glib::MainContext::channel(glib::Priority::DEFAULT);
|
||||
std::thread::spawn(move || {
|
||||
let result = Command::new("bakery")
|
||||
.args(["update", "--all"])
|
||||
.output();
|
||||
match result {
|
||||
Ok(out) => {
|
||||
let _ = sender.send(String::from_utf8_lossy(&out.stdout).to_string());
|
||||
}
|
||||
Err(e) => {
|
||||
let _ = sender.send(format!("Error: {e}\n"));
|
||||
}
|
||||
}
|
||||
});
|
||||
receiver.attach(None, move |msg| {
|
||||
log_buf.set_text(&msg);
|
||||
glib::ControlFlow::Break
|
||||
});
|
||||
log_buf.set_text("");
|
||||
stream_command(&["bakery", "update", "--all"], log_buf.clone());
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue