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,9 +1,10 @@
|
|||
use gtk4::prelude::*;
|
||||
use gtk4::{
|
||||
Box as GBox, Button, Label, ListBox, ListBoxRow, MessageDialog, Orientation, ScrolledWindow,
|
||||
AlertDialog, Box as GBox, Button, Label, ListBox, ListBoxRow, Orientation, ScrolledWindow,
|
||||
};
|
||||
use std::process::Command;
|
||||
|
||||
#[derive(Clone)]
|
||||
struct SnapshotRow {
|
||||
number: String,
|
||||
date: String,
|
||||
|
|
@ -22,47 +23,60 @@ fn list_snapshots() -> Vec<SnapshotRow> {
|
|||
text.lines()
|
||||
.skip(2) // header + separator
|
||||
.filter_map(|line| {
|
||||
let cols: Vec<&str> = line.splitn(3, '|').collect();
|
||||
if cols.len() == 3 {
|
||||
Some(SnapshotRow {
|
||||
number: cols[0].trim().to_string(),
|
||||
date: cols[1].trim().to_string(),
|
||||
description: cols[2].trim().to_string(),
|
||||
})
|
||||
} else {
|
||||
None
|
||||
}
|
||||
let mut cols = line.splitn(3, '|');
|
||||
Some(SnapshotRow {
|
||||
number: cols.next()?.trim().to_string(),
|
||||
date: cols.next()?.trim().to_string(),
|
||||
description: cols.next()?.trim().to_string(),
|
||||
})
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn confirm_rollback(number: &str) {
|
||||
let number = number.to_string();
|
||||
let dialog = MessageDialog::new(
|
||||
None::<>k4::Window>,
|
||||
gtk4::DialogFlags::MODAL,
|
||||
gtk4::MessageType::Question,
|
||||
gtk4::ButtonsType::OkCancel,
|
||||
&format!("Roll back to snapshot #{number}?\n\nReboot required to apply."),
|
||||
);
|
||||
dialog.connect_response(move |d, resp| {
|
||||
if resp == gtk4::ResponseType::Ok {
|
||||
let _ = Command::new("snapper")
|
||||
.args(["rollback", &number])
|
||||
.status();
|
||||
let info = MessageDialog::new(
|
||||
None::<>k4::Window>,
|
||||
gtk4::DialogFlags::MODAL,
|
||||
gtk4::MessageType::Info,
|
||||
gtk4::ButtonsType::Ok,
|
||||
"Rollback queued. Please reboot to apply.",
|
||||
);
|
||||
info.connect_response(|d, _| d.destroy());
|
||||
info.present();
|
||||
}
|
||||
d.destroy();
|
||||
});
|
||||
dialog.present();
|
||||
fn populate_list(list: &ListBox) {
|
||||
while let Some(child) = list.first_child() {
|
||||
list.remove(&child);
|
||||
}
|
||||
let snapshots = list_snapshots();
|
||||
if snapshots.is_empty() {
|
||||
let row = ListBoxRow::new();
|
||||
row.set_selectable(false);
|
||||
let lbl = Label::new(Some("No snapshots found (snapper may not be configured yet)"));
|
||||
lbl.set_margin_top(8);
|
||||
lbl.set_margin_bottom(8);
|
||||
lbl.set_margin_start(8);
|
||||
row.set_child(Some(&lbl));
|
||||
list.append(&row);
|
||||
return;
|
||||
}
|
||||
for snap in &snapshots {
|
||||
let row = ListBoxRow::new();
|
||||
row.set_widget_name(&snap.number);
|
||||
|
||||
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 num_lbl = Label::new(Some(&snap.number));
|
||||
num_lbl.set_width_chars(4);
|
||||
num_lbl.set_xalign(0.0);
|
||||
|
||||
let date_lbl = Label::new(Some(&snap.date));
|
||||
date_lbl.set_width_chars(22);
|
||||
date_lbl.set_xalign(0.0);
|
||||
|
||||
let desc_lbl = Label::new(Some(&snap.description));
|
||||
desc_lbl.set_hexpand(true);
|
||||
desc_lbl.set_xalign(0.0);
|
||||
|
||||
hbox.append(&num_lbl);
|
||||
hbox.append(&date_lbl);
|
||||
hbox.append(&desc_lbl);
|
||||
row.set_child(Some(&hbox));
|
||||
list.append(&row);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn build() -> GBox {
|
||||
|
|
@ -74,56 +88,16 @@ pub fn build() -> GBox {
|
|||
title.set_xalign(0.0);
|
||||
vbox.append(&title);
|
||||
|
||||
let subtitle =
|
||||
Label::new(Some("System snapshots created by snap-pac on each pacman transaction."));
|
||||
let subtitle = Label::new(Some(
|
||||
"System snapshots created by snap-pac on each pacman transaction.",
|
||||
));
|
||||
subtitle.set_xalign(0.0);
|
||||
subtitle.set_margin_bottom(16);
|
||||
vbox.append(&subtitle);
|
||||
|
||||
let list = ListBox::new();
|
||||
list.set_selection_mode(gtk4::SelectionMode::Single);
|
||||
|
||||
let snapshots = list_snapshots();
|
||||
if snapshots.is_empty() {
|
||||
let row = ListBoxRow::new();
|
||||
let lbl = Label::new(Some(
|
||||
"No snapshots found (snapper may not be configured yet)",
|
||||
));
|
||||
lbl.set_margin_top(8);
|
||||
lbl.set_margin_bottom(8);
|
||||
lbl.set_margin_start(8);
|
||||
row.set_child(Some(&lbl));
|
||||
list.append(&row);
|
||||
} else {
|
||||
for snap in &snapshots {
|
||||
let row = ListBoxRow::new();
|
||||
row.set_widget_name(&snap.number);
|
||||
|
||||
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 num_lbl = Label::new(Some(&snap.number));
|
||||
num_lbl.set_width_chars(4);
|
||||
num_lbl.set_xalign(0.0);
|
||||
|
||||
let date_lbl = Label::new(Some(&snap.date));
|
||||
date_lbl.set_width_chars(22);
|
||||
date_lbl.set_xalign(0.0);
|
||||
|
||||
let desc_lbl = Label::new(Some(&snap.description));
|
||||
desc_lbl.set_hexpand(true);
|
||||
desc_lbl.set_xalign(0.0);
|
||||
|
||||
hbox.append(&num_lbl);
|
||||
hbox.append(&date_lbl);
|
||||
hbox.append(&desc_lbl);
|
||||
row.set_child(Some(&hbox));
|
||||
list.append(&row);
|
||||
}
|
||||
}
|
||||
populate_list(&list);
|
||||
|
||||
let scroll = ScrolledWindow::new();
|
||||
scroll.set_vexpand(true);
|
||||
|
|
@ -133,32 +107,81 @@ pub fn build() -> GBox {
|
|||
let btn_row = GBox::new(Orientation::Horizontal, 8);
|
||||
btn_row.set_margin_top(12);
|
||||
|
||||
let refresh_btn = Button::with_label("Refresh");
|
||||
let rollback_btn = Button::with_label("Rollback to selected");
|
||||
let delete_btn = Button::with_label("Delete selected");
|
||||
delete_btn.add_css_class("destructive-action");
|
||||
|
||||
{
|
||||
let list = list.clone();
|
||||
rollback_btn.connect_clicked(move |_| {
|
||||
let Some(row) = list.selected_row() else {
|
||||
return;
|
||||
};
|
||||
let number = row.widget_name().to_string();
|
||||
confirm_rollback(&number);
|
||||
refresh_btn.connect_clicked(move |_| {
|
||||
populate_list(&list);
|
||||
});
|
||||
}
|
||||
|
||||
{
|
||||
let list = list.clone();
|
||||
delete_btn.connect_clicked(move |_| {
|
||||
let Some(row) = list.selected_row() else {
|
||||
return;
|
||||
};
|
||||
rollback_btn.connect_clicked(move |btn| {
|
||||
let Some(row) = list.selected_row() else { return };
|
||||
let number = row.widget_name().to_string();
|
||||
let _ = Command::new("snapper").args(["delete", &number]).status();
|
||||
if number.is_empty() { return }
|
||||
|
||||
let window = btn
|
||||
.root()
|
||||
.and_then(|r| r.downcast::<gtk4::Window>().ok());
|
||||
|
||||
let dialog = AlertDialog::builder()
|
||||
.message(&format!("Roll back to snapshot #{number}?"))
|
||||
.detail("The current system state will be replaced on next boot. \
|
||||
A polkit prompt will ask for your password.")
|
||||
.buttons(["Cancel", "Roll back"])
|
||||
.cancel_button(0)
|
||||
.default_button(0)
|
||||
.build();
|
||||
|
||||
dialog.choose(window.as_ref(), gtk4::gio::Cancellable::NONE, move |result| {
|
||||
if result == Ok(1) {
|
||||
// pkexec so polkit handles the privilege escalation
|
||||
std::thread::spawn(move || {
|
||||
let _ = Command::new("pkexec")
|
||||
.args(["snapper", "rollback", &number])
|
||||
.status();
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
{
|
||||
let list = list.clone();
|
||||
delete_btn.connect_clicked(move |btn| {
|
||||
let Some(row) = list.selected_row() else { return };
|
||||
let number = row.widget_name().to_string();
|
||||
if number.is_empty() { return }
|
||||
|
||||
let window = btn
|
||||
.root()
|
||||
.and_then(|r| r.downcast::<gtk4::Window>().ok());
|
||||
|
||||
let list = list.clone();
|
||||
let dialog = AlertDialog::builder()
|
||||
.message(&format!("Delete snapshot #{number}?"))
|
||||
.detail("This cannot be undone.")
|
||||
.buttons(["Cancel", "Delete"])
|
||||
.cancel_button(0)
|
||||
.default_button(0)
|
||||
.build();
|
||||
|
||||
dialog.choose(window.as_ref(), gtk4::gio::Cancellable::NONE, move |result| {
|
||||
if result == Ok(1) {
|
||||
let _ = Command::new("snapper").args(["delete", &number]).status();
|
||||
populate_list(&list);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
btn_row.append(&refresh_btn);
|
||||
btn_row.append(&rollback_btn);
|
||||
btn_row.append(&delete_btn);
|
||||
vbox.append(&btn_row);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue