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
|
|
@ -9,3 +9,4 @@ glib = "0.20"
|
||||||
serde = { version = "1", features = ["derive"] }
|
serde = { version = "1", features = ["derive"] }
|
||||||
serde_json = "1"
|
serde_json = "1"
|
||||||
toml = "0.8"
|
toml = "0.8"
|
||||||
|
async-channel = "2"
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
use std::error::Error;
|
use std::error::Error;
|
||||||
use std::path::Path;
|
use std::path::{Path, PathBuf};
|
||||||
|
|
||||||
pub fn load<T: for<'de> serde::Deserialize<'de>>(path: &Path) -> Result<T, Box<dyn Error>> {
|
pub fn load<T: for<'de> serde::Deserialize<'de>>(path: &Path) -> Result<T, Box<dyn Error>> {
|
||||||
let text = std::fs::read_to_string(path)?;
|
let text = std::fs::read_to_string(path)?;
|
||||||
|
|
@ -14,11 +14,11 @@ pub fn save<T: serde::Serialize>(path: &Path, val: &T) -> Result<(), Box<dyn Err
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn config_dir() -> std::path::PathBuf {
|
pub fn config_dir() -> PathBuf {
|
||||||
dirs_path()
|
let home = std::env::var("HOME").unwrap_or_else(|_| {
|
||||||
}
|
std::env::var("XDG_CONFIG_HOME")
|
||||||
|
.map(|p| PathBuf::from(p).parent().unwrap_or(Path::new("/")).to_string_lossy().to_string())
|
||||||
fn dirs_path() -> std::path::PathBuf {
|
.unwrap_or_else(|_| "/home/user".to_string())
|
||||||
let home = std::env::var("HOME").unwrap_or_else(|_| "/root".to_string());
|
});
|
||||||
std::path::PathBuf::from(home).join(".config")
|
PathBuf::from(home).join(".config")
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,4 @@
|
||||||
mod config;
|
mod config;
|
||||||
mod state;
|
|
||||||
mod theme;
|
mod theme;
|
||||||
mod ui;
|
mod ui;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
use gtk4::prelude::*;
|
use gtk4::prelude::*;
|
||||||
use gtk4::{Box as GBox, Label, ListBox, ListBoxRow, Orientation, Separator};
|
use gtk4::{Box as GBox, Label, ListBox, ListBoxRow, Orientation};
|
||||||
|
|
||||||
pub struct SidebarItem {
|
pub struct SidebarItem {
|
||||||
pub id: &'static str,
|
pub id: &'static str,
|
||||||
|
|
@ -7,17 +7,17 @@ pub struct SidebarItem {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub const APPS_ITEMS: &[SidebarItem] = &[
|
pub const APPS_ITEMS: &[SidebarItem] = &[
|
||||||
SidebarItem { id: "bread", label: "bread" },
|
SidebarItem { id: "bread", label: "bread" },
|
||||||
SidebarItem { id: "breadbar", label: "breadbar" },
|
SidebarItem { id: "breadbar", label: "breadbar" },
|
||||||
SidebarItem { id: "breadbox", label: "breadbox" },
|
SidebarItem { id: "breadbox", label: "breadbox" },
|
||||||
SidebarItem { id: "breadcrumbs", label: "breadcrumbs" },
|
SidebarItem { id: "breadcrumbs", label: "breadcrumbs" },
|
||||||
SidebarItem { id: "breadpad", label: "breadpad" },
|
SidebarItem { id: "breadpad", label: "breadpad" },
|
||||||
];
|
];
|
||||||
|
|
||||||
pub const SYSTEM_ITEMS: &[SidebarItem] = &[
|
pub const SYSTEM_ITEMS: &[SidebarItem] = &[
|
||||||
SidebarItem { id: "snapshots", label: "Snapshots" },
|
SidebarItem { id: "snapshots", label: "Snapshots" },
|
||||||
SidebarItem { id: "packages", label: "Packages" },
|
SidebarItem { id: "packages", label: "Packages" },
|
||||||
SidebarItem { id: "hyprland", label: "Hyprland" },
|
SidebarItem { id: "hyprland", label: "Hyprland" },
|
||||||
];
|
];
|
||||||
|
|
||||||
pub fn build() -> (GBox, ListBox) {
|
pub fn build() -> (GBox, ListBox) {
|
||||||
|
|
@ -32,9 +32,17 @@ pub fn build() -> (GBox, ListBox) {
|
||||||
append_section(&list, "Apps", APPS_ITEMS);
|
append_section(&list, "Apps", APPS_ITEMS);
|
||||||
append_section(&list, "System", SYSTEM_ITEMS);
|
append_section(&list, "System", SYSTEM_ITEMS);
|
||||||
|
|
||||||
// Select first item by default
|
// Select the snapshots row so it matches the default stack page
|
||||||
if let Some(first) = list.row_at_index(1) {
|
let mut i = 0;
|
||||||
list.select_row(Some(&first));
|
loop {
|
||||||
|
match list.row_at_index(i) {
|
||||||
|
None => break,
|
||||||
|
Some(row) if row.widget_name() == "snapshots" => {
|
||||||
|
list.select_row(Some(&row));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
_ => i += 1,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
vbox.append(&list);
|
vbox.append(&list);
|
||||||
|
|
@ -42,7 +50,6 @@ pub fn build() -> (GBox, ListBox) {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn append_section(list: &ListBox, title: &str, items: &[SidebarItem]) {
|
fn append_section(list: &ListBox, title: &str, items: &[SidebarItem]) {
|
||||||
// Section header (non-selectable)
|
|
||||||
let header_row = ListBoxRow::new();
|
let header_row = ListBoxRow::new();
|
||||||
header_row.set_selectable(false);
|
header_row.set_selectable(false);
|
||||||
header_row.set_activatable(false);
|
header_row.set_activatable(false);
|
||||||
|
|
@ -55,7 +62,6 @@ fn append_section(list: &ListBox, title: &str, items: &[SidebarItem]) {
|
||||||
for item in items {
|
for item in items {
|
||||||
let row = ListBoxRow::new();
|
let row = ListBoxRow::new();
|
||||||
row.set_widget_name(item.id);
|
row.set_widget_name(item.id);
|
||||||
|
|
||||||
let lbl = Label::new(Some(item.label));
|
let lbl = Label::new(Some(item.label));
|
||||||
lbl.set_xalign(0.0);
|
lbl.set_xalign(0.0);
|
||||||
lbl.set_margin_top(2);
|
lbl.set_margin_top(2);
|
||||||
|
|
|
||||||
|
|
@ -14,34 +14,22 @@ pub struct BreadConfig {
|
||||||
pub adapters: AdaptersConfig,
|
pub adapters: AdaptersConfig,
|
||||||
}
|
}
|
||||||
|
|
||||||
fn default_log_level() -> String {
|
fn default_log_level() -> String { "info".to_string() }
|
||||||
"info".to_string()
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Deserialize, Serialize, Clone, Default)]
|
#[derive(Deserialize, Serialize, Clone, Default)]
|
||||||
pub struct AdaptersConfig {
|
pub struct AdaptersConfig {
|
||||||
#[serde(default = "default_true")]
|
#[serde(default = "default_true")] pub keyboard: bool,
|
||||||
pub keyboard: bool,
|
#[serde(default = "default_true")] pub mouse: bool,
|
||||||
#[serde(default = "default_true")]
|
#[serde(default = "default_true")] pub touchpad: bool,
|
||||||
pub mouse: bool,
|
#[serde(default = "default_true")] pub bluetooth: bool,
|
||||||
#[serde(default = "default_true")]
|
#[serde(default = "default_true")] pub gamepad: bool,
|
||||||
pub touchpad: bool,
|
|
||||||
#[serde(default = "default_true")]
|
|
||||||
pub bluetooth: bool,
|
|
||||||
#[serde(default = "default_true")]
|
|
||||||
pub gamepad: bool,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn default_true() -> bool {
|
fn default_true() -> bool { true }
|
||||||
true
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Default for BreadConfig {
|
impl Default for BreadConfig {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
Self {
|
Self { log_level: default_log_level(), adapters: AdaptersConfig::default() }
|
||||||
log_level: default_log_level(),
|
|
||||||
adapters: AdaptersConfig::default(),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -49,7 +37,12 @@ fn config_path() -> std::path::PathBuf {
|
||||||
config::config_dir().join("bread/breadd.toml")
|
config::config_dir().join("bread/breadd.toml")
|
||||||
}
|
}
|
||||||
|
|
||||||
fn adapter_row(label: &str, active: bool, cfg: Rc<RefCell<BreadConfig>>, field: &'static str) -> GBox {
|
fn adapter_row(
|
||||||
|
label: &str,
|
||||||
|
active: bool,
|
||||||
|
cfg: Rc<RefCell<BreadConfig>>,
|
||||||
|
field: &'static str,
|
||||||
|
) -> GBox {
|
||||||
let row = GBox::new(Orientation::Horizontal, 16);
|
let row = GBox::new(Orientation::Horizontal, 16);
|
||||||
let lbl = Label::new(Some(label));
|
let lbl = Label::new(Some(label));
|
||||||
lbl.set_hexpand(true);
|
lbl.set_hexpand(true);
|
||||||
|
|
@ -60,11 +53,11 @@ fn adapter_row(label: &str, active: bool, cfg: Rc<RefCell<BreadConfig>>, field:
|
||||||
let val = s.is_active();
|
let val = s.is_active();
|
||||||
let mut c = cfg.borrow_mut();
|
let mut c = cfg.borrow_mut();
|
||||||
match field {
|
match field {
|
||||||
"keyboard" => c.adapters.keyboard = val,
|
"keyboard" => c.adapters.keyboard = val,
|
||||||
"mouse" => c.adapters.mouse = val,
|
"mouse" => c.adapters.mouse = val,
|
||||||
"touchpad" => c.adapters.touchpad = val,
|
"touchpad" => c.adapters.touchpad = val,
|
||||||
"bluetooth" => c.adapters.bluetooth = val,
|
"bluetooth" => c.adapters.bluetooth = val,
|
||||||
"gamepad" => c.adapters.gamepad = val,
|
"gamepad" => c.adapters.gamepad = val,
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
@ -94,15 +87,10 @@ pub fn build() -> GBox {
|
||||||
lbl.set_xalign(0.0);
|
lbl.set_xalign(0.0);
|
||||||
let levels = StringList::new(&["error", "warn", "info", "debug", "trace"]);
|
let levels = StringList::new(&["error", "warn", "info", "debug", "trace"]);
|
||||||
let dropdown = DropDown::new(Some(levels), gtk4::Expression::NONE);
|
let dropdown = DropDown::new(Some(levels), gtk4::Expression::NONE);
|
||||||
let current_pos = match cfg.borrow().log_level.as_str() {
|
let pos = match cfg.borrow().log_level.as_str() {
|
||||||
"error" => 0u32,
|
"error" => 0u32, "warn" => 1, "info" => 2, "debug" => 3, "trace" => 4, _ => 2,
|
||||||
"warn" => 1,
|
|
||||||
"info" => 2,
|
|
||||||
"debug" => 3,
|
|
||||||
"trace" => 4,
|
|
||||||
_ => 2,
|
|
||||||
};
|
};
|
||||||
dropdown.set_selected(current_pos);
|
dropdown.set_selected(pos);
|
||||||
{
|
{
|
||||||
let cfg = cfg.clone();
|
let cfg = cfg.clone();
|
||||||
dropdown.connect_selected_notify(move |dd| {
|
dropdown.connect_selected_notify(move |dd| {
|
||||||
|
|
@ -116,7 +104,6 @@ pub fn build() -> GBox {
|
||||||
row.append(&dropdown);
|
row.append(&dropdown);
|
||||||
vbox.append(&row);
|
vbox.append(&row);
|
||||||
|
|
||||||
// Adapter toggles
|
|
||||||
let adapter_label = Label::new(Some("Adapters"));
|
let adapter_label = Label::new(Some("Adapters"));
|
||||||
adapter_label.set_xalign(0.0);
|
adapter_label.set_xalign(0.0);
|
||||||
adapter_label.set_margin_top(8);
|
adapter_label.set_margin_top(8);
|
||||||
|
|
@ -125,25 +112,44 @@ pub fn build() -> GBox {
|
||||||
|
|
||||||
let (kbd, mouse, touchpad, bluetooth, gamepad) = {
|
let (kbd, mouse, touchpad, bluetooth, gamepad) = {
|
||||||
let c = cfg.borrow();
|
let c = cfg.borrow();
|
||||||
(c.adapters.keyboard, c.adapters.mouse, c.adapters.touchpad, c.adapters.bluetooth, c.adapters.gamepad)
|
(c.adapters.keyboard, c.adapters.mouse, c.adapters.touchpad,
|
||||||
|
c.adapters.bluetooth, c.adapters.gamepad)
|
||||||
};
|
};
|
||||||
|
vbox.append(&adapter_row("Keyboard", kbd, cfg.clone(), "keyboard"));
|
||||||
vbox.append(&adapter_row("Keyboard", kbd, cfg.clone(), "keyboard"));
|
vbox.append(&adapter_row("Mouse", mouse, cfg.clone(), "mouse"));
|
||||||
vbox.append(&adapter_row("Mouse", mouse, cfg.clone(), "mouse"));
|
vbox.append(&adapter_row("Touchpad", touchpad, cfg.clone(), "touchpad"));
|
||||||
vbox.append(&adapter_row("Touchpad", touchpad, cfg.clone(), "touchpad"));
|
|
||||||
vbox.append(&adapter_row("Bluetooth", bluetooth, cfg.clone(), "bluetooth"));
|
vbox.append(&adapter_row("Bluetooth", bluetooth, cfg.clone(), "bluetooth"));
|
||||||
vbox.append(&adapter_row("Gamepad", gamepad, cfg.clone(), "gamepad"));
|
vbox.append(&adapter_row("Gamepad", gamepad, cfg.clone(), "gamepad"));
|
||||||
|
|
||||||
|
let btn_row = GBox::new(Orientation::Horizontal, 12);
|
||||||
|
btn_row.set_margin_top(16);
|
||||||
|
|
||||||
let save_btn = Button::with_label("Save");
|
let save_btn = Button::with_label("Save");
|
||||||
save_btn.set_margin_top(16);
|
let status_lbl = Label::new(None);
|
||||||
save_btn.set_halign(gtk4::Align::Start);
|
status_lbl.add_css_class("dim-label");
|
||||||
|
|
||||||
{
|
{
|
||||||
let cfg = cfg.clone();
|
let cfg = cfg.clone();
|
||||||
|
let path = path.clone();
|
||||||
|
let status_lbl = status_lbl.clone();
|
||||||
save_btn.connect_clicked(move |_| {
|
save_btn.connect_clicked(move |_| {
|
||||||
let _ = config::save(&path, &*cfg.borrow());
|
match config::save(&path, &*cfg.borrow()) {
|
||||||
|
Ok(()) => {
|
||||||
|
status_lbl.set_text("Saved");
|
||||||
|
let lbl = status_lbl.clone();
|
||||||
|
glib::timeout_add_seconds_local(3, move || {
|
||||||
|
lbl.set_text("");
|
||||||
|
glib::ControlFlow::Break
|
||||||
|
});
|
||||||
|
}
|
||||||
|
Err(e) => status_lbl.set_text(&format!("Error: {e}")),
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
vbox.append(&save_btn);
|
|
||||||
|
btn_row.append(&save_btn);
|
||||||
|
btn_row.append(&status_lbl);
|
||||||
|
vbox.append(&btn_row);
|
||||||
|
|
||||||
vbox
|
vbox
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@ use gtk4::{Box as GBox, Button, Label, Orientation, ScrolledWindow, TextView};
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
|
|
||||||
fn css_path() -> PathBuf {
|
fn css_path() -> PathBuf {
|
||||||
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());
|
||||||
PathBuf::from(home).join(".config/breadbar/style.css")
|
PathBuf::from(home).join(".config/breadbar/style.css")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -38,21 +38,39 @@ pub fn build() -> GBox {
|
||||||
scroll.set_child(Some(&text_view));
|
scroll.set_child(Some(&text_view));
|
||||||
vbox.append(&scroll);
|
vbox.append(&scroll);
|
||||||
|
|
||||||
|
let btn_row = GBox::new(Orientation::Horizontal, 12);
|
||||||
|
btn_row.set_margin_top(12);
|
||||||
|
|
||||||
let save_btn = Button::with_label("Save");
|
let save_btn = Button::with_label("Save");
|
||||||
save_btn.set_margin_top(12);
|
let status_lbl = Label::new(None);
|
||||||
save_btn.set_halign(gtk4::Align::Start);
|
status_lbl.add_css_class("dim-label");
|
||||||
|
|
||||||
{
|
{
|
||||||
let path = path.clone();
|
let path = path.clone();
|
||||||
|
let status_lbl = status_lbl.clone();
|
||||||
save_btn.connect_clicked(move |_| {
|
save_btn.connect_clicked(move |_| {
|
||||||
let (start, end) = buf.bounds();
|
let (start, end) = buf.bounds();
|
||||||
let text = buf.text(&start, &end, false);
|
let text = buf.text(&start, &end, false);
|
||||||
if let Some(parent) = path.parent() {
|
if let Some(parent) = path.parent() {
|
||||||
let _ = std::fs::create_dir_all(parent);
|
let _ = std::fs::create_dir_all(parent);
|
||||||
}
|
}
|
||||||
let _ = std::fs::write(&path, text.as_str());
|
match std::fs::write(&path, text.as_str()) {
|
||||||
|
Ok(()) => {
|
||||||
|
status_lbl.set_text("Saved");
|
||||||
|
let lbl = status_lbl.clone();
|
||||||
|
glib::timeout_add_seconds_local(3, move || {
|
||||||
|
lbl.set_text("");
|
||||||
|
glib::ControlFlow::Break
|
||||||
|
});
|
||||||
|
}
|
||||||
|
Err(e) => status_lbl.set_text(&format!("Error: {e}")),
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
vbox.append(&save_btn);
|
|
||||||
|
btn_row.append(&save_btn);
|
||||||
|
btn_row.append(&status_lbl);
|
||||||
|
vbox.append(&btn_row);
|
||||||
|
|
||||||
vbox
|
vbox
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -29,6 +29,8 @@ fn rebuild_list(list: &ListBox, cfg: &Rc<RefCell<BreadboxConfig>>) {
|
||||||
}
|
}
|
||||||
for (i, ctx) in cfg.borrow().context.iter().enumerate() {
|
for (i, ctx) in cfg.borrow().context.iter().enumerate() {
|
||||||
let row = ListBoxRow::new();
|
let row = ListBoxRow::new();
|
||||||
|
row.set_selectable(false);
|
||||||
|
|
||||||
let hbox = GBox::new(Orientation::Horizontal, 8);
|
let hbox = GBox::new(Orientation::Horizontal, 8);
|
||||||
hbox.set_margin_top(6);
|
hbox.set_margin_top(6);
|
||||||
hbox.set_margin_bottom(6);
|
hbox.set_margin_bottom(6);
|
||||||
|
|
@ -37,13 +39,17 @@ fn rebuild_list(list: &ListBox, cfg: &Rc<RefCell<BreadboxConfig>>) {
|
||||||
|
|
||||||
let name_entry = Entry::new();
|
let name_entry = Entry::new();
|
||||||
name_entry.set_text(&ctx.name);
|
name_entry.set_text(&ctx.name);
|
||||||
name_entry.set_width_chars(16);
|
name_entry.set_width_chars(14);
|
||||||
|
name_entry.set_placeholder_text(Some("name"));
|
||||||
|
|
||||||
let apps_entry = Entry::new();
|
let apps_entry = Entry::new();
|
||||||
apps_entry.set_text(&ctx.apps.join(", "));
|
apps_entry.set_text(&ctx.apps.join(", "));
|
||||||
apps_entry.set_hexpand(true);
|
apps_entry.set_hexpand(true);
|
||||||
apps_entry.set_placeholder_text(Some("app1, app2, ..."));
|
apps_entry.set_placeholder_text(Some("app1, app2, ..."));
|
||||||
|
|
||||||
|
let remove_btn = Button::with_label("Remove");
|
||||||
|
remove_btn.add_css_class("destructive-action");
|
||||||
|
|
||||||
{
|
{
|
||||||
let cfg = cfg.clone();
|
let cfg = cfg.clone();
|
||||||
name_entry.connect_changed(move |e| {
|
name_entry.connect_changed(move |e| {
|
||||||
|
|
@ -56,8 +62,7 @@ fn rebuild_list(list: &ListBox, cfg: &Rc<RefCell<BreadboxConfig>>) {
|
||||||
let cfg = cfg.clone();
|
let cfg = cfg.clone();
|
||||||
apps_entry.connect_changed(move |e| {
|
apps_entry.connect_changed(move |e| {
|
||||||
if let Some(c) = cfg.borrow_mut().context.get_mut(i) {
|
if let Some(c) = cfg.borrow_mut().context.get_mut(i) {
|
||||||
c.apps = e
|
c.apps = e.text()
|
||||||
.text()
|
|
||||||
.split(',')
|
.split(',')
|
||||||
.map(|s| s.trim().to_string())
|
.map(|s| s.trim().to_string())
|
||||||
.filter(|s| !s.is_empty())
|
.filter(|s| !s.is_empty())
|
||||||
|
|
@ -65,9 +70,18 @@ fn rebuild_list(list: &ListBox, cfg: &Rc<RefCell<BreadboxConfig>>) {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
{
|
||||||
|
let cfg = cfg.clone();
|
||||||
|
let list = list.clone();
|
||||||
|
remove_btn.connect_clicked(move |_| {
|
||||||
|
cfg.borrow_mut().context.remove(i);
|
||||||
|
rebuild_list(&list, &cfg);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
hbox.append(&name_entry);
|
hbox.append(&name_entry);
|
||||||
hbox.append(&apps_entry);
|
hbox.append(&apps_entry);
|
||||||
|
hbox.append(&remove_btn);
|
||||||
row.set_child(Some(&hbox));
|
row.set_child(Some(&hbox));
|
||||||
list.append(&row);
|
list.append(&row);
|
||||||
}
|
}
|
||||||
|
|
@ -93,7 +107,6 @@ pub fn build() -> GBox {
|
||||||
|
|
||||||
let list = ListBox::new();
|
let list = ListBox::new();
|
||||||
list.set_selection_mode(gtk4::SelectionMode::None);
|
list.set_selection_mode(gtk4::SelectionMode::None);
|
||||||
|
|
||||||
rebuild_list(&list, &cfg);
|
rebuild_list(&list, &cfg);
|
||||||
|
|
||||||
let scroll = ScrolledWindow::new();
|
let scroll = ScrolledWindow::new();
|
||||||
|
|
@ -118,16 +131,31 @@ pub fn build() -> GBox {
|
||||||
}
|
}
|
||||||
|
|
||||||
let save_btn = Button::with_label("Save");
|
let save_btn = Button::with_label("Save");
|
||||||
|
let status_lbl = Label::new(None);
|
||||||
|
status_lbl.add_css_class("dim-label");
|
||||||
|
|
||||||
{
|
{
|
||||||
let cfg = cfg.clone();
|
let cfg = cfg.clone();
|
||||||
let path = path.clone();
|
let path = path.clone();
|
||||||
|
let status_lbl = status_lbl.clone();
|
||||||
save_btn.connect_clicked(move |_| {
|
save_btn.connect_clicked(move |_| {
|
||||||
let _ = config::save(&path, &*cfg.borrow());
|
match config::save(&path, &*cfg.borrow()) {
|
||||||
|
Ok(()) => {
|
||||||
|
status_lbl.set_text("Saved");
|
||||||
|
let lbl = status_lbl.clone();
|
||||||
|
glib::timeout_add_seconds_local(3, move || {
|
||||||
|
lbl.set_text("");
|
||||||
|
glib::ControlFlow::Break
|
||||||
|
});
|
||||||
|
}
|
||||||
|
Err(e) => status_lbl.set_text(&format!("Error: {e}")),
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
btn_row.append(&add_btn);
|
btn_row.append(&add_btn);
|
||||||
btn_row.append(&save_btn);
|
btn_row.append(&save_btn);
|
||||||
|
btn_row.append(&status_lbl);
|
||||||
vbox.append(&btn_row);
|
vbox.append(&btn_row);
|
||||||
|
|
||||||
vbox
|
vbox
|
||||||
|
|
|
||||||
|
|
@ -29,6 +29,8 @@ fn rebuild_list(list: &ListBox, cfg: &Rc<RefCell<BreadcrumbsConfig>>) {
|
||||||
}
|
}
|
||||||
for (i, profile) in cfg.borrow().profile.iter().enumerate() {
|
for (i, profile) in cfg.borrow().profile.iter().enumerate() {
|
||||||
let row = ListBoxRow::new();
|
let row = ListBoxRow::new();
|
||||||
|
row.set_selectable(false);
|
||||||
|
|
||||||
let hbox = GBox::new(Orientation::Horizontal, 8);
|
let hbox = GBox::new(Orientation::Horizontal, 8);
|
||||||
hbox.set_margin_top(6);
|
hbox.set_margin_top(6);
|
||||||
hbox.set_margin_bottom(6);
|
hbox.set_margin_bottom(6);
|
||||||
|
|
@ -37,13 +39,17 @@ fn rebuild_list(list: &ListBox, cfg: &Rc<RefCell<BreadcrumbsConfig>>) {
|
||||||
|
|
||||||
let name_entry = Entry::new();
|
let name_entry = Entry::new();
|
||||||
name_entry.set_text(&profile.name);
|
name_entry.set_text(&profile.name);
|
||||||
name_entry.set_width_chars(16);
|
name_entry.set_width_chars(14);
|
||||||
|
name_entry.set_placeholder_text(Some("name"));
|
||||||
|
|
||||||
let ssids_entry = Entry::new();
|
let ssids_entry = Entry::new();
|
||||||
ssids_entry.set_text(&profile.ssids.join(", "));
|
ssids_entry.set_text(&profile.ssids.join(", "));
|
||||||
ssids_entry.set_hexpand(true);
|
ssids_entry.set_hexpand(true);
|
||||||
ssids_entry.set_placeholder_text(Some("SSID1, SSID2, ..."));
|
ssids_entry.set_placeholder_text(Some("SSID1, SSID2, ..."));
|
||||||
|
|
||||||
|
let remove_btn = Button::with_label("Remove");
|
||||||
|
remove_btn.add_css_class("destructive-action");
|
||||||
|
|
||||||
{
|
{
|
||||||
let cfg = cfg.clone();
|
let cfg = cfg.clone();
|
||||||
name_entry.connect_changed(move |e| {
|
name_entry.connect_changed(move |e| {
|
||||||
|
|
@ -56,8 +62,7 @@ fn rebuild_list(list: &ListBox, cfg: &Rc<RefCell<BreadcrumbsConfig>>) {
|
||||||
let cfg = cfg.clone();
|
let cfg = cfg.clone();
|
||||||
ssids_entry.connect_changed(move |e| {
|
ssids_entry.connect_changed(move |e| {
|
||||||
if let Some(p) = cfg.borrow_mut().profile.get_mut(i) {
|
if let Some(p) = cfg.borrow_mut().profile.get_mut(i) {
|
||||||
p.ssids = e
|
p.ssids = e.text()
|
||||||
.text()
|
|
||||||
.split(',')
|
.split(',')
|
||||||
.map(|s| s.trim().to_string())
|
.map(|s| s.trim().to_string())
|
||||||
.filter(|s| !s.is_empty())
|
.filter(|s| !s.is_empty())
|
||||||
|
|
@ -65,9 +70,18 @@ fn rebuild_list(list: &ListBox, cfg: &Rc<RefCell<BreadcrumbsConfig>>) {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
{
|
||||||
|
let cfg = cfg.clone();
|
||||||
|
let list = list.clone();
|
||||||
|
remove_btn.connect_clicked(move |_| {
|
||||||
|
cfg.borrow_mut().profile.remove(i);
|
||||||
|
rebuild_list(&list, &cfg);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
hbox.append(&name_entry);
|
hbox.append(&name_entry);
|
||||||
hbox.append(&ssids_entry);
|
hbox.append(&ssids_entry);
|
||||||
|
hbox.append(&remove_btn);
|
||||||
row.set_child(Some(&hbox));
|
row.set_child(Some(&hbox));
|
||||||
list.append(&row);
|
list.append(&row);
|
||||||
}
|
}
|
||||||
|
|
@ -93,7 +107,6 @@ pub fn build() -> GBox {
|
||||||
|
|
||||||
let list = ListBox::new();
|
let list = ListBox::new();
|
||||||
list.set_selection_mode(gtk4::SelectionMode::None);
|
list.set_selection_mode(gtk4::SelectionMode::None);
|
||||||
|
|
||||||
rebuild_list(&list, &cfg);
|
rebuild_list(&list, &cfg);
|
||||||
|
|
||||||
let scroll = ScrolledWindow::new();
|
let scroll = ScrolledWindow::new();
|
||||||
|
|
@ -118,16 +131,31 @@ pub fn build() -> GBox {
|
||||||
}
|
}
|
||||||
|
|
||||||
let save_btn = Button::with_label("Save");
|
let save_btn = Button::with_label("Save");
|
||||||
|
let status_lbl = Label::new(None);
|
||||||
|
status_lbl.add_css_class("dim-label");
|
||||||
|
|
||||||
{
|
{
|
||||||
let cfg = cfg.clone();
|
let cfg = cfg.clone();
|
||||||
let path = path.clone();
|
let path = path.clone();
|
||||||
|
let status_lbl = status_lbl.clone();
|
||||||
save_btn.connect_clicked(move |_| {
|
save_btn.connect_clicked(move |_| {
|
||||||
let _ = config::save(&path, &*cfg.borrow());
|
match config::save(&path, &*cfg.borrow()) {
|
||||||
|
Ok(()) => {
|
||||||
|
status_lbl.set_text("Saved");
|
||||||
|
let lbl = status_lbl.clone();
|
||||||
|
glib::timeout_add_seconds_local(3, move || {
|
||||||
|
lbl.set_text("");
|
||||||
|
glib::ControlFlow::Break
|
||||||
|
});
|
||||||
|
}
|
||||||
|
Err(e) => status_lbl.set_text(&format!("Error: {e}")),
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
btn_row.append(&add_btn);
|
btn_row.append(&add_btn);
|
||||||
btn_row.append(&save_btn);
|
btn_row.append(&save_btn);
|
||||||
|
btn_row.append(&status_lbl);
|
||||||
vbox.append(&btn_row);
|
vbox.append(&btn_row);
|
||||||
|
|
||||||
vbox
|
vbox
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@ use crate::config;
|
||||||
|
|
||||||
#[derive(Deserialize, Serialize, Clone)]
|
#[derive(Deserialize, Serialize, Clone)]
|
||||||
pub struct BreadpadConfig {
|
pub struct BreadpadConfig {
|
||||||
#[serde(default = "default_model")]
|
#[serde(default)]
|
||||||
pub model: String,
|
pub model: String,
|
||||||
#[serde(default = "default_true")]
|
#[serde(default = "default_true")]
|
||||||
pub reminders: bool,
|
pub reminders: bool,
|
||||||
|
|
@ -16,21 +16,11 @@ pub struct BreadpadConfig {
|
||||||
pub calendar: bool,
|
pub calendar: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
fn default_model() -> String {
|
fn default_true() -> bool { true }
|
||||||
"claude-sonnet-4-6".to_string()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn default_true() -> bool {
|
|
||||||
true
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Default for BreadpadConfig {
|
impl Default for BreadpadConfig {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
Self {
|
Self { model: String::new(), reminders: true, calendar: true }
|
||||||
model: default_model(),
|
|
||||||
reminders: true,
|
|
||||||
calendar: true,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -58,6 +48,7 @@ pub fn build() -> GBox {
|
||||||
lbl.set_xalign(0.0);
|
lbl.set_xalign(0.0);
|
||||||
let model_entry = Entry::new();
|
let model_entry = Entry::new();
|
||||||
model_entry.set_text(&cfg.borrow().model);
|
model_entry.set_text(&cfg.borrow().model);
|
||||||
|
model_entry.set_placeholder_text(Some("e.g. claude-sonnet-4-6"));
|
||||||
{
|
{
|
||||||
let cfg = cfg.clone();
|
let cfg = cfg.clone();
|
||||||
model_entry.connect_changed(move |e| {
|
model_entry.connect_changed(move |e| {
|
||||||
|
|
@ -68,7 +59,7 @@ pub fn build() -> GBox {
|
||||||
row.append(&model_entry);
|
row.append(&model_entry);
|
||||||
vbox.append(&row);
|
vbox.append(&row);
|
||||||
|
|
||||||
// Reminders toggle
|
// Reminders
|
||||||
let row = GBox::new(Orientation::Horizontal, 16);
|
let row = GBox::new(Orientation::Horizontal, 16);
|
||||||
let lbl = Label::new(Some("Reminders"));
|
let lbl = Label::new(Some("Reminders"));
|
||||||
lbl.set_hexpand(true);
|
lbl.set_hexpand(true);
|
||||||
|
|
@ -77,15 +68,13 @@ pub fn build() -> GBox {
|
||||||
sw.set_active(cfg.borrow().reminders);
|
sw.set_active(cfg.borrow().reminders);
|
||||||
{
|
{
|
||||||
let cfg = cfg.clone();
|
let cfg = cfg.clone();
|
||||||
sw.connect_active_notify(move |s| {
|
sw.connect_active_notify(move |s| { cfg.borrow_mut().reminders = s.is_active(); });
|
||||||
cfg.borrow_mut().reminders = s.is_active();
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
row.append(&lbl);
|
row.append(&lbl);
|
||||||
row.append(&sw);
|
row.append(&sw);
|
||||||
vbox.append(&row);
|
vbox.append(&row);
|
||||||
|
|
||||||
// Calendar toggle
|
// Calendar
|
||||||
let row = GBox::new(Orientation::Horizontal, 16);
|
let row = GBox::new(Orientation::Horizontal, 16);
|
||||||
let lbl = Label::new(Some("Calendar integration"));
|
let lbl = Label::new(Some("Calendar integration"));
|
||||||
lbl.set_hexpand(true);
|
lbl.set_hexpand(true);
|
||||||
|
|
@ -94,25 +83,40 @@ pub fn build() -> GBox {
|
||||||
sw.set_active(cfg.borrow().calendar);
|
sw.set_active(cfg.borrow().calendar);
|
||||||
{
|
{
|
||||||
let cfg = cfg.clone();
|
let cfg = cfg.clone();
|
||||||
sw.connect_active_notify(move |s| {
|
sw.connect_active_notify(move |s| { cfg.borrow_mut().calendar = s.is_active(); });
|
||||||
cfg.borrow_mut().calendar = s.is_active();
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
row.append(&lbl);
|
row.append(&lbl);
|
||||||
row.append(&sw);
|
row.append(&sw);
|
||||||
vbox.append(&row);
|
vbox.append(&row);
|
||||||
|
|
||||||
|
let btn_row = GBox::new(Orientation::Horizontal, 12);
|
||||||
|
btn_row.set_margin_top(16);
|
||||||
|
|
||||||
let save_btn = Button::with_label("Save");
|
let save_btn = Button::with_label("Save");
|
||||||
save_btn.set_margin_top(16);
|
let status_lbl = Label::new(None);
|
||||||
save_btn.set_halign(gtk4::Align::Start);
|
status_lbl.add_css_class("dim-label");
|
||||||
|
|
||||||
{
|
{
|
||||||
let cfg = cfg.clone();
|
let cfg = cfg.clone();
|
||||||
let path = path.clone();
|
let status_lbl = status_lbl.clone();
|
||||||
save_btn.connect_clicked(move |_| {
|
save_btn.connect_clicked(move |_| {
|
||||||
let _ = config::save(&path, &*cfg.borrow());
|
match config::save(&path, &*cfg.borrow()) {
|
||||||
|
Ok(()) => {
|
||||||
|
status_lbl.set_text("Saved");
|
||||||
|
let lbl = status_lbl.clone();
|
||||||
|
glib::timeout_add_seconds_local(3, move || {
|
||||||
|
lbl.set_text("");
|
||||||
|
glib::ControlFlow::Break
|
||||||
|
});
|
||||||
|
}
|
||||||
|
Err(e) => status_lbl.set_text(&format!("Error: {e}")),
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
vbox.append(&save_btn);
|
|
||||||
|
btn_row.append(&save_btn);
|
||||||
|
btn_row.append(&status_lbl);
|
||||||
|
vbox.append(&btn_row);
|
||||||
|
|
||||||
vbox
|
vbox
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -17,14 +17,14 @@ fn get_monitors() -> Vec<String> {
|
||||||
let w = m.get("width")?.as_u64()?;
|
let w = m.get("width")?.as_u64()?;
|
||||||
let h = m.get("height")?.as_u64()?;
|
let h = m.get("height")?.as_u64()?;
|
||||||
let refresh = m.get("refreshRate")?.as_f64()?;
|
let refresh = m.get("refreshRate")?.as_f64()?;
|
||||||
Some(format!("{name} {w}×{h} @ {refresh:.0}Hz"))
|
Some(format!("{name} {w}x{h} @ {refresh:.0}Hz"))
|
||||||
})
|
})
|
||||||
.collect()
|
.collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn config_path() -> std::path::PathBuf {
|
fn hypr_path(name: &str) -> std::path::PathBuf {
|
||||||
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());
|
||||||
std::path::PathBuf::from(home).join(".config/hypr/hyprland.conf")
|
std::path::PathBuf::from(home).join(".config/hypr").join(name)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn build() -> GBox {
|
pub fn build() -> GBox {
|
||||||
|
|
@ -36,7 +36,6 @@ pub fn build() -> GBox {
|
||||||
title.set_xalign(0.0);
|
title.set_xalign(0.0);
|
||||||
vbox.append(&title);
|
vbox.append(&title);
|
||||||
|
|
||||||
// Monitors section
|
|
||||||
let monitors_lbl = Label::new(Some("Connected monitors"));
|
let monitors_lbl = Label::new(Some("Connected monitors"));
|
||||||
monitors_lbl.set_xalign(0.0);
|
monitors_lbl.set_xalign(0.0);
|
||||||
monitors_lbl.set_margin_top(8);
|
monitors_lbl.set_margin_top(8);
|
||||||
|
|
@ -52,38 +51,35 @@ pub fn build() -> GBox {
|
||||||
for mon in &monitors {
|
for mon in &monitors {
|
||||||
let lbl = Label::new(Some(mon));
|
let lbl = Label::new(Some(mon));
|
||||||
lbl.set_xalign(0.0);
|
lbl.set_xalign(0.0);
|
||||||
lbl.add_css_class("monospace");
|
lbl.set_monospace(true);
|
||||||
vbox.append(&lbl);
|
vbox.append(&lbl);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Open config button
|
|
||||||
let open_btn = Button::with_label("Open hyprland.conf in editor");
|
let open_btn = Button::with_label("Open hyprland.conf in editor");
|
||||||
open_btn.set_margin_top(16);
|
open_btn.set_margin_top(16);
|
||||||
open_btn.set_halign(gtk4::Align::Start);
|
open_btn.set_halign(gtk4::Align::Start);
|
||||||
{
|
{
|
||||||
let path = config_path();
|
let conf_path = hypr_path("hyprland.conf");
|
||||||
open_btn.connect_clicked(move |_| {
|
open_btn.connect_clicked(move |_| {
|
||||||
let editor = std::env::var("EDITOR").unwrap_or_else(|_| "foot".to_string());
|
let editor = std::env::var("EDITOR").unwrap_or_else(|_| "foot".to_string());
|
||||||
let _ = Command::new(&editor)
|
if let Ok(mut child) = Command::new(&editor).arg(&conf_path).spawn() {
|
||||||
.arg(path.to_str().unwrap_or(""))
|
std::thread::spawn(move || { let _ = child.wait(); });
|
||||||
.spawn();
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
vbox.append(&open_btn);
|
vbox.append(&open_btn);
|
||||||
|
|
||||||
// Open keybinds button
|
|
||||||
let keybinds_btn = Button::with_label("Open keybinds.conf in editor");
|
let keybinds_btn = Button::with_label("Open keybinds.conf in editor");
|
||||||
keybinds_btn.set_margin_top(8);
|
keybinds_btn.set_margin_top(8);
|
||||||
keybinds_btn.set_halign(gtk4::Align::Start);
|
keybinds_btn.set_halign(gtk4::Align::Start);
|
||||||
{
|
{
|
||||||
let home = std::env::var("HOME").unwrap_or_else(|_| "/root".to_string());
|
let kb_path = hypr_path("keybinds.conf");
|
||||||
let kb_path = std::path::PathBuf::from(home).join(".config/hypr/keybinds.conf");
|
|
||||||
keybinds_btn.connect_clicked(move |_| {
|
keybinds_btn.connect_clicked(move |_| {
|
||||||
let editor = std::env::var("EDITOR").unwrap_or_else(|_| "foot".to_string());
|
let editor = std::env::var("EDITOR").unwrap_or_else(|_| "foot".to_string());
|
||||||
let _ = Command::new(&editor)
|
if let Ok(mut child) = Command::new(&editor).arg(&kb_path).spawn() {
|
||||||
.arg(kb_path.to_str().unwrap_or(""))
|
std::thread::spawn(move || { let _ = child.wait(); });
|
||||||
.spawn();
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
vbox.append(&keybinds_btn);
|
vbox.append(&keybinds_btn);
|
||||||
|
|
|
||||||
|
|
@ -1,31 +1,20 @@
|
||||||
|
use async_channel;
|
||||||
use gtk4::prelude::*;
|
use gtk4::prelude::*;
|
||||||
use gtk4::{
|
use gtk4::{
|
||||||
Box as GBox, Button, Label, ListBox, ListBoxRow, Orientation, ScrolledWindow, TextView,
|
Box as GBox, Button, Label, ListBox, ListBoxRow, Orientation, ScrolledWindow, TextView,
|
||||||
};
|
};
|
||||||
use serde::Deserialize;
|
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::process::Command;
|
use std::io::{BufRead, BufReader};
|
||||||
|
use std::process::{Command, Stdio};
|
||||||
#[derive(Deserialize, Default)]
|
|
||||||
struct InstalledPackages {
|
|
||||||
#[serde(flatten)]
|
|
||||||
packages: HashMap<String, PackageInfo>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Deserialize)]
|
|
||||||
struct PackageInfo {
|
|
||||||
version: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
fn read_installed() -> HashMap<String, String> {
|
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)
|
let path = std::path::Path::new(&home)
|
||||||
.join(".local/state/bakery/installed.json");
|
.join(".local/state/bakery/installed.json");
|
||||||
|
|
||||||
let Ok(text) = std::fs::read_to_string(&path) else {
|
let Ok(text) = std::fs::read_to_string(&path) else {
|
||||||
return HashMap::new();
|
return HashMap::new();
|
||||||
};
|
};
|
||||||
|
|
||||||
let Ok(parsed) = serde_json::from_str::<HashMap<String, serde_json::Value>>(&text) else {
|
let Ok(parsed) = serde_json::from_str::<HashMap<String, serde_json::Value>>(&text) else {
|
||||||
return HashMap::new();
|
return HashMap::new();
|
||||||
};
|
};
|
||||||
|
|
@ -35,15 +24,57 @@ fn read_installed() -> HashMap<String, String> {
|
||||||
.filter_map(|(name, val)| {
|
.filter_map(|(name, val)| {
|
||||||
let version = val
|
let version = val
|
||||||
.get("version")
|
.get("version")
|
||||||
.or_else(|| val.as_str().map(|_| &val))
|
|
||||||
.and_then(|v| v.as_str())
|
.and_then(|v| v.as_str())
|
||||||
.map(|s| s.to_string())
|
.unwrap_or("unknown")
|
||||||
.unwrap_or_else(|| "unknown".to_string());
|
.to_string();
|
||||||
Some((name, version))
|
Some((name, version))
|
||||||
})
|
})
|
||||||
.collect()
|
.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 {
|
pub fn build() -> GBox {
|
||||||
let vbox = GBox::new(Orientation::Vertical, 0);
|
let vbox = GBox::new(Orientation::Vertical, 0);
|
||||||
vbox.add_css_class("view-content");
|
vbox.add_css_class("view-content");
|
||||||
|
|
@ -64,7 +95,10 @@ pub fn build() -> GBox {
|
||||||
let packages = read_installed();
|
let packages = read_installed();
|
||||||
if packages.is_empty() {
|
if packages.is_empty() {
|
||||||
let row = ListBoxRow::new();
|
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_top(8);
|
||||||
lbl.set_margin_bottom(8);
|
lbl.set_margin_bottom(8);
|
||||||
lbl.set_margin_start(8);
|
lbl.set_margin_start(8);
|
||||||
|
|
@ -73,8 +107,10 @@ pub fn build() -> GBox {
|
||||||
} else {
|
} else {
|
||||||
let mut names: Vec<_> = packages.iter().collect();
|
let mut names: Vec<_> = packages.iter().collect();
|
||||||
names.sort_by_key(|(k, _)| k.as_str());
|
names.sort_by_key(|(k, _)| k.as_str());
|
||||||
|
|
||||||
for (name, version) in names {
|
for (name, version) in names {
|
||||||
let row = ListBoxRow::new();
|
let row = ListBoxRow::new();
|
||||||
|
row.set_selectable(false);
|
||||||
let hbox = GBox::new(Orientation::Horizontal, 16);
|
let hbox = GBox::new(Orientation::Horizontal, 16);
|
||||||
hbox.set_margin_top(6);
|
hbox.set_margin_top(6);
|
||||||
hbox.set_margin_bottom(6);
|
hbox.set_margin_bottom(6);
|
||||||
|
|
@ -88,12 +124,16 @@ pub fn build() -> GBox {
|
||||||
let ver_lbl = Label::new(Some(version));
|
let ver_lbl = Label::new(Some(version));
|
||||||
ver_lbl.set_xalign(1.0);
|
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 pkg_name = name.clone();
|
||||||
|
let update_btn = Button::with_label("Update");
|
||||||
update_btn.connect_clicked(move |_| {
|
update_btn.connect_clicked(move |_| {
|
||||||
let _ = Command::new("bakery")
|
match Command::new("bakery").args(["update", &pkg_name]).spawn() {
|
||||||
.args(["update", &pkg_name])
|
Ok(mut child) => {
|
||||||
.spawn();
|
std::thread::spawn(move || { let _ = child.wait(); });
|
||||||
|
}
|
||||||
|
Err(e) => eprintln!("bakery update failed: {e}"),
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
hbox.append(&name_lbl);
|
hbox.append(&name_lbl);
|
||||||
|
|
@ -109,56 +149,32 @@ pub fn build() -> GBox {
|
||||||
scroll.set_child(Some(&list));
|
scroll.set_child(Some(&list));
|
||||||
vbox.append(&scroll);
|
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);
|
let btn_row = GBox::new(Orientation::Horizontal, 8);
|
||||||
btn_row.set_margin_top(12);
|
btn_row.set_margin_top(12);
|
||||||
|
|
||||||
let check_btn = Button::with_label("Check for updates");
|
let check_btn = Button::with_label("Check for updates");
|
||||||
let update_all_btn = Button::with_label("Update all");
|
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();
|
let log_buf = log_buf.clone();
|
||||||
check_btn.connect_clicked(move |_| {
|
check_btn.connect_clicked(move |_| {
|
||||||
log_buf.set_text("Checking for updates...\n");
|
log_buf.set_text("");
|
||||||
match Command::new("bakery").args(["list"]).output() {
|
stream_command(&["bakery", "list"], log_buf.clone());
|
||||||
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"));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
let log_buf = log_buf.clone();
|
let log_buf = log_buf.clone();
|
||||||
update_all_btn.connect_clicked(move |_| {
|
update_all_btn.connect_clicked(move |_| {
|
||||||
log_buf.set_text("Running bakery update --all...\n");
|
log_buf.set_text("");
|
||||||
let (sender, receiver) = glib::MainContext::channel(glib::Priority::DEFAULT);
|
stream_command(&["bakery", "update", "--all"], log_buf.clone());
|
||||||
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
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,10 @@
|
||||||
use gtk4::prelude::*;
|
use gtk4::prelude::*;
|
||||||
use gtk4::{
|
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;
|
use std::process::Command;
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
struct SnapshotRow {
|
struct SnapshotRow {
|
||||||
number: String,
|
number: String,
|
||||||
date: String,
|
date: String,
|
||||||
|
|
@ -22,47 +23,60 @@ fn list_snapshots() -> Vec<SnapshotRow> {
|
||||||
text.lines()
|
text.lines()
|
||||||
.skip(2) // header + separator
|
.skip(2) // header + separator
|
||||||
.filter_map(|line| {
|
.filter_map(|line| {
|
||||||
let cols: Vec<&str> = line.splitn(3, '|').collect();
|
let mut cols = line.splitn(3, '|');
|
||||||
if cols.len() == 3 {
|
Some(SnapshotRow {
|
||||||
Some(SnapshotRow {
|
number: cols.next()?.trim().to_string(),
|
||||||
number: cols[0].trim().to_string(),
|
date: cols.next()?.trim().to_string(),
|
||||||
date: cols[1].trim().to_string(),
|
description: cols.next()?.trim().to_string(),
|
||||||
description: cols[2].trim().to_string(),
|
})
|
||||||
})
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
.collect()
|
.collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn confirm_rollback(number: &str) {
|
fn populate_list(list: &ListBox) {
|
||||||
let number = number.to_string();
|
while let Some(child) = list.first_child() {
|
||||||
let dialog = MessageDialog::new(
|
list.remove(&child);
|
||||||
None::<>k4::Window>,
|
}
|
||||||
gtk4::DialogFlags::MODAL,
|
let snapshots = list_snapshots();
|
||||||
gtk4::MessageType::Question,
|
if snapshots.is_empty() {
|
||||||
gtk4::ButtonsType::OkCancel,
|
let row = ListBoxRow::new();
|
||||||
&format!("Roll back to snapshot #{number}?\n\nReboot required to apply."),
|
row.set_selectable(false);
|
||||||
);
|
let lbl = Label::new(Some("No snapshots found (snapper may not be configured yet)"));
|
||||||
dialog.connect_response(move |d, resp| {
|
lbl.set_margin_top(8);
|
||||||
if resp == gtk4::ResponseType::Ok {
|
lbl.set_margin_bottom(8);
|
||||||
let _ = Command::new("snapper")
|
lbl.set_margin_start(8);
|
||||||
.args(["rollback", &number])
|
row.set_child(Some(&lbl));
|
||||||
.status();
|
list.append(&row);
|
||||||
let info = MessageDialog::new(
|
return;
|
||||||
None::<>k4::Window>,
|
}
|
||||||
gtk4::DialogFlags::MODAL,
|
for snap in &snapshots {
|
||||||
gtk4::MessageType::Info,
|
let row = ListBoxRow::new();
|
||||||
gtk4::ButtonsType::Ok,
|
row.set_widget_name(&snap.number);
|
||||||
"Rollback queued. Please reboot to apply.",
|
|
||||||
);
|
let hbox = GBox::new(Orientation::Horizontal, 16);
|
||||||
info.connect_response(|d, _| d.destroy());
|
hbox.set_margin_top(6);
|
||||||
info.present();
|
hbox.set_margin_bottom(6);
|
||||||
}
|
hbox.set_margin_start(8);
|
||||||
d.destroy();
|
hbox.set_margin_end(8);
|
||||||
});
|
|
||||||
dialog.present();
|
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 {
|
pub fn build() -> GBox {
|
||||||
|
|
@ -74,56 +88,16 @@ pub fn build() -> GBox {
|
||||||
title.set_xalign(0.0);
|
title.set_xalign(0.0);
|
||||||
vbox.append(&title);
|
vbox.append(&title);
|
||||||
|
|
||||||
let subtitle =
|
let subtitle = Label::new(Some(
|
||||||
Label::new(Some("System snapshots created by snap-pac on each pacman transaction."));
|
"System snapshots created by snap-pac on each pacman transaction.",
|
||||||
|
));
|
||||||
subtitle.set_xalign(0.0);
|
subtitle.set_xalign(0.0);
|
||||||
subtitle.set_margin_bottom(16);
|
subtitle.set_margin_bottom(16);
|
||||||
vbox.append(&subtitle);
|
vbox.append(&subtitle);
|
||||||
|
|
||||||
let list = ListBox::new();
|
let list = ListBox::new();
|
||||||
list.set_selection_mode(gtk4::SelectionMode::Single);
|
list.set_selection_mode(gtk4::SelectionMode::Single);
|
||||||
|
populate_list(&list);
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let scroll = ScrolledWindow::new();
|
let scroll = ScrolledWindow::new();
|
||||||
scroll.set_vexpand(true);
|
scroll.set_vexpand(true);
|
||||||
|
|
@ -133,32 +107,81 @@ pub fn build() -> GBox {
|
||||||
let btn_row = GBox::new(Orientation::Horizontal, 8);
|
let btn_row = GBox::new(Orientation::Horizontal, 8);
|
||||||
btn_row.set_margin_top(12);
|
btn_row.set_margin_top(12);
|
||||||
|
|
||||||
|
let refresh_btn = Button::with_label("Refresh");
|
||||||
let rollback_btn = Button::with_label("Rollback to selected");
|
let rollback_btn = Button::with_label("Rollback to selected");
|
||||||
let delete_btn = Button::with_label("Delete selected");
|
let delete_btn = Button::with_label("Delete selected");
|
||||||
delete_btn.add_css_class("destructive-action");
|
delete_btn.add_css_class("destructive-action");
|
||||||
|
|
||||||
{
|
{
|
||||||
let list = list.clone();
|
let list = list.clone();
|
||||||
rollback_btn.connect_clicked(move |_| {
|
refresh_btn.connect_clicked(move |_| {
|
||||||
let Some(row) = list.selected_row() else {
|
populate_list(&list);
|
||||||
return;
|
|
||||||
};
|
|
||||||
let number = row.widget_name().to_string();
|
|
||||||
confirm_rollback(&number);
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
let list = list.clone();
|
let list = list.clone();
|
||||||
delete_btn.connect_clicked(move |_| {
|
rollback_btn.connect_clicked(move |btn| {
|
||||||
let Some(row) = list.selected_row() else {
|
let Some(row) = list.selected_row() else { return };
|
||||||
return;
|
|
||||||
};
|
|
||||||
let number = row.widget_name().to_string();
|
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(&rollback_btn);
|
||||||
btn_row.append(&delete_btn);
|
btn_row.append(&delete_btn);
|
||||||
vbox.append(&btn_row);
|
vbox.append(&btn_row);
|
||||||
|
|
|
||||||
|
|
@ -22,9 +22,11 @@ decoration {
|
||||||
size = 6
|
size = 6
|
||||||
passes = 2
|
passes = 2
|
||||||
}
|
}
|
||||||
drop_shadow = true
|
shadow {
|
||||||
shadow_range = 12
|
enabled = true
|
||||||
shadow_render_power = 3
|
range = 12
|
||||||
|
render_power = 3
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
animations {
|
animations {
|
||||||
29
iso/airootfs/etc/calamares/branding/bos/branding.desc
Normal file
29
iso/airootfs/etc/calamares/branding/bos/branding.desc
Normal file
|
|
@ -0,0 +1,29 @@
|
||||||
|
---
|
||||||
|
componentName: bos
|
||||||
|
|
||||||
|
strings:
|
||||||
|
productName: "Bread Operating System"
|
||||||
|
shortProductName: "BOS"
|
||||||
|
version: "rolling"
|
||||||
|
shortVersion: "rolling"
|
||||||
|
versionedName: "BOS (rolling)"
|
||||||
|
shortVersionedName: "BOS"
|
||||||
|
bootloaderEntryName: "BOS"
|
||||||
|
productUrl: "https://github.com/Breadway/bos"
|
||||||
|
supportUrl: "https://github.com/Breadway/bos/issues"
|
||||||
|
knownIssuesUrl: "https://github.com/Breadway/bos/issues"
|
||||||
|
releaseNotesUrl: "https://github.com/Breadway/bos/releases"
|
||||||
|
|
||||||
|
images:
|
||||||
|
productLogo: "logo.png"
|
||||||
|
productIcon: "logo.png"
|
||||||
|
productWelcome: "languages.png"
|
||||||
|
|
||||||
|
slideshow: "show.qml"
|
||||||
|
slideshowAPI: 2
|
||||||
|
|
||||||
|
style:
|
||||||
|
sidebarBackground: "#3b4252"
|
||||||
|
sidebarText: "#eceff4"
|
||||||
|
sidebarTextSelect: "#5e81ac"
|
||||||
|
sidebarTextHighlight:"#eceff4"
|
||||||
43
iso/airootfs/etc/calamares/branding/bos/show.qml
Normal file
43
iso/airootfs/etc/calamares/branding/bos/show.qml
Normal file
|
|
@ -0,0 +1,43 @@
|
||||||
|
/* BOS installer slideshow */
|
||||||
|
import QtQuick 2.15
|
||||||
|
import io.calamares.ui 1.0
|
||||||
|
|
||||||
|
Presentation {
|
||||||
|
id: presentation
|
||||||
|
|
||||||
|
Slide {
|
||||||
|
anchors.fill: parent
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
anchors.fill: parent
|
||||||
|
color: "#2e3440"
|
||||||
|
|
||||||
|
Column {
|
||||||
|
anchors.centerIn: parent
|
||||||
|
spacing: 20
|
||||||
|
|
||||||
|
Text {
|
||||||
|
text: "Bread Operating System"
|
||||||
|
color: "#eceff4"
|
||||||
|
font.pointSize: 28
|
||||||
|
font.bold: true
|
||||||
|
anchors.horizontalCenter: parent.horizontalCenter
|
||||||
|
}
|
||||||
|
|
||||||
|
Text {
|
||||||
|
text: "Installing your system…"
|
||||||
|
color: "#88c0d0"
|
||||||
|
font.pointSize: 16
|
||||||
|
anchors.horizontalCenter: parent.horizontalCenter
|
||||||
|
}
|
||||||
|
|
||||||
|
Text {
|
||||||
|
text: "Hyprland · bread · bakery · snapshots"
|
||||||
|
color: "#616e88"
|
||||||
|
font.pointSize: 12
|
||||||
|
anchors.horizontalCenter: parent.horizontalCenter
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
10
iso/airootfs/etc/calamares/modules/mount.conf
Normal file
10
iso/airootfs/etc/calamares/modules/mount.conf
Normal file
|
|
@ -0,0 +1,10 @@
|
||||||
|
---
|
||||||
|
# Extra mount options applied by filesystem type.
|
||||||
|
# Btrfs subvolume mounts are already configured in partition.conf.
|
||||||
|
mountOptions:
|
||||||
|
- filesystem: default
|
||||||
|
options: [noatime]
|
||||||
|
- filesystem: btrfs
|
||||||
|
options: [noatime, "compress=zstd", "space_cache=v2"]
|
||||||
|
- filesystem: vfat
|
||||||
|
options: [umask=0077]
|
||||||
7
iso/airootfs/etc/calamares/modules/unpackfs.conf
Normal file
7
iso/airootfs/etc/calamares/modules/unpackfs.conf
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
---
|
||||||
|
# Unpack the live squashfs onto the target partition.
|
||||||
|
# "arch" matches profiledef.sh install_dir; adjust if that changes.
|
||||||
|
unpack:
|
||||||
|
- source: "/run/archiso/bootmnt/arch/x86_64/airootfs.sfs"
|
||||||
|
sourcefs: "squashfs"
|
||||||
|
destination: ""
|
||||||
|
|
@ -9,7 +9,7 @@ sed -i 's/NUMBER_MIN_AGE="[^"]*"/NUMBER_MIN_AGE="1800"/' /etc/snapper/configs/ro
|
||||||
sed -i 's/NUMBER_LIMIT="[^"]*"/NUMBER_LIMIT="10"/' /etc/snapper/configs/root
|
sed -i 's/NUMBER_LIMIT="[^"]*"/NUMBER_LIMIT="10"/' /etc/snapper/configs/root
|
||||||
sed -i 's/NUMBER_LIMIT_IMPORTANT="[^"]*"/NUMBER_LIMIT_IMPORTANT="5"/' /etc/snapper/configs/root
|
sed -i 's/NUMBER_LIMIT_IMPORTANT="[^"]*"/NUMBER_LIMIT_IMPORTANT="5"/' /etc/snapper/configs/root
|
||||||
|
|
||||||
# Allow main user to use snapper without sudo
|
# Allow main user to list/create/delete snapshots without sudo
|
||||||
MAIN_USER=$(getent passwd 1000 | cut -d: -f1)
|
MAIN_USER=$(getent passwd 1000 | cut -d: -f1)
|
||||||
sed -i "s/ALLOW_USERS=\"\"/ALLOW_USERS=\"$MAIN_USER\"/" /etc/snapper/configs/root
|
sed -i "s/ALLOW_USERS=\"\"/ALLOW_USERS=\"$MAIN_USER\"/" /etc/snapper/configs/root
|
||||||
|
|
||||||
|
|
@ -19,18 +19,19 @@ systemctl enable bluetooth
|
||||||
systemctl enable snapper-cleanup.timer
|
systemctl enable snapper-cleanup.timer
|
||||||
systemctl enable grub-btrfs.path
|
systemctl enable grub-btrfs.path
|
||||||
|
|
||||||
# --- Bakery install ---
|
# --- Bakery: install bread ecosystem ---
|
||||||
|
# Requires [breadway] repo in /etc/pacman.conf — see iso/pacman.conf
|
||||||
if command -v bakery &>/dev/null; then
|
if command -v bakery &>/dev/null; then
|
||||||
sudo -u "$MAIN_USER" bakery install bread breadbar breadbox breadcrumbs breadpad bos-settings
|
sudo -u "$MAIN_USER" bakery install bread breadbar breadbox breadcrumbs breadpad bos-settings
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# --- Deploy dotfiles (skip existing files) ---
|
# --- Deploy dotfiles into user home (skip any file that already exists) ---
|
||||||
DOTFILES_SRC="/etc/skel/.config"
|
SKEL_SRC="/etc/skel/.config"
|
||||||
DOTFILES_DEST="/home/$MAIN_USER/.config"
|
DOTFILES_DEST="/home/$MAIN_USER/.config"
|
||||||
|
|
||||||
if [[ -d "$DOTFILES_SRC" ]]; then
|
if [[ -d "$SKEL_SRC" ]]; then
|
||||||
mkdir -p "$DOTFILES_DEST"
|
mkdir -p "$DOTFILES_DEST"
|
||||||
cp -rn "$DOTFILES_SRC/." "$DOTFILES_DEST/"
|
cp -rn "$SKEL_SRC/." "$DOTFILES_DEST/"
|
||||||
chown -R "$MAIN_USER:$MAIN_USER" "$DOTFILES_DEST"
|
chown -R "$MAIN_USER:$MAIN_USER" "$DOTFILES_DEST"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
---
|
---
|
||||||
modules-search: [local, /usr/lib/calamares/modules]
|
modules-search: [/etc/calamares/modules, /usr/lib/calamares/modules]
|
||||||
|
|
||||||
sequence:
|
sequence:
|
||||||
- show:
|
- show:
|
||||||
|
|
|
||||||
11
iso/airootfs/etc/polkit-1/rules.d/10-snapper.rules
Normal file
11
iso/airootfs/etc/polkit-1/rules.d/10-snapper.rules
Normal file
|
|
@ -0,0 +1,11 @@
|
||||||
|
// Allow members of the wheel group to perform snapper rollback via pkexec
|
||||||
|
// without a password prompt. Other snapper operations (list/create/delete)
|
||||||
|
// are controlled by ALLOW_USERS in /etc/snapper/configs/root.
|
||||||
|
polkit.addRule(function(action, subject) {
|
||||||
|
if (action.id == "io.opensuse.Snapper.Rollback" &&
|
||||||
|
subject.local &&
|
||||||
|
subject.active &&
|
||||||
|
subject.isInGroup("wheel")) {
|
||||||
|
return polkit.Result.YES;
|
||||||
|
}
|
||||||
|
});
|
||||||
8
iso/airootfs/etc/skel/.config/bread/breadd.toml
Normal file
8
iso/airootfs/etc/skel/.config/bread/breadd.toml
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
log_level = "info"
|
||||||
|
|
||||||
|
[adapters]
|
||||||
|
keyboard = true
|
||||||
|
mouse = true
|
||||||
|
touchpad = true
|
||||||
|
bluetooth = true
|
||||||
|
gamepad = true
|
||||||
1
iso/airootfs/etc/skel/.config/bread/init.lua
Normal file
1
iso/airootfs/etc/skel/.config/bread/init.lua
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
bread.activate_profile("default")
|
||||||
3
iso/airootfs/etc/skel/.config/breadbox/config.toml
Normal file
3
iso/airootfs/etc/skel/.config/breadbox/config.toml
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
[[context]]
|
||||||
|
name = "default"
|
||||||
|
apps = ["firefox", "foot", "nautilus", "code"]
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
[[profile]]
|
||||||
|
name = "home"
|
||||||
|
ssids = []
|
||||||
56
iso/airootfs/etc/skel/.config/hypr/hyprland.conf
Normal file
56
iso/airootfs/etc/skel/.config/hypr/hyprland.conf
Normal file
|
|
@ -0,0 +1,56 @@
|
||||||
|
monitor=,preferred,auto,1
|
||||||
|
|
||||||
|
exec-once = breadd
|
||||||
|
exec-once = breadbar
|
||||||
|
exec-once = breadbox-sync
|
||||||
|
|
||||||
|
source = ~/.config/hypr/keybinds.conf
|
||||||
|
|
||||||
|
general {
|
||||||
|
gaps_in = 5
|
||||||
|
gaps_out = 10
|
||||||
|
border_size = 2
|
||||||
|
col.active_border = rgba(88c0d0ff)
|
||||||
|
col.inactive_border = rgba(4c566aff)
|
||||||
|
layout = dwindle
|
||||||
|
}
|
||||||
|
|
||||||
|
decoration {
|
||||||
|
rounding = 8
|
||||||
|
blur {
|
||||||
|
enabled = true
|
||||||
|
size = 6
|
||||||
|
passes = 2
|
||||||
|
}
|
||||||
|
shadow {
|
||||||
|
enabled = true
|
||||||
|
range = 12
|
||||||
|
render_power = 3
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
animations {
|
||||||
|
enabled = true
|
||||||
|
bezier = ease, 0.25, 0.1, 0.25, 1.0
|
||||||
|
animation = windows, 1, 4, ease
|
||||||
|
animation = fade, 1, 4, ease
|
||||||
|
animation = workspaces, 1, 5, ease
|
||||||
|
}
|
||||||
|
|
||||||
|
input {
|
||||||
|
kb_layout = us
|
||||||
|
follow_mouse = 1
|
||||||
|
touchpad {
|
||||||
|
natural_scroll = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
dwindle {
|
||||||
|
pseudotile = true
|
||||||
|
preserve_split = true
|
||||||
|
}
|
||||||
|
|
||||||
|
misc {
|
||||||
|
disable_hyprland_logo = true
|
||||||
|
disable_splash_rendering = true
|
||||||
|
}
|
||||||
58
iso/airootfs/etc/skel/.config/hypr/keybinds.conf
Normal file
58
iso/airootfs/etc/skel/.config/hypr/keybinds.conf
Normal file
|
|
@ -0,0 +1,58 @@
|
||||||
|
$mod = SUPER
|
||||||
|
|
||||||
|
# App launchers
|
||||||
|
bind = $mod, Space, exec, breadbox
|
||||||
|
bind = $mod, N, exec, breadpad
|
||||||
|
bind = $mod, M, exec, breadman
|
||||||
|
bind = $mod, S, exec, bos-settings
|
||||||
|
|
||||||
|
# Core
|
||||||
|
bind = $mod, Return, exec, foot
|
||||||
|
bind = $mod, Q, killactive
|
||||||
|
bind = $mod SHIFT, E, exit
|
||||||
|
bind = $mod, F, fullscreen
|
||||||
|
|
||||||
|
# Focus
|
||||||
|
bind = $mod, H, movefocus, l
|
||||||
|
bind = $mod, L, movefocus, r
|
||||||
|
bind = $mod, K, movefocus, u
|
||||||
|
bind = $mod, J, movefocus, d
|
||||||
|
|
||||||
|
# Move windows
|
||||||
|
bind = $mod SHIFT, H, movewindow, l
|
||||||
|
bind = $mod SHIFT, L, movewindow, r
|
||||||
|
bind = $mod SHIFT, K, movewindow, u
|
||||||
|
bind = $mod SHIFT, J, movewindow, d
|
||||||
|
|
||||||
|
# Workspaces
|
||||||
|
bind = $mod, 1, workspace, 1
|
||||||
|
bind = $mod, 2, workspace, 2
|
||||||
|
bind = $mod, 3, workspace, 3
|
||||||
|
bind = $mod, 4, workspace, 4
|
||||||
|
bind = $mod, 5, workspace, 5
|
||||||
|
|
||||||
|
bind = $mod SHIFT, 1, movetoworkspace, 1
|
||||||
|
bind = $mod SHIFT, 2, movetoworkspace, 2
|
||||||
|
bind = $mod SHIFT, 3, movetoworkspace, 3
|
||||||
|
bind = $mod SHIFT, 4, movetoworkspace, 4
|
||||||
|
bind = $mod SHIFT, 5, movetoworkspace, 5
|
||||||
|
|
||||||
|
# Scroll through workspaces
|
||||||
|
bind = $mod, mouse_down, workspace, e+1
|
||||||
|
bind = $mod, mouse_up, workspace, e-1
|
||||||
|
|
||||||
|
# Mouse binds
|
||||||
|
bindm = $mod, mouse:272, movewindow
|
||||||
|
bindm = $mod, mouse:273, resizewindow
|
||||||
|
|
||||||
|
# Volume
|
||||||
|
bind = , XF86AudioRaiseVolume, exec, wpctl set-volume @DEFAULT_AUDIO_SINK@ 5%+
|
||||||
|
bind = , XF86AudioLowerVolume, exec, wpctl set-volume @DEFAULT_AUDIO_SINK@ 5%-
|
||||||
|
bind = , XF86AudioMute, exec, wpctl set-mute @DEFAULT_AUDIO_SINK@ toggle
|
||||||
|
|
||||||
|
# Brightness
|
||||||
|
bind = , XF86MonBrightnessUp, exec, brightnessctl set 5%+
|
||||||
|
bind = , XF86MonBrightnessDown, exec, brightnessctl set 5%-
|
||||||
|
|
||||||
|
# Screenshot
|
||||||
|
bind = , Print, exec, grimblast copy area
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
[Service]
|
||||||
|
ExecStart=
|
||||||
|
ExecStart=-/sbin/agetty -o '-p -f -- \\u' --noclear --autologin root - $TERM
|
||||||
4
iso/airootfs/root/.bash_profile
Normal file
4
iso/airootfs/root/.bash_profile
Normal file
|
|
@ -0,0 +1,4 @@
|
||||||
|
# Auto-start Hyprland on tty1 in the live session
|
||||||
|
if [[ "$(tty)" == "/dev/tty1" ]] && [[ -z "$WAYLAND_DISPLAY" ]]; then
|
||||||
|
exec Hyprland
|
||||||
|
fi
|
||||||
28
iso/airootfs/root/.config/hypr/hyprland.conf
Normal file
28
iso/airootfs/root/.config/hypr/hyprland.conf
Normal file
|
|
@ -0,0 +1,28 @@
|
||||||
|
# Live-session Hyprland config — launches Calamares on start.
|
||||||
|
# This is NOT the installed system config; that lives in dotfiles/hypr/.
|
||||||
|
|
||||||
|
monitor=,preferred,auto,1
|
||||||
|
|
||||||
|
exec-once = calamares
|
||||||
|
|
||||||
|
general {
|
||||||
|
border_size = 2
|
||||||
|
col.active_border = rgba(88c0d0ff)
|
||||||
|
col.inactive_border = rgba(4c566aff)
|
||||||
|
}
|
||||||
|
|
||||||
|
decoration {
|
||||||
|
rounding = 4
|
||||||
|
}
|
||||||
|
|
||||||
|
input {
|
||||||
|
kb_layout = us
|
||||||
|
follow_mouse = 1
|
||||||
|
}
|
||||||
|
|
||||||
|
misc {
|
||||||
|
disable_hyprland_logo = true
|
||||||
|
disable_splash_rendering = true
|
||||||
|
# Keep compositor running if calamares exits (user can relaunch)
|
||||||
|
exit_window_request_force = false
|
||||||
|
}
|
||||||
|
|
@ -47,10 +47,9 @@ gtk4-layer-shell
|
||||||
librsvg
|
librsvg
|
||||||
libpulse
|
libpulse
|
||||||
|
|
||||||
# Display
|
# Display (wlroots is bundled with Hyprland; don't list separately)
|
||||||
wayland
|
wayland
|
||||||
wayland-protocols
|
wayland-protocols
|
||||||
wlroots
|
|
||||||
|
|
||||||
# Fonts
|
# Fonts
|
||||||
noto-fonts
|
noto-fonts
|
||||||
|
|
@ -63,10 +62,13 @@ foot
|
||||||
# File manager
|
# File manager
|
||||||
nautilus
|
nautilus
|
||||||
|
|
||||||
# Installer
|
# Installer — sourced from [breadway] repo (see pacman.conf)
|
||||||
calamares
|
calamares
|
||||||
calamares-qt6
|
calamares-qt6
|
||||||
|
|
||||||
|
# Bread ecosystem — sourced from [breadway] repo
|
||||||
|
bakery
|
||||||
|
|
||||||
# Utilities
|
# Utilities
|
||||||
sudo
|
sudo
|
||||||
git
|
git
|
||||||
|
|
|
||||||
38
iso/pacman.conf
Normal file
38
iso/pacman.conf
Normal file
|
|
@ -0,0 +1,38 @@
|
||||||
|
#
|
||||||
|
# BOS pacman.conf — used during ISO build and installed to the target system.
|
||||||
|
# Based on the standard Arch Linux pacman.conf.
|
||||||
|
#
|
||||||
|
|
||||||
|
[options]
|
||||||
|
HoldPkg = pacman glibc
|
||||||
|
Architecture = auto
|
||||||
|
CheckSpace
|
||||||
|
ParallelDownloads = 5
|
||||||
|
|
||||||
|
Color
|
||||||
|
VerbosePkgLists
|
||||||
|
ILoveCandy
|
||||||
|
|
||||||
|
SigLevel = Required DatabaseOptional
|
||||||
|
LocalFileSigLevel = Optional
|
||||||
|
|
||||||
|
[core]
|
||||||
|
Include = /etc/pacman.d/mirrorlist
|
||||||
|
|
||||||
|
[extra]
|
||||||
|
Include = /etc/pacman.d/mirrorlist
|
||||||
|
|
||||||
|
[multilib]
|
||||||
|
Include = /etc/pacman.d/mirrorlist
|
||||||
|
|
||||||
|
# -----------------------------------------------------------------------
|
||||||
|
# Breadway custom repo — provides: bakery, calamares (pre-built), and the
|
||||||
|
# bread ecosystem packages (bread, breadbar, breadbox, breadcrumbs, breadpad,
|
||||||
|
# bos-settings).
|
||||||
|
#
|
||||||
|
# TODO: Replace this URL with the actual hosted repo before building.
|
||||||
|
# See: https://github.com/Breadway/repo for setup instructions.
|
||||||
|
# -----------------------------------------------------------------------
|
||||||
|
[breadway]
|
||||||
|
SigLevel = Optional TrustAll
|
||||||
|
Server = https://repo.breadway.dev/$arch
|
||||||
Loading…
Add table
Add a link
Reference in a new issue