diff --git a/bos-settings/Cargo.toml b/bos-settings/Cargo.toml index 24dabe0..d906354 100644 --- a/bos-settings/Cargo.toml +++ b/bos-settings/Cargo.toml @@ -9,3 +9,4 @@ glib = "0.20" serde = { version = "1", features = ["derive"] } serde_json = "1" toml = "0.8" +async-channel = "2" diff --git a/bos-settings/src/config/mod.rs b/bos-settings/src/config/mod.rs index 08b005b..da3b8eb 100644 --- a/bos-settings/src/config/mod.rs +++ b/bos-settings/src/config/mod.rs @@ -1,5 +1,5 @@ use std::error::Error; -use std::path::Path; +use std::path::{Path, PathBuf}; pub fn load serde::Deserialize<'de>>(path: &Path) -> Result> { let text = std::fs::read_to_string(path)?; @@ -14,11 +14,11 @@ pub fn save(path: &Path, val: &T) -> Result<(), Box std::path::PathBuf { - dirs_path() -} - -fn dirs_path() -> std::path::PathBuf { - let home = std::env::var("HOME").unwrap_or_else(|_| "/root".to_string()); - std::path::PathBuf::from(home).join(".config") +pub fn config_dir() -> PathBuf { + 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()) + .unwrap_or_else(|_| "/home/user".to_string()) + }); + PathBuf::from(home).join(".config") } diff --git a/bos-settings/src/main.rs b/bos-settings/src/main.rs index c93a8d2..fc13dc2 100644 --- a/bos-settings/src/main.rs +++ b/bos-settings/src/main.rs @@ -1,5 +1,4 @@ mod config; -mod state; mod theme; mod ui; diff --git a/bos-settings/src/ui/sidebar.rs b/bos-settings/src/ui/sidebar.rs index daafb0b..4395591 100644 --- a/bos-settings/src/ui/sidebar.rs +++ b/bos-settings/src/ui/sidebar.rs @@ -1,5 +1,5 @@ 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 id: &'static str, @@ -7,17 +7,17 @@ pub struct SidebarItem { } pub const APPS_ITEMS: &[SidebarItem] = &[ - SidebarItem { id: "bread", label: "bread" }, - SidebarItem { id: "breadbar", label: "breadbar" }, - SidebarItem { id: "breadbox", label: "breadbox" }, + SidebarItem { id: "bread", label: "bread" }, + SidebarItem { id: "breadbar", label: "breadbar" }, + SidebarItem { id: "breadbox", label: "breadbox" }, SidebarItem { id: "breadcrumbs", label: "breadcrumbs" }, - SidebarItem { id: "breadpad", label: "breadpad" }, + SidebarItem { id: "breadpad", label: "breadpad" }, ]; pub const SYSTEM_ITEMS: &[SidebarItem] = &[ SidebarItem { id: "snapshots", label: "Snapshots" }, - SidebarItem { id: "packages", label: "Packages" }, - SidebarItem { id: "hyprland", label: "Hyprland" }, + SidebarItem { id: "packages", label: "Packages" }, + SidebarItem { id: "hyprland", label: "Hyprland" }, ]; pub fn build() -> (GBox, ListBox) { @@ -32,9 +32,17 @@ pub fn build() -> (GBox, ListBox) { append_section(&list, "Apps", APPS_ITEMS); append_section(&list, "System", SYSTEM_ITEMS); - // Select first item by default - if let Some(first) = list.row_at_index(1) { - list.select_row(Some(&first)); + // Select the snapshots row so it matches the default stack page + let mut i = 0; + 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); @@ -42,7 +50,6 @@ pub fn build() -> (GBox, ListBox) { } fn append_section(list: &ListBox, title: &str, items: &[SidebarItem]) { - // Section header (non-selectable) let header_row = ListBoxRow::new(); header_row.set_selectable(false); header_row.set_activatable(false); @@ -55,7 +62,6 @@ fn append_section(list: &ListBox, title: &str, items: &[SidebarItem]) { for item in items { let row = ListBoxRow::new(); row.set_widget_name(item.id); - let lbl = Label::new(Some(item.label)); lbl.set_xalign(0.0); lbl.set_margin_top(2); diff --git a/bos-settings/src/ui/views/bread.rs b/bos-settings/src/ui/views/bread.rs index ca4b080..27d0596 100644 --- a/bos-settings/src/ui/views/bread.rs +++ b/bos-settings/src/ui/views/bread.rs @@ -14,34 +14,22 @@ pub struct BreadConfig { pub adapters: AdaptersConfig, } -fn default_log_level() -> String { - "info".to_string() -} +fn default_log_level() -> String { "info".to_string() } #[derive(Deserialize, Serialize, Clone, Default)] pub struct AdaptersConfig { - #[serde(default = "default_true")] - pub keyboard: bool, - #[serde(default = "default_true")] - pub mouse: bool, - #[serde(default = "default_true")] - pub touchpad: bool, - #[serde(default = "default_true")] - pub bluetooth: bool, - #[serde(default = "default_true")] - pub gamepad: bool, + #[serde(default = "default_true")] pub keyboard: bool, + #[serde(default = "default_true")] pub mouse: bool, + #[serde(default = "default_true")] pub touchpad: bool, + #[serde(default = "default_true")] pub bluetooth: bool, + #[serde(default = "default_true")] pub gamepad: bool, } -fn default_true() -> bool { - true -} +fn default_true() -> bool { true } impl Default for BreadConfig { fn default() -> Self { - Self { - log_level: default_log_level(), - adapters: AdaptersConfig::default(), - } + Self { 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") } -fn adapter_row(label: &str, active: bool, cfg: Rc>, field: &'static str) -> GBox { +fn adapter_row( + label: &str, + active: bool, + cfg: Rc>, + field: &'static str, +) -> GBox { let row = GBox::new(Orientation::Horizontal, 16); let lbl = Label::new(Some(label)); lbl.set_hexpand(true); @@ -60,11 +53,11 @@ fn adapter_row(label: &str, active: bool, cfg: Rc>, field: let val = s.is_active(); let mut c = cfg.borrow_mut(); match field { - "keyboard" => c.adapters.keyboard = val, - "mouse" => c.adapters.mouse = val, - "touchpad" => c.adapters.touchpad = val, - "bluetooth" => c.adapters.bluetooth = val, - "gamepad" => c.adapters.gamepad = val, + "keyboard" => c.adapters.keyboard = val, + "mouse" => c.adapters.mouse = val, + "touchpad" => c.adapters.touchpad = val, + "bluetooth" => c.adapters.bluetooth = val, + "gamepad" => c.adapters.gamepad = val, _ => {} } }); @@ -94,15 +87,10 @@ pub fn build() -> GBox { lbl.set_xalign(0.0); let levels = StringList::new(&["error", "warn", "info", "debug", "trace"]); let dropdown = DropDown::new(Some(levels), gtk4::Expression::NONE); - let current_pos = match cfg.borrow().log_level.as_str() { - "error" => 0u32, - "warn" => 1, - "info" => 2, - "debug" => 3, - "trace" => 4, - _ => 2, + let pos = match cfg.borrow().log_level.as_str() { + "error" => 0u32, "warn" => 1, "info" => 2, "debug" => 3, "trace" => 4, _ => 2, }; - dropdown.set_selected(current_pos); + dropdown.set_selected(pos); { let cfg = cfg.clone(); dropdown.connect_selected_notify(move |dd| { @@ -116,7 +104,6 @@ pub fn build() -> GBox { row.append(&dropdown); vbox.append(&row); - // Adapter toggles let adapter_label = Label::new(Some("Adapters")); adapter_label.set_xalign(0.0); adapter_label.set_margin_top(8); @@ -125,25 +112,44 @@ pub fn build() -> GBox { let (kbd, mouse, touchpad, bluetooth, gamepad) = { 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("Mouse", mouse, cfg.clone(), "mouse")); - vbox.append(&adapter_row("Touchpad", touchpad, cfg.clone(), "touchpad")); + vbox.append(&adapter_row("Keyboard", kbd, cfg.clone(), "keyboard")); + vbox.append(&adapter_row("Mouse", mouse, cfg.clone(), "mouse")); + vbox.append(&adapter_row("Touchpad", touchpad, cfg.clone(), "touchpad")); 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"); - save_btn.set_margin_top(16); - save_btn.set_halign(gtk4::Align::Start); + let status_lbl = Label::new(None); + status_lbl.add_css_class("dim-label"); + { let cfg = cfg.clone(); + let path = path.clone(); + let status_lbl = status_lbl.clone(); 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 } diff --git a/bos-settings/src/ui/views/breadbar.rs b/bos-settings/src/ui/views/breadbar.rs index 2314111..9f1ceb7 100644 --- a/bos-settings/src/ui/views/breadbar.rs +++ b/bos-settings/src/ui/views/breadbar.rs @@ -3,7 +3,7 @@ use gtk4::{Box as GBox, Button, Label, Orientation, ScrolledWindow, TextView}; use std::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") } @@ -38,21 +38,39 @@ pub fn build() -> GBox { scroll.set_child(Some(&text_view)); vbox.append(&scroll); + let btn_row = GBox::new(Orientation::Horizontal, 12); + btn_row.set_margin_top(12); + let save_btn = Button::with_label("Save"); - save_btn.set_margin_top(12); - save_btn.set_halign(gtk4::Align::Start); + let status_lbl = Label::new(None); + status_lbl.add_css_class("dim-label"); + { let path = path.clone(); + let status_lbl = status_lbl.clone(); save_btn.connect_clicked(move |_| { let (start, end) = buf.bounds(); let text = buf.text(&start, &end, false); if let Some(parent) = path.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 } diff --git a/bos-settings/src/ui/views/breadbox.rs b/bos-settings/src/ui/views/breadbox.rs index 715ce71..36f88ac 100644 --- a/bos-settings/src/ui/views/breadbox.rs +++ b/bos-settings/src/ui/views/breadbox.rs @@ -29,6 +29,8 @@ fn rebuild_list(list: &ListBox, cfg: &Rc>) { } for (i, ctx) in cfg.borrow().context.iter().enumerate() { let row = ListBoxRow::new(); + row.set_selectable(false); + let hbox = GBox::new(Orientation::Horizontal, 8); hbox.set_margin_top(6); hbox.set_margin_bottom(6); @@ -37,13 +39,17 @@ fn rebuild_list(list: &ListBox, cfg: &Rc>) { let name_entry = Entry::new(); 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(); apps_entry.set_text(&ctx.apps.join(", ")); apps_entry.set_hexpand(true); 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(); name_entry.connect_changed(move |e| { @@ -56,8 +62,7 @@ fn rebuild_list(list: &ListBox, cfg: &Rc>) { let cfg = cfg.clone(); apps_entry.connect_changed(move |e| { if let Some(c) = cfg.borrow_mut().context.get_mut(i) { - c.apps = e - .text() + c.apps = e.text() .split(',') .map(|s| s.trim().to_string()) .filter(|s| !s.is_empty()) @@ -65,9 +70,18 @@ fn rebuild_list(list: &ListBox, cfg: &Rc>) { } }); } + { + 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(&apps_entry); + hbox.append(&remove_btn); row.set_child(Some(&hbox)); list.append(&row); } @@ -93,7 +107,6 @@ pub fn build() -> GBox { let list = ListBox::new(); list.set_selection_mode(gtk4::SelectionMode::None); - rebuild_list(&list, &cfg); let scroll = ScrolledWindow::new(); @@ -118,16 +131,31 @@ pub fn build() -> GBox { } 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 path = path.clone(); + let status_lbl = status_lbl.clone(); 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(&save_btn); + btn_row.append(&status_lbl); vbox.append(&btn_row); vbox diff --git a/bos-settings/src/ui/views/breadcrumbs.rs b/bos-settings/src/ui/views/breadcrumbs.rs index 4062645..f165f43 100644 --- a/bos-settings/src/ui/views/breadcrumbs.rs +++ b/bos-settings/src/ui/views/breadcrumbs.rs @@ -29,6 +29,8 @@ fn rebuild_list(list: &ListBox, cfg: &Rc>) { } for (i, profile) in cfg.borrow().profile.iter().enumerate() { let row = ListBoxRow::new(); + row.set_selectable(false); + let hbox = GBox::new(Orientation::Horizontal, 8); hbox.set_margin_top(6); hbox.set_margin_bottom(6); @@ -37,13 +39,17 @@ fn rebuild_list(list: &ListBox, cfg: &Rc>) { let name_entry = Entry::new(); 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(); ssids_entry.set_text(&profile.ssids.join(", ")); ssids_entry.set_hexpand(true); 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(); name_entry.connect_changed(move |e| { @@ -56,8 +62,7 @@ fn rebuild_list(list: &ListBox, cfg: &Rc>) { let cfg = cfg.clone(); ssids_entry.connect_changed(move |e| { if let Some(p) = cfg.borrow_mut().profile.get_mut(i) { - p.ssids = e - .text() + p.ssids = e.text() .split(',') .map(|s| s.trim().to_string()) .filter(|s| !s.is_empty()) @@ -65,9 +70,18 @@ fn rebuild_list(list: &ListBox, cfg: &Rc>) { } }); } + { + 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(&ssids_entry); + hbox.append(&remove_btn); row.set_child(Some(&hbox)); list.append(&row); } @@ -93,7 +107,6 @@ pub fn build() -> GBox { let list = ListBox::new(); list.set_selection_mode(gtk4::SelectionMode::None); - rebuild_list(&list, &cfg); let scroll = ScrolledWindow::new(); @@ -118,16 +131,31 @@ pub fn build() -> GBox { } 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 path = path.clone(); + let status_lbl = status_lbl.clone(); 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(&save_btn); + btn_row.append(&status_lbl); vbox.append(&btn_row); vbox diff --git a/bos-settings/src/ui/views/breadpad.rs b/bos-settings/src/ui/views/breadpad.rs index 9c553f4..6e24346 100644 --- a/bos-settings/src/ui/views/breadpad.rs +++ b/bos-settings/src/ui/views/breadpad.rs @@ -8,7 +8,7 @@ use crate::config; #[derive(Deserialize, Serialize, Clone)] pub struct BreadpadConfig { - #[serde(default = "default_model")] + #[serde(default)] pub model: String, #[serde(default = "default_true")] pub reminders: bool, @@ -16,21 +16,11 @@ pub struct BreadpadConfig { pub calendar: bool, } -fn default_model() -> String { - "claude-sonnet-4-6".to_string() -} - -fn default_true() -> bool { - true -} +fn default_true() -> bool { true } impl Default for BreadpadConfig { fn default() -> Self { - Self { - model: default_model(), - reminders: true, - calendar: true, - } + Self { model: String::new(), reminders: true, calendar: true } } } @@ -58,6 +48,7 @@ pub fn build() -> GBox { lbl.set_xalign(0.0); let model_entry = Entry::new(); model_entry.set_text(&cfg.borrow().model); + model_entry.set_placeholder_text(Some("e.g. claude-sonnet-4-6")); { let cfg = cfg.clone(); model_entry.connect_changed(move |e| { @@ -68,7 +59,7 @@ pub fn build() -> GBox { row.append(&model_entry); vbox.append(&row); - // Reminders toggle + // Reminders let row = GBox::new(Orientation::Horizontal, 16); let lbl = Label::new(Some("Reminders")); lbl.set_hexpand(true); @@ -77,15 +68,13 @@ pub fn build() -> GBox { sw.set_active(cfg.borrow().reminders); { let cfg = cfg.clone(); - sw.connect_active_notify(move |s| { - cfg.borrow_mut().reminders = s.is_active(); - }); + sw.connect_active_notify(move |s| { cfg.borrow_mut().reminders = s.is_active(); }); } row.append(&lbl); row.append(&sw); vbox.append(&row); - // Calendar toggle + // Calendar let row = GBox::new(Orientation::Horizontal, 16); let lbl = Label::new(Some("Calendar integration")); lbl.set_hexpand(true); @@ -94,25 +83,40 @@ pub fn build() -> GBox { sw.set_active(cfg.borrow().calendar); { let cfg = cfg.clone(); - sw.connect_active_notify(move |s| { - cfg.borrow_mut().calendar = s.is_active(); - }); + sw.connect_active_notify(move |s| { cfg.borrow_mut().calendar = s.is_active(); }); } row.append(&lbl); row.append(&sw); vbox.append(&row); + let btn_row = GBox::new(Orientation::Horizontal, 12); + btn_row.set_margin_top(16); + let save_btn = Button::with_label("Save"); - save_btn.set_margin_top(16); - save_btn.set_halign(gtk4::Align::Start); + let status_lbl = Label::new(None); + status_lbl.add_css_class("dim-label"); + { let cfg = cfg.clone(); - let path = path.clone(); + let status_lbl = status_lbl.clone(); 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 } diff --git a/bos-settings/src/ui/views/hyprland.rs b/bos-settings/src/ui/views/hyprland.rs index cb43b30..0ed704d 100644 --- a/bos-settings/src/ui/views/hyprland.rs +++ b/bos-settings/src/ui/views/hyprland.rs @@ -17,14 +17,14 @@ fn get_monitors() -> Vec { let w = m.get("width")?.as_u64()?; let h = m.get("height")?.as_u64()?; 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() } -fn config_path() -> std::path::PathBuf { - let home = std::env::var("HOME").unwrap_or_else(|_| "/root".to_string()); - std::path::PathBuf::from(home).join(".config/hypr/hyprland.conf") +fn hypr_path(name: &str) -> std::path::PathBuf { + let home = std::env::var("HOME").unwrap_or_else(|_| "/home/user".to_string()); + std::path::PathBuf::from(home).join(".config/hypr").join(name) } pub fn build() -> GBox { @@ -36,7 +36,6 @@ pub fn build() -> GBox { title.set_xalign(0.0); vbox.append(&title); - // Monitors section let monitors_lbl = Label::new(Some("Connected monitors")); monitors_lbl.set_xalign(0.0); monitors_lbl.set_margin_top(8); @@ -52,38 +51,35 @@ pub fn build() -> GBox { for mon in &monitors { let lbl = Label::new(Some(mon)); lbl.set_xalign(0.0); - lbl.add_css_class("monospace"); + lbl.set_monospace(true); vbox.append(&lbl); } } - // Open config button let open_btn = Button::with_label("Open hyprland.conf in editor"); open_btn.set_margin_top(16); open_btn.set_halign(gtk4::Align::Start); { - let path = config_path(); + let conf_path = hypr_path("hyprland.conf"); open_btn.connect_clicked(move |_| { let editor = std::env::var("EDITOR").unwrap_or_else(|_| "foot".to_string()); - let _ = Command::new(&editor) - .arg(path.to_str().unwrap_or("")) - .spawn(); + if let Ok(mut child) = Command::new(&editor).arg(&conf_path).spawn() { + std::thread::spawn(move || { let _ = child.wait(); }); + } }); } vbox.append(&open_btn); - // Open keybinds button let keybinds_btn = Button::with_label("Open keybinds.conf in editor"); keybinds_btn.set_margin_top(8); keybinds_btn.set_halign(gtk4::Align::Start); { - let home = std::env::var("HOME").unwrap_or_else(|_| "/root".to_string()); - let kb_path = std::path::PathBuf::from(home).join(".config/hypr/keybinds.conf"); + let kb_path = hypr_path("keybinds.conf"); keybinds_btn.connect_clicked(move |_| { let editor = std::env::var("EDITOR").unwrap_or_else(|_| "foot".to_string()); - let _ = Command::new(&editor) - .arg(kb_path.to_str().unwrap_or("")) - .spawn(); + if let Ok(mut child) = Command::new(&editor).arg(&kb_path).spawn() { + std::thread::spawn(move || { let _ = child.wait(); }); + } }); } vbox.append(&keybinds_btn); diff --git a/bos-settings/src/ui/views/packages.rs b/bos-settings/src/ui/views/packages.rs index 448ec53..6b3aedc 100644 --- a/bos-settings/src/ui/views/packages.rs +++ b/bos-settings/src/ui/views/packages.rs @@ -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, -} - -#[derive(Deserialize)] -struct PackageInfo { - version: String, -} +use std::io::{BufRead, BufReader}; +use std::process::{Command, Stdio}; fn read_installed() -> HashMap { - 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::>(&text) else { return HashMap::new(); }; @@ -35,15 +24,57 @@ fn read_installed() -> HashMap { .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::(256); + let args: Vec = 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()); }); } diff --git a/bos-settings/src/ui/views/snapshots.rs b/bos-settings/src/ui/views/snapshots.rs index da34141..d3ac8b7 100644 --- a/bos-settings/src/ui/views/snapshots.rs +++ b/bos-settings/src/ui/views/snapshots.rs @@ -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 { 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::().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::().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); diff --git a/dotfiles/hyprland/hyprland.conf b/dotfiles/hypr/hyprland.conf similarity index 90% rename from dotfiles/hyprland/hyprland.conf rename to dotfiles/hypr/hyprland.conf index 5e56982..e509b63 100644 --- a/dotfiles/hyprland/hyprland.conf +++ b/dotfiles/hypr/hyprland.conf @@ -22,9 +22,11 @@ decoration { size = 6 passes = 2 } - drop_shadow = true - shadow_range = 12 - shadow_render_power = 3 + shadow { + enabled = true + range = 12 + render_power = 3 + } } animations { diff --git a/dotfiles/hyprland/keybinds.conf b/dotfiles/hypr/keybinds.conf similarity index 100% rename from dotfiles/hyprland/keybinds.conf rename to dotfiles/hypr/keybinds.conf diff --git a/iso/airootfs/etc/calamares/branding/bos/branding.desc b/iso/airootfs/etc/calamares/branding/bos/branding.desc new file mode 100644 index 0000000..d3034ab --- /dev/null +++ b/iso/airootfs/etc/calamares/branding/bos/branding.desc @@ -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" diff --git a/iso/airootfs/etc/calamares/branding/bos/show.qml b/iso/airootfs/etc/calamares/branding/bos/show.qml new file mode 100644 index 0000000..446c624 --- /dev/null +++ b/iso/airootfs/etc/calamares/branding/bos/show.qml @@ -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 + } + } + } + } +} diff --git a/iso/airootfs/etc/calamares/modules/mount.conf b/iso/airootfs/etc/calamares/modules/mount.conf new file mode 100644 index 0000000..767bf57 --- /dev/null +++ b/iso/airootfs/etc/calamares/modules/mount.conf @@ -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] diff --git a/iso/airootfs/etc/calamares/modules/unpackfs.conf b/iso/airootfs/etc/calamares/modules/unpackfs.conf new file mode 100644 index 0000000..7d9589e --- /dev/null +++ b/iso/airootfs/etc/calamares/modules/unpackfs.conf @@ -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: "" diff --git a/iso/post-install.sh b/iso/airootfs/etc/calamares/post-install.sh similarity index 77% rename from iso/post-install.sh rename to iso/airootfs/etc/calamares/post-install.sh index e3f3182..967292b 100644 --- a/iso/post-install.sh +++ b/iso/airootfs/etc/calamares/post-install.sh @@ -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_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) 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 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 sudo -u "$MAIN_USER" bakery install bread breadbar breadbox breadcrumbs breadpad bos-settings fi -# --- Deploy dotfiles (skip existing files) --- -DOTFILES_SRC="/etc/skel/.config" +# --- Deploy dotfiles into user home (skip any file that already exists) --- +SKEL_SRC="/etc/skel/.config" DOTFILES_DEST="/home/$MAIN_USER/.config" -if [[ -d "$DOTFILES_SRC" ]]; then +if [[ -d "$SKEL_SRC" ]]; then mkdir -p "$DOTFILES_DEST" - cp -rn "$DOTFILES_SRC/." "$DOTFILES_DEST/" + cp -rn "$SKEL_SRC/." "$DOTFILES_DEST/" chown -R "$MAIN_USER:$MAIN_USER" "$DOTFILES_DEST" fi diff --git a/iso/airootfs/etc/calamares/settings.conf b/iso/airootfs/etc/calamares/settings.conf index 499e1cb..ea17565 100644 --- a/iso/airootfs/etc/calamares/settings.conf +++ b/iso/airootfs/etc/calamares/settings.conf @@ -1,5 +1,5 @@ --- -modules-search: [local, /usr/lib/calamares/modules] +modules-search: [/etc/calamares/modules, /usr/lib/calamares/modules] sequence: - show: diff --git a/iso/airootfs/etc/polkit-1/rules.d/10-snapper.rules b/iso/airootfs/etc/polkit-1/rules.d/10-snapper.rules new file mode 100644 index 0000000..dc2b538 --- /dev/null +++ b/iso/airootfs/etc/polkit-1/rules.d/10-snapper.rules @@ -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; + } +}); diff --git a/iso/airootfs/etc/skel/.config/bread/breadd.toml b/iso/airootfs/etc/skel/.config/bread/breadd.toml new file mode 100644 index 0000000..8473fe3 --- /dev/null +++ b/iso/airootfs/etc/skel/.config/bread/breadd.toml @@ -0,0 +1,8 @@ +log_level = "info" + +[adapters] +keyboard = true +mouse = true +touchpad = true +bluetooth = true +gamepad = true diff --git a/iso/airootfs/etc/skel/.config/bread/init.lua b/iso/airootfs/etc/skel/.config/bread/init.lua new file mode 100644 index 0000000..270ee7e --- /dev/null +++ b/iso/airootfs/etc/skel/.config/bread/init.lua @@ -0,0 +1 @@ +bread.activate_profile("default") diff --git a/iso/airootfs/etc/skel/.config/breadbox/config.toml b/iso/airootfs/etc/skel/.config/breadbox/config.toml new file mode 100644 index 0000000..797b3cc --- /dev/null +++ b/iso/airootfs/etc/skel/.config/breadbox/config.toml @@ -0,0 +1,3 @@ +[[context]] +name = "default" +apps = ["firefox", "foot", "nautilus", "code"] diff --git a/iso/airootfs/etc/skel/.config/breadcrumbs/breadcrumbs.toml b/iso/airootfs/etc/skel/.config/breadcrumbs/breadcrumbs.toml new file mode 100644 index 0000000..46e26f3 --- /dev/null +++ b/iso/airootfs/etc/skel/.config/breadcrumbs/breadcrumbs.toml @@ -0,0 +1,3 @@ +[[profile]] +name = "home" +ssids = [] diff --git a/iso/airootfs/etc/skel/.config/hypr/hyprland.conf b/iso/airootfs/etc/skel/.config/hypr/hyprland.conf new file mode 100644 index 0000000..e509b63 --- /dev/null +++ b/iso/airootfs/etc/skel/.config/hypr/hyprland.conf @@ -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 +} diff --git a/iso/airootfs/etc/skel/.config/hypr/keybinds.conf b/iso/airootfs/etc/skel/.config/hypr/keybinds.conf new file mode 100644 index 0000000..7cf8cdd --- /dev/null +++ b/iso/airootfs/etc/skel/.config/hypr/keybinds.conf @@ -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 diff --git a/iso/airootfs/etc/systemd/system/getty@tty1.service.d/autologin.conf b/iso/airootfs/etc/systemd/system/getty@tty1.service.d/autologin.conf new file mode 100644 index 0000000..b9d22eb --- /dev/null +++ b/iso/airootfs/etc/systemd/system/getty@tty1.service.d/autologin.conf @@ -0,0 +1,3 @@ +[Service] +ExecStart= +ExecStart=-/sbin/agetty -o '-p -f -- \\u' --noclear --autologin root - $TERM diff --git a/iso/airootfs/root/.bash_profile b/iso/airootfs/root/.bash_profile new file mode 100644 index 0000000..fd13f6a --- /dev/null +++ b/iso/airootfs/root/.bash_profile @@ -0,0 +1,4 @@ +# Auto-start Hyprland on tty1 in the live session +if [[ "$(tty)" == "/dev/tty1" ]] && [[ -z "$WAYLAND_DISPLAY" ]]; then + exec Hyprland +fi diff --git a/iso/airootfs/root/.config/hypr/hyprland.conf b/iso/airootfs/root/.config/hypr/hyprland.conf new file mode 100644 index 0000000..8adb80a --- /dev/null +++ b/iso/airootfs/root/.config/hypr/hyprland.conf @@ -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 +} diff --git a/iso/packages.x86_64 b/iso/packages.x86_64 index b7fbf35..32c97d4 100644 --- a/iso/packages.x86_64 +++ b/iso/packages.x86_64 @@ -47,10 +47,9 @@ gtk4-layer-shell librsvg libpulse -# Display +# Display (wlroots is bundled with Hyprland; don't list separately) wayland wayland-protocols -wlroots # Fonts noto-fonts @@ -63,10 +62,13 @@ foot # File manager nautilus -# Installer +# Installer — sourced from [breadway] repo (see pacman.conf) calamares calamares-qt6 +# Bread ecosystem — sourced from [breadway] repo +bakery + # Utilities sudo git diff --git a/iso/pacman.conf b/iso/pacman.conf new file mode 100644 index 0000000..c071a87 --- /dev/null +++ b/iso/pacman.conf @@ -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