Fix boot-critical, session, and UX bugs found in round-3 UX audit
Snapshots panel was non-functional (wrong snapper flag); breadgreet picked the wrong session .desktop and skipped bos-session's PATH fixup; Calamares aborted offline installs over an unnecessary packages module; snapshot rollback silently no-op'd on BOS's pinned-subvolume layout (now points at grub-btrfs instead); breadcrumbs was configurable but had no daemon to run it. Also: SUPER+I double-bind, breadpaper/packages panels blocking the GTK main thread, dead polkit rule, and a sweep of smaller drift (default sidebar view, stale wording, missing .desktop launchers, wallpaper daemon not recording its own default).
This commit is contained in:
parent
a8f1592e75
commit
d78a2343f4
32 changed files with 553 additions and 209 deletions
|
|
@ -12,6 +12,7 @@ pub const APPS_ITEMS: &[SidebarItem] = &[
|
||||||
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" },
|
||||||
|
SidebarItem { id: "breadpaper", label: "breadpaper" },
|
||||||
SidebarItem { id: "breadsearch", label: "breadsearch" },
|
SidebarItem { id: "breadsearch", label: "breadsearch" },
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|
@ -33,12 +34,12 @@ 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 the snapshots row so it matches the default stack page
|
// Select the bread row so it matches the default stack page
|
||||||
let mut i = 0;
|
let mut i = 0;
|
||||||
loop {
|
loop {
|
||||||
match list.row_at_index(i) {
|
match list.row_at_index(i) {
|
||||||
None => break,
|
None => break,
|
||||||
Some(row) if row.widget_name() == "snapshots" => {
|
Some(row) if row.widget_name() == "bread" => {
|
||||||
list.select_row(Some(&row));
|
list.select_row(Some(&row));
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -356,7 +356,9 @@ pub fn build() -> GBox {
|
||||||
&doc,
|
&doc,
|
||||||
&["settings", "default_profile"],
|
&["settings", "default_profile"],
|
||||||
&["home", "away"],
|
&["home", "away"],
|
||||||
"home",
|
// breadcrumbs' own default_profile_name() is "away", not "home" —
|
||||||
|
// this was showing the wrong value for an unset key.
|
||||||
|
"away",
|
||||||
));
|
));
|
||||||
content.append(&w::entry_row("DNS", &doc, &["settings", "dns"], "1.1.1.1", ""));
|
content.append(&w::entry_row("DNS", &doc, &["settings", "dns"], "1.1.1.1", ""));
|
||||||
content.append(&w::entry_row(
|
content.append(&w::entry_row(
|
||||||
|
|
|
||||||
153
bos-settings/src/ui/views/breadpaper.rs
Normal file
153
bos-settings/src/ui/views/breadpaper.rs
Normal file
|
|
@ -0,0 +1,153 @@
|
||||||
|
//! breadpaper — wallpaper manager. No config file to edit here; breadpaper
|
||||||
|
//! takes no persistent settings, just an image path via its CLI (`breadpaper
|
||||||
|
//! set <path>` / `breadpaper get`). This panel is a thin GUI front-end for
|
||||||
|
//! that CLI so wallpaper (and the pywal-driven theme it generates) has a
|
||||||
|
//! discoverable home in BOS Settings instead of only being reachable from a
|
||||||
|
//! terminal.
|
||||||
|
|
||||||
|
use std::path::PathBuf;
|
||||||
|
use std::process::Command;
|
||||||
|
|
||||||
|
use gtk4::prelude::*;
|
||||||
|
use gtk4::{
|
||||||
|
Box as GBox, Button, FileChooserAction, FileChooserDialog, Image, Label, Orientation,
|
||||||
|
ResponseType,
|
||||||
|
};
|
||||||
|
|
||||||
|
use crate::ui::widgets as w;
|
||||||
|
|
||||||
|
fn current_wallpaper() -> Option<PathBuf> {
|
||||||
|
let out = Command::new("breadpaper").arg("get").output().ok()?;
|
||||||
|
if !out.status.success() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let s = String::from_utf8_lossy(&out.stdout).trim().to_string();
|
||||||
|
if s.is_empty() {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
Some(PathBuf::from(s))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn refresh_preview(preview: &Image, path_lbl: &Label) {
|
||||||
|
match current_wallpaper() {
|
||||||
|
Some(path) => {
|
||||||
|
path_lbl.set_text(&path.display().to_string());
|
||||||
|
preview.set_from_file(Some(&path));
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
path_lbl.set_text("No wallpaper set");
|
||||||
|
preview.set_icon_name(Some("image-missing"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn build() -> GBox {
|
||||||
|
let (outer, c) = w::view_scaffold("breadpaper");
|
||||||
|
|
||||||
|
c.append(&w::hint(
|
||||||
|
"Sets the desktop wallpaper, generates a matching pywal palette, and \
|
||||||
|
reloads the shared bread-theme stylesheet — the wallpaper drives \
|
||||||
|
the whole desktop's accent colors.",
|
||||||
|
));
|
||||||
|
|
||||||
|
let preview = Image::new();
|
||||||
|
preview.set_pixel_size(320);
|
||||||
|
preview.set_margin_top(8);
|
||||||
|
preview.set_margin_bottom(8);
|
||||||
|
c.append(&preview);
|
||||||
|
|
||||||
|
let path_lbl = Label::new(None);
|
||||||
|
path_lbl.set_wrap(true);
|
||||||
|
path_lbl.add_css_class("dim-label");
|
||||||
|
c.append(&path_lbl);
|
||||||
|
|
||||||
|
refresh_preview(&preview, &path_lbl);
|
||||||
|
|
||||||
|
let btn_row = GBox::new(Orientation::Horizontal, 8);
|
||||||
|
btn_row.set_margin_top(8);
|
||||||
|
|
||||||
|
let choose_btn = Button::with_label("Choose image...");
|
||||||
|
let status = Label::new(None);
|
||||||
|
status.add_css_class("dim-label");
|
||||||
|
|
||||||
|
{
|
||||||
|
let preview = preview.clone();
|
||||||
|
let path_lbl = path_lbl.clone();
|
||||||
|
let status = status.clone();
|
||||||
|
choose_btn.connect_clicked(move |btn| {
|
||||||
|
let window = btn.root().and_then(|r| r.downcast::<gtk4::Window>().ok());
|
||||||
|
let dialog = FileChooserDialog::new(
|
||||||
|
Some("Choose a wallpaper"),
|
||||||
|
window.as_ref(),
|
||||||
|
FileChooserAction::Open,
|
||||||
|
&[("Cancel", ResponseType::Cancel), ("Set", ResponseType::Accept)],
|
||||||
|
);
|
||||||
|
// Restricted to what breadpaper's own validate() actually
|
||||||
|
// accepts (png/jpg/jpeg/webp/gif/bmp) — add_pixbuf_formats()
|
||||||
|
// also offers svg/tiff/etc. that breadpaper rejects outright.
|
||||||
|
let filter = gtk4::FileFilter::new();
|
||||||
|
for ext in ["png", "jpg", "jpeg", "webp", "gif", "bmp"] {
|
||||||
|
filter.add_suffix(ext);
|
||||||
|
}
|
||||||
|
filter.set_name(Some("Images"));
|
||||||
|
dialog.add_filter(&filter);
|
||||||
|
|
||||||
|
let preview = preview.clone();
|
||||||
|
let path_lbl = path_lbl.clone();
|
||||||
|
let status = status.clone();
|
||||||
|
dialog.connect_response(move |dialog, response| {
|
||||||
|
if response == ResponseType::Accept {
|
||||||
|
if let Some(file) = dialog.file() {
|
||||||
|
if let Some(path) = file.path() {
|
||||||
|
// breadpaper set runs `awww img` + pywal palette
|
||||||
|
// generation, which is routinely 1-3s (pywal
|
||||||
|
// spawns Python + an ImageMagick backend) — not
|
||||||
|
// the "sub-second" call this used to assume.
|
||||||
|
// GTK widgets aren't Send, so run it in a thread
|
||||||
|
// and hand the result back over a channel
|
||||||
|
// (same pattern as snapshots.rs).
|
||||||
|
status.set_text("Setting...");
|
||||||
|
let (tx, rx) = async_channel::bounded::<bool>(1);
|
||||||
|
std::thread::spawn(move || {
|
||||||
|
let ok = Command::new("breadpaper")
|
||||||
|
.arg("set")
|
||||||
|
.arg(&path)
|
||||||
|
.status()
|
||||||
|
.map(|s| s.success())
|
||||||
|
.unwrap_or(false);
|
||||||
|
let _ = tx.send_blocking(ok);
|
||||||
|
});
|
||||||
|
|
||||||
|
let preview = preview.clone();
|
||||||
|
let path_lbl = path_lbl.clone();
|
||||||
|
let status = status.clone();
|
||||||
|
glib::spawn_future_local(async move {
|
||||||
|
let ok = rx.recv().await.unwrap_or(false);
|
||||||
|
if ok {
|
||||||
|
refresh_preview(&preview, &path_lbl);
|
||||||
|
status.set_text("Wallpaper set");
|
||||||
|
} else {
|
||||||
|
status.set_text("breadpaper failed — see terminal/journal");
|
||||||
|
}
|
||||||
|
let lbl = status.clone();
|
||||||
|
glib::timeout_add_seconds_local(3, move || {
|
||||||
|
lbl.set_text("");
|
||||||
|
glib::ControlFlow::Break
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
dialog.close();
|
||||||
|
});
|
||||||
|
dialog.show();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
btn_row.append(&choose_btn);
|
||||||
|
btn_row.append(&status);
|
||||||
|
c.append(&btn_row);
|
||||||
|
|
||||||
|
outer
|
||||||
|
}
|
||||||
|
|
@ -34,16 +34,22 @@ pub fn build() -> GBox {
|
||||||
));
|
));
|
||||||
|
|
||||||
c.append(&w::section("Model"));
|
c.append(&w::section("Model"));
|
||||||
|
// breadmill supports npu/rocm backends in its own codebase, but the
|
||||||
|
// prebuilt binary bakery actually ships is CPU-only (no --features
|
||||||
|
// npu/rocm in its release build) — offering them here would just be a
|
||||||
|
// dropdown option that silently falls back to cpu. Re-add once a build
|
||||||
|
// with those features is published.
|
||||||
c.append(&w::dropdown_row(
|
c.append(&w::dropdown_row(
|
||||||
"Compute backend",
|
"Compute backend",
|
||||||
&doc,
|
&doc,
|
||||||
&["model", "backend"],
|
&["model", "backend"],
|
||||||
&["cpu", "npu", "rocm"],
|
&["cpu"],
|
||||||
"cpu",
|
"cpu",
|
||||||
));
|
));
|
||||||
c.append(&w::hint(
|
c.append(&w::hint(
|
||||||
"npu needs breadmill built with --features npu (AMD Ryzen AI SDK); \
|
"The bakery-published breadmill is CPU-only for now. NPU (AMD Ryzen \
|
||||||
rocm needs --features rocm. Falls back to cpu if not compiled in.",
|
AI) and ROCm backends exist in breadmill's own code but need a \
|
||||||
|
separately-built binary with those features enabled.",
|
||||||
));
|
));
|
||||||
|
|
||||||
c.append(&w::section("Index"));
|
c.append(&w::section("Index"));
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ pub mod breadbar;
|
||||||
pub mod breadbox;
|
pub mod breadbox;
|
||||||
pub mod breadcrumbs;
|
pub mod breadcrumbs;
|
||||||
pub mod breadpad;
|
pub mod breadpad;
|
||||||
|
pub mod breadpaper;
|
||||||
pub mod breadsearch;
|
pub mod breadsearch;
|
||||||
pub mod hyprland;
|
pub mod hyprland;
|
||||||
pub mod packages;
|
pub mod packages;
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,8 @@ use std::collections::HashMap;
|
||||||
use std::io::{BufRead, BufReader};
|
use std::io::{BufRead, BufReader};
|
||||||
use std::process::{Command, Stdio};
|
use std::process::{Command, Stdio};
|
||||||
|
|
||||||
|
use crate::ui::widgets as w;
|
||||||
|
|
||||||
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(|_| "/root".to_string());
|
||||||
let path = std::path::Path::new(&home)
|
let path = std::path::Path::new(&home)
|
||||||
|
|
@ -42,6 +44,14 @@ fn read_installed() -> HashMap<String, String> {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn stream_command(args: &[&str], log_buf: gtk4::TextBuffer) {
|
fn stream_command(args: &[&str], log_buf: gtk4::TextBuffer) {
|
||||||
|
stream_command_then(args, log_buf, || {});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Same as stream_command, but runs `on_done` once the process's output
|
||||||
|
/// stream ends (i.e. the child has exited) — the channel closes when both
|
||||||
|
/// the stdout- and stderr-forwarding threads drop their sender, which only
|
||||||
|
/// happens after `child.wait()` returns.
|
||||||
|
fn stream_command_then(args: &[&str], log_buf: gtk4::TextBuffer, on_done: impl FnOnce() + 'static) {
|
||||||
let (sender, receiver) = async_channel::bounded::<String>(256);
|
let (sender, receiver) = async_channel::bounded::<String>(256);
|
||||||
let args: Vec<String> = args.iter().map(|s| s.to_string()).collect();
|
let args: Vec<String> = args.iter().map(|s| s.to_string()).collect();
|
||||||
|
|
||||||
|
|
@ -65,7 +75,7 @@ fn stream_command(args: &[&str], log_buf: gtk4::TextBuffer) {
|
||||||
let stderr = child.stderr.take().expect("stderr piped");
|
let stderr = child.stderr.take().expect("stderr piped");
|
||||||
|
|
||||||
let tx2 = sender.clone();
|
let tx2 = sender.clone();
|
||||||
std::thread::spawn(move || {
|
let stderr_thread = std::thread::spawn(move || {
|
||||||
for line in BufReader::new(stderr).lines().flatten() {
|
for line in BufReader::new(stderr).lines().flatten() {
|
||||||
let _ = tx2.send_blocking(line);
|
let _ = tx2.send_blocking(line);
|
||||||
}
|
}
|
||||||
|
|
@ -75,6 +85,7 @@ fn stream_command(args: &[&str], log_buf: gtk4::TextBuffer) {
|
||||||
let _ = sender.send_blocking(line);
|
let _ = sender.send_blocking(line);
|
||||||
}
|
}
|
||||||
let _ = child.wait();
|
let _ = child.wait();
|
||||||
|
let _ = stderr_thread.join();
|
||||||
});
|
});
|
||||||
|
|
||||||
glib::spawn_future_local(async move {
|
glib::spawn_future_local(async move {
|
||||||
|
|
@ -82,25 +93,14 @@ fn stream_command(args: &[&str], log_buf: gtk4::TextBuffer) {
|
||||||
let mut end = log_buf.end_iter();
|
let mut end = log_buf.end_iter();
|
||||||
log_buf.insert(&mut end, &format!("{line}\n"));
|
log_buf.insert(&mut end, &format!("{line}\n"));
|
||||||
}
|
}
|
||||||
|
on_done();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn build() -> GBox {
|
fn populate_packages(list: &ListBox, log_buf: >k4::TextBuffer) {
|
||||||
let vbox = GBox::new(Orientation::Vertical, 0);
|
while let Some(child) = list.first_child() {
|
||||||
vbox.add_css_class("view-content");
|
list.remove(&child);
|
||||||
|
}
|
||||||
let title = Label::new(Some("Packages"));
|
|
||||||
title.add_css_class("title");
|
|
||||||
title.set_xalign(0.0);
|
|
||||||
vbox.append(&title);
|
|
||||||
|
|
||||||
let subtitle = Label::new(Some("Bread ecosystem packages installed via bakery."));
|
|
||||||
subtitle.set_xalign(0.0);
|
|
||||||
subtitle.set_margin_bottom(16);
|
|
||||||
vbox.append(&subtitle);
|
|
||||||
|
|
||||||
let list = ListBox::new();
|
|
||||||
list.set_selection_mode(gtk4::SelectionMode::None);
|
|
||||||
|
|
||||||
let packages = read_installed();
|
let packages = read_installed();
|
||||||
if packages.is_empty() {
|
if packages.is_empty() {
|
||||||
|
|
@ -114,52 +114,82 @@ pub fn build() -> GBox {
|
||||||
lbl.set_margin_start(8);
|
lbl.set_margin_start(8);
|
||||||
row.set_child(Some(&lbl));
|
row.set_child(Some(&lbl));
|
||||||
list.append(&row);
|
list.append(&row);
|
||||||
} else {
|
return;
|
||||||
let mut names: Vec<_> = packages.iter().collect();
|
|
||||||
names.sort_by_key(|(k, _)| k.as_str());
|
|
||||||
|
|
||||||
for (name, version) in names {
|
|
||||||
let row = ListBoxRow::new();
|
|
||||||
row.set_selectable(false);
|
|
||||||
let hbox = GBox::new(Orientation::Horizontal, 16);
|
|
||||||
hbox.set_margin_top(6);
|
|
||||||
hbox.set_margin_bottom(6);
|
|
||||||
hbox.set_margin_start(8);
|
|
||||||
hbox.set_margin_end(8);
|
|
||||||
|
|
||||||
let name_lbl = Label::new(Some(name));
|
|
||||||
name_lbl.set_hexpand(true);
|
|
||||||
name_lbl.set_xalign(0.0);
|
|
||||||
|
|
||||||
let ver_lbl = Label::new(Some(version));
|
|
||||||
ver_lbl.set_xalign(1.0);
|
|
||||||
|
|
||||||
// Spawn a thread to reap the child process — no zombies
|
|
||||||
let pkg_name = name.clone();
|
|
||||||
let update_btn = Button::with_label("Update");
|
|
||||||
update_btn.connect_clicked(move |_| {
|
|
||||||
match Command::new("bakery").args(["update", &pkg_name]).spawn() {
|
|
||||||
Ok(mut child) => {
|
|
||||||
std::thread::spawn(move || { let _ = child.wait(); });
|
|
||||||
}
|
|
||||||
Err(_) => {} // bakery not found; button is a no-op
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
hbox.append(&name_lbl);
|
|
||||||
hbox.append(&ver_lbl);
|
|
||||||
hbox.append(&update_btn);
|
|
||||||
row.set_child(Some(&hbox));
|
|
||||||
list.append(&row);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let mut names: Vec<_> = packages.iter().collect();
|
||||||
|
names.sort_by_key(|(k, _)| k.as_str());
|
||||||
|
|
||||||
|
for (name, version) in names {
|
||||||
|
let row = ListBoxRow::new();
|
||||||
|
row.set_selectable(false);
|
||||||
|
let hbox = GBox::new(Orientation::Horizontal, 16);
|
||||||
|
hbox.set_margin_top(6);
|
||||||
|
hbox.set_margin_bottom(6);
|
||||||
|
hbox.set_margin_start(8);
|
||||||
|
hbox.set_margin_end(8);
|
||||||
|
|
||||||
|
let name_lbl = Label::new(Some(name));
|
||||||
|
name_lbl.set_hexpand(true);
|
||||||
|
name_lbl.set_xalign(0.0);
|
||||||
|
|
||||||
|
let ver_lbl = Label::new(Some(version));
|
||||||
|
ver_lbl.set_xalign(1.0);
|
||||||
|
|
||||||
|
let pkg_name = name.clone();
|
||||||
|
let update_btn = Button::with_label("Update");
|
||||||
|
{
|
||||||
|
let log_buf = log_buf.clone();
|
||||||
|
let list = list.clone();
|
||||||
|
update_btn.connect_clicked(move |_| {
|
||||||
|
log_buf.set_text("");
|
||||||
|
let list2 = list.clone();
|
||||||
|
let log_buf2 = log_buf.clone();
|
||||||
|
// Route through stream_command (like the other buttons) so
|
||||||
|
// output is visible and the row refreshes with the new
|
||||||
|
// version once the update actually finishes — previously
|
||||||
|
// this was fire-and-forget with the Err case silently
|
||||||
|
// swallowed, so a missing `bakery` binary made the button
|
||||||
|
// look broken with zero feedback either way.
|
||||||
|
stream_command_then(&["bakery", "update", &pkg_name], log_buf.clone(), move || {
|
||||||
|
populate_packages(&list2, &log_buf2);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
hbox.append(&name_lbl);
|
||||||
|
hbox.append(&ver_lbl);
|
||||||
|
hbox.append(&update_btn);
|
||||||
|
row.set_child(Some(&hbox));
|
||||||
|
list.append(&row);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn build() -> GBox {
|
||||||
|
let vbox = GBox::new(Orientation::Vertical, 0);
|
||||||
|
vbox.add_css_class("view-content");
|
||||||
|
|
||||||
|
let title = Label::new(Some("Packages"));
|
||||||
|
title.add_css_class("title");
|
||||||
|
title.set_xalign(0.0);
|
||||||
|
vbox.append(&title);
|
||||||
|
|
||||||
|
let subtitle = Label::new(Some("Bread ecosystem packages installed via bakery, and system packages via pacman below."));
|
||||||
|
subtitle.set_xalign(0.0);
|
||||||
|
subtitle.set_margin_bottom(16);
|
||||||
|
vbox.append(&subtitle);
|
||||||
|
|
||||||
|
let list = ListBox::new();
|
||||||
|
list.set_selection_mode(gtk4::SelectionMode::None);
|
||||||
|
|
||||||
|
let log_buf = gtk4::TextBuffer::new(None);
|
||||||
|
populate_packages(&list, &log_buf);
|
||||||
|
|
||||||
let scroll = ScrolledWindow::new();
|
let scroll = ScrolledWindow::new();
|
||||||
scroll.set_vexpand(true);
|
scroll.set_vexpand(true);
|
||||||
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);
|
let log_view = TextView::with_buffer(&log_buf);
|
||||||
log_view.set_editable(false);
|
log_view.set_editable(false);
|
||||||
log_view.set_monospace(true);
|
log_view.set_monospace(true);
|
||||||
|
|
@ -169,7 +199,9 @@ 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 check_btn = Button::with_label("Check for updates");
|
// Labeled "List installed", not "Check for updates" — bakery list is a
|
||||||
|
// listing of installed packages, it doesn't check for available updates.
|
||||||
|
let check_btn = Button::with_label("List installed");
|
||||||
let update_all_btn = Button::with_label("Update all");
|
let update_all_btn = Button::with_label("Update all");
|
||||||
|
|
||||||
{
|
{
|
||||||
|
|
@ -191,6 +223,35 @@ pub fn build() -> GBox {
|
||||||
btn_row.append(&check_btn);
|
btn_row.append(&check_btn);
|
||||||
btn_row.append(&update_all_btn);
|
btn_row.append(&update_all_btn);
|
||||||
vbox.append(&btn_row);
|
vbox.append(&btn_row);
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------
|
||||||
|
// System packages (pacman) — the other update channel. bakery only
|
||||||
|
// covers the userspace bread apps; base system/kernel/bos-settings/AUR
|
||||||
|
// republished packages come from pacman + the [breadway] repo, and
|
||||||
|
// bos-update (the CLI) already updates both — this panel previously
|
||||||
|
// only exposed the bakery half, so a user relying on it alone would
|
||||||
|
// never get base-system updates through the GUI.
|
||||||
|
// ---------------------------------------------------------------------
|
||||||
|
vbox.append(&w::section("System packages (pacman)"));
|
||||||
|
vbox.append(&w::hint(
|
||||||
|
"Base system, kernel, bos-settings, and republished AUR packages — \
|
||||||
|
the other half of what `bos-update` covers. Needs your password \
|
||||||
|
(polkit) since pacman requires root.",
|
||||||
|
));
|
||||||
|
|
||||||
|
let pacman_btn_row = GBox::new(Orientation::Horizontal, 8);
|
||||||
|
pacman_btn_row.set_margin_top(8);
|
||||||
|
let pacman_update_btn = Button::with_label("Update system (pacman -Syu)");
|
||||||
|
{
|
||||||
|
let log_buf = log_buf.clone();
|
||||||
|
pacman_update_btn.connect_clicked(move |_| {
|
||||||
|
log_buf.set_text("");
|
||||||
|
stream_command(&["pkexec", "pacman", "-Syu", "--noconfirm"], log_buf.clone());
|
||||||
|
});
|
||||||
|
}
|
||||||
|
pacman_btn_row.append(&pacman_update_btn);
|
||||||
|
vbox.append(&pacman_btn_row);
|
||||||
|
|
||||||
vbox.append(&log_view);
|
vbox.append(&log_view);
|
||||||
|
|
||||||
vbox
|
vbox
|
||||||
|
|
|
||||||
|
|
@ -12,20 +12,37 @@ struct SnapshotRow {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn list_snapshots() -> Vec<SnapshotRow> {
|
fn list_snapshots() -> Vec<SnapshotRow> {
|
||||||
|
// NOTE: the real flag is --columns, not --output-cols (which snapper
|
||||||
|
// rejects outright with "Unknown option") — confirmed against snapper
|
||||||
|
// 0.13's own --help. With the wrong flag this always failed and the
|
||||||
|
// panel silently showed "No snapshots found" on every install.
|
||||||
let Ok(output) = Command::new("snapper")
|
let Ok(output) = Command::new("snapper")
|
||||||
.args(["list", "--output-cols", "number,date,description"])
|
.args(["list", "--columns", "number,date,description"])
|
||||||
.output()
|
.output()
|
||||||
else {
|
else {
|
||||||
return Vec::new();
|
return Vec::new();
|
||||||
};
|
};
|
||||||
|
if !output.status.success() {
|
||||||
|
eprintln!(
|
||||||
|
"bos-settings: snapper list failed: {}",
|
||||||
|
String::from_utf8_lossy(&output.stderr).trim()
|
||||||
|
);
|
||||||
|
return Vec::new();
|
||||||
|
}
|
||||||
|
|
||||||
let text = String::from_utf8_lossy(&output.stdout);
|
let text = String::from_utf8_lossy(&output.stdout);
|
||||||
text.lines()
|
text.lines()
|
||||||
.skip(2) // header + separator
|
.skip(2) // header + separator
|
||||||
.filter_map(|line| {
|
.filter_map(|line| {
|
||||||
let mut cols = line.splitn(3, '|');
|
let mut cols = line.splitn(3, '|');
|
||||||
|
let number = cols.next()?.trim().to_string();
|
||||||
|
// Snapshot 0 ("current") always exists, can't be rolled back to
|
||||||
|
// or deleted, and isn't a real snapshot — filter it out.
|
||||||
|
if number == "0" {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
Some(SnapshotRow {
|
Some(SnapshotRow {
|
||||||
number: cols.next()?.trim().to_string(),
|
number,
|
||||||
date: cols.next()?.trim().to_string(),
|
date: cols.next()?.trim().to_string(),
|
||||||
description: cols.next()?.trim().to_string(),
|
description: cols.next()?.trim().to_string(),
|
||||||
})
|
})
|
||||||
|
|
@ -89,7 +106,8 @@ pub fn build() -> GBox {
|
||||||
vbox.append(&title);
|
vbox.append(&title);
|
||||||
|
|
||||||
let subtitle = Label::new(Some(
|
let subtitle = Label::new(Some(
|
||||||
"System snapshots created by snap-pac on each pacman transaction.",
|
"System snapshots created by snap-pac on each pacman transaction. \
|
||||||
|
Boot into one from the GRUB menu to recover; delete old ones here.",
|
||||||
));
|
));
|
||||||
subtitle.set_xalign(0.0);
|
subtitle.set_xalign(0.0);
|
||||||
subtitle.set_margin_bottom(16);
|
subtitle.set_margin_bottom(16);
|
||||||
|
|
@ -108,7 +126,7 @@ pub fn build() -> GBox {
|
||||||
btn_row.set_margin_top(12);
|
btn_row.set_margin_top(12);
|
||||||
|
|
||||||
let refresh_btn = Button::with_label("Refresh");
|
let refresh_btn = Button::with_label("Refresh");
|
||||||
let rollback_btn = Button::with_label("Rollback to selected");
|
let rollback_btn = Button::with_label("Boot into 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");
|
||||||
|
|
||||||
|
|
@ -130,23 +148,29 @@ pub fn build() -> GBox {
|
||||||
.root()
|
.root()
|
||||||
.and_then(|r| r.downcast::<gtk4::Window>().ok());
|
.and_then(|r| r.downcast::<gtk4::Window>().ok());
|
||||||
|
|
||||||
|
// BOS boots with root pinned to a named subvolume (grub emits
|
||||||
|
// rootflags=subvol=@), so `snapper rollback`'s usual mechanism —
|
||||||
|
// switching the btrfs *default* subvolume — has no effect here;
|
||||||
|
// grub never consults it. The real, working way to get back to a
|
||||||
|
// snapshot on this layout is grub-btrfs (already installed +
|
||||||
|
// running via grub-btrfsd.service): it generates a GRUB submenu
|
||||||
|
// entry per snapshot, bootable directly. So this button doesn't
|
||||||
|
// touch the filesystem at all — it just points you at that menu.
|
||||||
let dialog = AlertDialog::builder()
|
let dialog = AlertDialog::builder()
|
||||||
.message(&format!("Roll back to snapshot #{number}?"))
|
.message(&format!("Boot into snapshot #{number}?"))
|
||||||
.detail("The current system state will be replaced on next boot. \
|
.detail("Snapshots on BOS are booted directly from the GRUB \
|
||||||
A polkit prompt will ask for your password.")
|
menu (under \"BOS snapshots\"), not rolled back in \
|
||||||
.buttons(["Cancel", "Roll back"])
|
place. Reboot now and pick this snapshot there, or \
|
||||||
|
later if you'd rather keep working — the menu entry \
|
||||||
|
will still be there.")
|
||||||
|
.buttons(["Later", "Reboot now"])
|
||||||
.cancel_button(0)
|
.cancel_button(0)
|
||||||
.default_button(0)
|
.default_button(0)
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
dialog.choose(window.as_ref(), gtk4::gio::Cancellable::NONE, move |result| {
|
dialog.choose(window.as_ref(), gtk4::gio::Cancellable::NONE, move |result| {
|
||||||
if result == Ok(1) {
|
if result == Ok(1) {
|
||||||
// pkexec so polkit handles the privilege escalation
|
let _ = Command::new("systemctl").args(["reboot"]).spawn();
|
||||||
std::thread::spawn(move || {
|
|
||||||
let _ = Command::new("pkexec")
|
|
||||||
.args(["snapper", "rollback", &number])
|
|
||||||
.status();
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
@ -163,7 +187,6 @@ pub fn build() -> GBox {
|
||||||
.root()
|
.root()
|
||||||
.and_then(|r| r.downcast::<gtk4::Window>().ok());
|
.and_then(|r| r.downcast::<gtk4::Window>().ok());
|
||||||
|
|
||||||
let list = list.clone();
|
|
||||||
let dialog = AlertDialog::builder()
|
let dialog = AlertDialog::builder()
|
||||||
.message(&format!("Delete snapshot #{number}?"))
|
.message(&format!("Delete snapshot #{number}?"))
|
||||||
.detail("This cannot be undone.")
|
.detail("This cannot be undone.")
|
||||||
|
|
@ -172,11 +195,43 @@ pub fn build() -> GBox {
|
||||||
.default_button(0)
|
.default_button(0)
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
|
let window2 = window.clone();
|
||||||
|
let list2 = list.clone();
|
||||||
dialog.choose(window.as_ref(), gtk4::gio::Cancellable::NONE, move |result| {
|
dialog.choose(window.as_ref(), gtk4::gio::Cancellable::NONE, move |result| {
|
||||||
if result == Ok(1) {
|
if result != Ok(1) { return }
|
||||||
let _ = Command::new("snapper").args(["delete", &number]).status();
|
|
||||||
populate_list(&list);
|
// snapper's DBus path authorizes via ALLOW_USERS, not pkexec —
|
||||||
}
|
// this only works because post-install.sh seeds that config
|
||||||
|
// key, but if it's ever missing this fails silently unless we
|
||||||
|
// check the exit status. GTK widgets aren't Send, so hand the
|
||||||
|
// outcome back over a channel rather than touching them from
|
||||||
|
// the thread (same pattern as the rollback flow used to).
|
||||||
|
let (tx, rx) = async_channel::bounded::<bool>(1);
|
||||||
|
std::thread::spawn(move || {
|
||||||
|
let ok = Command::new("snapper")
|
||||||
|
.args(["delete", &number])
|
||||||
|
.status()
|
||||||
|
.map(|s| s.success())
|
||||||
|
.unwrap_or(false);
|
||||||
|
let _ = tx.send_blocking(ok);
|
||||||
|
});
|
||||||
|
|
||||||
|
let list = list2.clone();
|
||||||
|
let window = window2.clone();
|
||||||
|
glib::spawn_future_local(async move {
|
||||||
|
let ok = rx.recv().await.unwrap_or(false);
|
||||||
|
if ok {
|
||||||
|
populate_list(&list);
|
||||||
|
} else {
|
||||||
|
let err = AlertDialog::builder()
|
||||||
|
.message("Delete failed")
|
||||||
|
.detail("snapper delete exited with an error — the \
|
||||||
|
snapshot wasn't removed.")
|
||||||
|
.buttons(["OK"])
|
||||||
|
.build();
|
||||||
|
err.choose(window.as_ref(), gtk4::gio::Cancellable::NONE, |_| {});
|
||||||
|
}
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -32,11 +32,13 @@ pub fn build_ui(app: &Application) {
|
||||||
stack.add_named(&views::breadbox::build(), Some("breadbox"));
|
stack.add_named(&views::breadbox::build(), Some("breadbox"));
|
||||||
stack.add_named(&views::breadcrumbs::build(), Some("breadcrumbs"));
|
stack.add_named(&views::breadcrumbs::build(), Some("breadcrumbs"));
|
||||||
stack.add_named(&views::breadpad::build(), Some("breadpad"));
|
stack.add_named(&views::breadpad::build(), Some("breadpad"));
|
||||||
|
stack.add_named(&views::breadpaper::build(), Some("breadpaper"));
|
||||||
stack.add_named(&views::breadsearch::build(), Some("breadsearch"));
|
stack.add_named(&views::breadsearch::build(), Some("breadsearch"));
|
||||||
stack.add_named(&views::hyprland::build(), Some("hyprland"));
|
stack.add_named(&views::hyprland::build(), Some("hyprland"));
|
||||||
|
|
||||||
// Default to snapshots view
|
// Default to the bread panel — Snapshots was previously first, an odd
|
||||||
stack.set_visible_child_name("snapshots");
|
// first impression for a settings app named after the bread ecosystem.
|
||||||
|
stack.set_visible_child_name("bread");
|
||||||
|
|
||||||
{
|
{
|
||||||
let stack = stack.clone();
|
let stack = stack.clone();
|
||||||
|
|
|
||||||
|
|
@ -1 +1,5 @@
|
||||||
bread.activate_profile("default")
|
bread.once("bread.system.startup", function()
|
||||||
|
bread.profile.activate("default")
|
||||||
|
end)
|
||||||
|
|
||||||
|
return bread
|
||||||
|
|
|
||||||
|
|
@ -35,7 +35,12 @@ sequence:
|
||||||
- users
|
- users
|
||||||
- networkcfg
|
- networkcfg
|
||||||
- hwclock
|
- hwclock
|
||||||
- packages
|
# packages module removed: it set update_db:true with no
|
||||||
|
# skip_if_no_internet/ignore_update_db_error, so an offline install (the
|
||||||
|
# exact case bos-welcome's nmtui step exists for) aborted here with a
|
||||||
|
# fatal pacman -Sy failure. Its only try_install packages (pipewire-pulse,
|
||||||
|
# pipewire-alsa) are already in packages.x86_64 and installed by
|
||||||
|
# unpackfs, so the step did nothing useful even when it succeeded.
|
||||||
# archiso strips the kernel from the squashfs; stage it, drop the archiso
|
# archiso strips the kernel from the squashfs; stage it, drop the archiso
|
||||||
# initramfs config, and write a stock mkinitcpio preset before initcpio runs.
|
# initramfs config, and write a stock mkinitcpio preset before initcpio runs.
|
||||||
- shellprocess@kernel
|
- shellprocess@kernel
|
||||||
|
|
|
||||||
26
iso/airootfs/etc/greetd/breadgreet.example.toml
Normal file
26
iso/airootfs/etc/greetd/breadgreet.example.toml
Normal file
|
|
@ -0,0 +1,26 @@
|
||||||
|
# breadgreet commonly runs as the dedicated `greeter` system user (per
|
||||||
|
# greetd's own convention), so it checks /etc/greetd/breadgreet.toml first;
|
||||||
|
# copy this there for a real install. For local dev/testing under a normal
|
||||||
|
# user session it falls back to ~/.config/breadgreet/breadgreet.toml.
|
||||||
|
#
|
||||||
|
# Every field is optional and defaults to the value shown here.
|
||||||
|
|
||||||
|
[background]
|
||||||
|
mode = "color"
|
||||||
|
path = ""
|
||||||
|
blur = false
|
||||||
|
|
||||||
|
[clock]
|
||||||
|
format = "%H:%M"
|
||||||
|
|
||||||
|
[font]
|
||||||
|
family = "Varela Round"
|
||||||
|
|
||||||
|
[sessions]
|
||||||
|
# Directories scanned for .desktop session entries, in order.
|
||||||
|
wayland_dirs = ["/usr/share/wayland-sessions"]
|
||||||
|
xsessions_dirs = ["/usr/share/xsessions"]
|
||||||
|
# .desktop file stem (without extension) to auto-select. Falls back to the
|
||||||
|
# first entry found if this isn't present. v1 has no session picker UI —
|
||||||
|
# BOS only ships one session (Hyprland) today.
|
||||||
|
default = "hyprland"
|
||||||
15
iso/airootfs/etc/greetd/breadgreet.toml
Normal file
15
iso/airootfs/etc/greetd/breadgreet.toml
Normal file
|
|
@ -0,0 +1,15 @@
|
||||||
|
# BOS's real breadgreet config — see breadgreet.example.toml in this same
|
||||||
|
# directory for the full set of options with explanations.
|
||||||
|
#
|
||||||
|
# The one setting that actually matters here: `sessions.default` MUST be
|
||||||
|
# "bos", not breadgreet's compiled-in default of "hyprland". The `hyprland`
|
||||||
|
# pacman package installs its own /usr/share/wayland-sessions/hyprland.desktop
|
||||||
|
# alongside BOS's own bos.desktop, and breadgreet's session picker matches by
|
||||||
|
# .desktop file stem — with no override it picks "hyprland.desktop" over
|
||||||
|
# "bos.desktop", which skips bos-session's PATH fixup (adds ~/.local/bin for
|
||||||
|
# the bakery bread apps; greetd starts no login shell, so /etc/profile.d is
|
||||||
|
# never sourced any other way). Confirmed via breadgreet's own test suite
|
||||||
|
# (sessions.rs: discover_prefers_configured_default_over_first_entry).
|
||||||
|
|
||||||
|
[sessions]
|
||||||
|
default = "bos"
|
||||||
|
|
@ -2,11 +2,15 @@
|
||||||
# squashfs and enabled by post-install.sh; the live ISO instead autologins
|
# squashfs and enabled by post-install.sh; the live ISO instead autologins
|
||||||
# liveuser on tty1 (see getty@tty1 drop-in) so the installer comes straight up.
|
# liveuser on tty1 (see getty@tty1 drop-in) so the installer comes straight up.
|
||||||
#
|
#
|
||||||
# tuigreet shows a minimal greeter, then launches the BOS Hyprland session via
|
# breadgreet (bread-ecosystem's own greeter) is hosted under cage — a single-
|
||||||
# bos-session (which fixes up PATH for the bakery bread apps).
|
# client kiosk Wayland compositor, since greetd itself has no display of its
|
||||||
|
# own — and speaks greetd's IPC directly. It resolves the session to launch
|
||||||
|
# from /usr/share/wayland-sessions/bos.desktop, which points at bos-session
|
||||||
|
# (fixes up PATH for the bakery bread apps; greetd starts no login shell, so
|
||||||
|
# /etc/profile.d is never sourced otherwise).
|
||||||
[terminal]
|
[terminal]
|
||||||
vt = 1
|
vt = 1
|
||||||
|
|
||||||
[default_session]
|
[default_session]
|
||||||
command = "tuigreet --remember --time --cmd /usr/local/bin/bos-session"
|
command = "cage -s -- breadgreet"
|
||||||
user = "greeter"
|
user = "greeter"
|
||||||
|
|
|
||||||
|
|
@ -9,4 +9,3 @@ DOCUMENTATION_URL="https://wiki.archlinux.org/"
|
||||||
SUPPORT_URL="https://bbs.archlinux.org/"
|
SUPPORT_URL="https://bbs.archlinux.org/"
|
||||||
BUG_REPORT_URL="https://git.breadway.dev/Breadway/bos/issues"
|
BUG_REPORT_URL="https://git.breadway.dev/Breadway/bos/issues"
|
||||||
PRIVACY_POLICY_URL="https://terms.archlinux.org/docs/privacy-policy/"
|
PRIVACY_POLICY_URL="https://terms.archlinux.org/docs/privacy-policy/"
|
||||||
LOGO=archlinux-logo
|
|
||||||
|
|
|
||||||
|
|
@ -1,11 +0,0 @@
|
||||||
// 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;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
@ -1 +1,5 @@
|
||||||
bread.activate_profile("default")
|
bread.once("bread.system.startup", function()
|
||||||
|
bread.profile.activate("default")
|
||||||
|
end)
|
||||||
|
|
||||||
|
return bread
|
||||||
|
|
|
||||||
|
|
@ -1,3 +0,0 @@
|
||||||
[[profile]]
|
|
||||||
name = "home"
|
|
||||||
ssids = []
|
|
||||||
|
|
@ -0,0 +1,22 @@
|
||||||
|
# Copy to ~/.config/breadlock/breadlock.toml — every field is optional and
|
||||||
|
# defaults to the value shown here if omitted or the file doesn't exist.
|
||||||
|
|
||||||
|
[background]
|
||||||
|
# "color" (bread-theme palette background) or "image" (a PNG, cover-fit)
|
||||||
|
mode = "color"
|
||||||
|
path = ""
|
||||||
|
# v2 feature — accepted but currently just logs a warning and shows the
|
||||||
|
# background unblurred (needs a wlr-screencopy capture, not implemented yet).
|
||||||
|
blur = false
|
||||||
|
|
||||||
|
[clock]
|
||||||
|
# strftime format
|
||||||
|
format = "%H:%M"
|
||||||
|
|
||||||
|
[font]
|
||||||
|
family = "Varela Round"
|
||||||
|
|
||||||
|
[input]
|
||||||
|
# How long the "wrong password" state (red pill) shows before input
|
||||||
|
# re-enables, in milliseconds.
|
||||||
|
fail_timeout_ms = 800
|
||||||
|
|
@ -3,7 +3,7 @@
|
||||||
# playback, etc. won't dim or suspend the machine). Vendor-neutral: nothing here
|
# playback, etc. won't dim or suspend the machine). Vendor-neutral: nothing here
|
||||||
# is Intel/AMD specific.
|
# is Intel/AMD specific.
|
||||||
general {
|
general {
|
||||||
lock_cmd = pidof hyprlock || hyprlock
|
lock_cmd = pidof breadlock || breadlock
|
||||||
before_sleep_cmd = loginctl lock-session
|
before_sleep_cmd = loginctl lock-session
|
||||||
after_sleep_cmd = hyprctl dispatch dpms on
|
after_sleep_cmd = hyprctl dispatch dpms on
|
||||||
ignore_dbus_inhibit = false
|
ignore_dbus_inhibit = false
|
||||||
|
|
|
||||||
|
|
@ -124,10 +124,15 @@ hl.bind(mod .. " + comma", hl.dsp.exec_cmd("bos-settings"))
|
||||||
hl.bind(mod .. " + slash", hl.dsp.exec_cmd("bos-keybinds"))
|
hl.bind(mod .. " + slash", hl.dsp.exec_cmd("bos-keybinds"))
|
||||||
hl.bind(mod .. " + L", hl.dsp.exec_cmd("loginctl lock-session"))
|
hl.bind(mod .. " + L", hl.dsp.exec_cmd("loginctl lock-session"))
|
||||||
hl.bind(mod .. " + F", hl.dsp.window.fullscreen({ action = "toggle" }))
|
hl.bind(mod .. " + F", hl.dsp.window.fullscreen({ action = "toggle" }))
|
||||||
hl.bind(mod .. " + V", hl.dsp.window.float({ action = "toggle" }))
|
hl.bind(mod .. " + I", hl.dsp.window.float({ action = "toggle" }))
|
||||||
|
hl.bind(mod .. " + P", hl.dsp.window.pseudo({ action = "toggle" }))
|
||||||
|
hl.bind(mod .. " + R", hl.dsp.window.resize())
|
||||||
-- breadclip (its own gtk4-layer-shell popup — not a TUI, so no terminal
|
-- breadclip (its own gtk4-layer-shell popup — not a TUI, so no terminal
|
||||||
-- needed). Previously piped cliphist through fzf directly from the
|
-- needed). Previously piped cliphist through fzf directly from the
|
||||||
-- compositor with no terminal attached, which was a silent no-op.
|
-- compositor with no terminal attached, which was a silent no-op.
|
||||||
|
-- Bound on both V (breadclip's own suggested default, freed up now that
|
||||||
|
-- float toggle moved to I) and SHIFT+V (kept for muscle memory).
|
||||||
|
hl.bind(mod .. " + V", hl.dsp.exec_cmd("breadclip"))
|
||||||
hl.bind(mod .. " + SHIFT + V", hl.dsp.exec_cmd("breadclip"))
|
hl.bind(mod .. " + SHIFT + V", hl.dsp.exec_cmd("breadclip"))
|
||||||
hl.bind(mod .. " + T", hl.dsp.layout("togglesplit"))
|
hl.bind(mod .. " + T", hl.dsp.layout("togglesplit"))
|
||||||
hl.bind(mod .. " + Tab", hl.dsp.focus({ urgent_or_last = true }))
|
hl.bind(mod .. " + Tab", hl.dsp.focus({ urgent_or_last = true }))
|
||||||
|
|
@ -185,6 +190,7 @@ hl.bind("XF86MonBrightnessDown", hl.dsp.exec_cmd("brightnessctl -e4 -n2 set 5%-"
|
||||||
hl.bind("XF86AudioNext", hl.dsp.exec_cmd("playerctl next"), { locked = true })
|
hl.bind("XF86AudioNext", hl.dsp.exec_cmd("playerctl next"), { locked = true })
|
||||||
hl.bind("XF86AudioPrev", hl.dsp.exec_cmd("playerctl previous"), { locked = true })
|
hl.bind("XF86AudioPrev", hl.dsp.exec_cmd("playerctl previous"), { locked = true })
|
||||||
hl.bind("XF86AudioPlay", hl.dsp.exec_cmd("playerctl play-pause"), { locked = true })
|
hl.bind("XF86AudioPlay", hl.dsp.exec_cmd("playerctl play-pause"), { locked = true })
|
||||||
|
hl.bind("XF86Calculator", hl.dsp.exec_cmd("gnome-calculator"))
|
||||||
|
|
||||||
-- ---------------------------------------------------------------------------
|
-- ---------------------------------------------------------------------------
|
||||||
-- Autostart. polkit agent + the bread ecosystem + idle daemon + wallpaper.
|
-- Autostart. polkit agent + the bread ecosystem + idle daemon + wallpaper.
|
||||||
|
|
@ -206,8 +212,12 @@ hl.on("hyprland.start", function()
|
||||||
-- rather than an exec-once here.
|
-- rather than an exec-once here.
|
||||||
"/usr/lib/polkit-gnome/polkit-gnome-authentication-agent-1",
|
"/usr/lib/polkit-gnome/polkit-gnome-authentication-agent-1",
|
||||||
"awww-daemon",
|
"awww-daemon",
|
||||||
-- set the default wallpaper once the daemon is up (retry until ready)
|
-- Set the default wallpaper once the daemon is up (retry until
|
||||||
[[bash -c 'until awww img /usr/share/backgrounds/bos/bread-background.png 2>/dev/null; do sleep 0.3; done']],
|
-- ready) via `breadpaper set`, not raw `awww img` — breadpaper also
|
||||||
|
-- generates the pywal palette and reloads bread-theme, and records
|
||||||
|
-- the path so `breadpaper get` (and its bos-settings panel) show
|
||||||
|
-- the real default instead of "No wallpaper set" on a fresh install.
|
||||||
|
[[bash -c 'until breadpaper set /usr/share/backgrounds/bos/bread-background.png 2>/dev/null; do sleep 0.3; done']],
|
||||||
-- breadd runs as a systemd user service (~/.config/systemd/user/breadd.service,
|
-- breadd runs as a systemd user service (~/.config/systemd/user/breadd.service,
|
||||||
-- enabled in skel). It autostarts at login but before Hyprland exists, so
|
-- enabled in skel). It autostarts at login but before Hyprland exists, so
|
||||||
-- push the compositor's Wayland env into the user manager and restart breadd
|
-- push the compositor's Wayland env into the user manager and restart breadd
|
||||||
|
|
|
||||||
|
|
@ -1,28 +0,0 @@
|
||||||
# Lock screen (hyprlock). Solid dark background (no runtime-generated wallpaper
|
|
||||||
# dependency), accent matched to the Hyprland border colour.
|
|
||||||
general {
|
|
||||||
hide_cursor = true
|
|
||||||
}
|
|
||||||
|
|
||||||
background {
|
|
||||||
monitor =
|
|
||||||
color = rgba(46, 52, 64, 1.0)
|
|
||||||
blur_passes = 0
|
|
||||||
}
|
|
||||||
|
|
||||||
input-field {
|
|
||||||
monitor =
|
|
||||||
size = 20%, 5%
|
|
||||||
outline_thickness = 3
|
|
||||||
inner_color = rgba(0, 0, 0, 0.2)
|
|
||||||
outer_color = rgba(136, 192, 208, 0.8)
|
|
||||||
check_color = rgba(120, 220, 140, 0.95)
|
|
||||||
fail_color = rgba(255, 90, 90, 0.95)
|
|
||||||
font_color = rgba(255, 255, 255, 0.95)
|
|
||||||
fade_on_empty = false
|
|
||||||
rounding = 12
|
|
||||||
placeholder_text = <i>Password…</i>
|
|
||||||
position = 0, -20
|
|
||||||
halign = center
|
|
||||||
valign = center
|
|
||||||
}
|
|
||||||
|
|
@ -0,0 +1,9 @@
|
||||||
|
[Desktop Entry]
|
||||||
|
Name=breadclip
|
||||||
|
Comment=Clipboard history
|
||||||
|
Exec=breadclip
|
||||||
|
Icon=edit-paste
|
||||||
|
Terminal=false
|
||||||
|
Type=Application
|
||||||
|
Categories=Utility;
|
||||||
|
StartupWMClass=breadclip
|
||||||
|
|
@ -0,0 +1,9 @@
|
||||||
|
[Desktop Entry]
|
||||||
|
Name=breadman
|
||||||
|
Comment=Notes and reminders manager
|
||||||
|
Exec=breadman
|
||||||
|
Icon=accessories-text-editor
|
||||||
|
Terminal=false
|
||||||
|
Type=Application
|
||||||
|
Categories=Utility;Office;
|
||||||
|
StartupWMClass=breadman
|
||||||
|
|
@ -0,0 +1,9 @@
|
||||||
|
[Desktop Entry]
|
||||||
|
Name=breadmon
|
||||||
|
Comment=Monitor layout manager (TUI)
|
||||||
|
Exec=breadmon
|
||||||
|
Icon=video-display
|
||||||
|
Terminal=true
|
||||||
|
Type=Application
|
||||||
|
Categories=Settings;System;
|
||||||
|
StartupWMClass=breadmon
|
||||||
|
|
@ -0,0 +1,9 @@
|
||||||
|
[Desktop Entry]
|
||||||
|
Name=breadsearch
|
||||||
|
Comment=Semantic system-wide search
|
||||||
|
Exec=breadsearch
|
||||||
|
Icon=system-search
|
||||||
|
Terminal=false
|
||||||
|
Type=Application
|
||||||
|
Categories=Utility;
|
||||||
|
StartupWMClass=breadsearch
|
||||||
|
|
@ -1,19 +0,0 @@
|
||||||
# Auto-start Hyprland on tty1 in the live session
|
|
||||||
if [[ "$(tty)" == "/dev/tty1" ]] && [[ -z "$WAYLAND_DISPLAY" ]]; then
|
|
||||||
# Allow a software-rendering fallback so the live session comes up even
|
|
||||||
# without a GPU (VMs, headless, exotic hardware). On real hardware wlroots
|
|
||||||
# still selects the hardware renderer; this only permits llvmpipe when no
|
|
||||||
# GPU renderer is available. Must be exported before Hyprland starts —
|
|
||||||
# wlroots reads it at renderer init, earlier than any Hyprland `env=` line.
|
|
||||||
export WLR_RENDERER_ALLOW_SOFTWARE=1
|
|
||||||
# Software cursors: hardware-cursor planes are often unusable in VMs and
|
|
||||||
# show as invisible/garbled; this is the reliable choice for a live medium.
|
|
||||||
export WLR_NO_HARDWARE_CURSORS=1
|
|
||||||
# Run the compositor, capturing its output so a failed live boot is
|
|
||||||
# diagnosable (Hyprland also keeps its own log under $XDG_RUNTIME_DIR/hypr/).
|
|
||||||
# On exit, drop to an interactive shell with the error in view instead of
|
|
||||||
# letting the getty autologin respawn-loop hide it behind a blank cursor.
|
|
||||||
Hyprland &>/var/log/hyprland-live.log
|
|
||||||
echo "Hyprland exited (rc=$?). Log: /var/log/hyprland-live.log"
|
|
||||||
exec bash -i
|
|
||||||
fi
|
|
||||||
|
|
@ -1,28 +0,0 @@
|
||||||
# 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
|
|
||||||
}
|
|
||||||
|
|
@ -18,16 +18,18 @@ if ! id liveuser &>/dev/null; then
|
||||||
passwd -d liveuser >/dev/null
|
passwd -d liveuser >/dev/null
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Layer the installer onto the live desktop: auto-launch it, and bind Super+I to
|
# Layer the installer onto the live desktop: auto-launch it, and bind
|
||||||
# relaunch it after it's been closed. Appended (in Lua) to the skel hyprland.lua
|
# Super+Shift+I to relaunch it after it's been closed. Appended (in Lua) to
|
||||||
# native config so the full desktop stays intact.
|
# the skel hyprland.lua native config so the full desktop stays intact.
|
||||||
|
# NOTE: plain SUPER+I is float-toggle in the skel config — don't reuse it
|
||||||
|
# here, it was tried once and silently double-bound both actions.
|
||||||
HYPR=/home/liveuser/.config/hypr/hyprland.lua
|
HYPR=/home/liveuser/.config/hypr/hyprland.lua
|
||||||
install -d -m 0755 -o liveuser -g liveuser /home/liveuser/.config/hypr
|
install -d -m 0755 -o liveuser -g liveuser /home/liveuser/.config/hypr
|
||||||
if ! grep -q bos-launch-calamares "$HYPR" 2>/dev/null; then
|
if ! grep -q bos-launch-calamares "$HYPR" 2>/dev/null; then
|
||||||
cat >>"$HYPR" <<'EOF'
|
cat >>"$HYPR" <<'EOF'
|
||||||
|
|
||||||
-- --- live-media installer (added by bos-live-setup; absent on installed system) ---
|
-- --- live-media installer (added by bos-live-setup; absent on installed system) ---
|
||||||
hl.bind("SUPER + I", hl.dsp.exec_cmd("bos-launch-calamares"))
|
hl.bind("SUPER + SHIFT + I", hl.dsp.exec_cmd("bos-launch-calamares"))
|
||||||
hl.on("hyprland.start", function() hl.dispatch(hl.dsp.exec_cmd("bos-launch-calamares")) end)
|
hl.on("hyprland.start", function() hl.dispatch(hl.dsp.exec_cmd("bos-launch-calamares")) end)
|
||||||
EOF
|
EOF
|
||||||
fi
|
fi
|
||||||
|
|
|
||||||
|
|
@ -1,24 +1,36 @@
|
||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
# First-run welcome. Shows a short getting-started message once, then drops a
|
# First-run welcome + first-run/every-login network check. Launched from the
|
||||||
# marker so it never shows again. Launched from the Hyprland autostart; the
|
# Hyprland autostart; the bos-welcome window class is floated/centred by a
|
||||||
# bos-welcome window class is floated/centred by a Hyprland window rule.
|
# Hyprland window rule.
|
||||||
set -u
|
set -u
|
||||||
|
|
||||||
# Never run in the live/installer session — only on an installed system.
|
# Never run in the live/installer session — only on an installed system.
|
||||||
[[ "$(id -un)" == "liveuser" ]] && exit 0
|
[[ "$(id -un)" == "liveuser" ]] && exit 0
|
||||||
|
|
||||||
marker="${XDG_CONFIG_HOME:-$HOME/.config}/bos/.welcomed"
|
welcomed_marker="${XDG_CONFIG_HOME:-$HOME/.config}/bos/.welcomed"
|
||||||
[[ -f "$marker" ]] && exit 0
|
mkdir -p "$(dirname "$welcomed_marker")"
|
||||||
mkdir -p "$(dirname "$marker")"
|
|
||||||
|
|
||||||
# First-run network check. A fresh install usually boots with no connection
|
# Network check. A fresh install usually boots with no connection (Wi-Fi
|
||||||
# (Wi-Fi isn't configured during install), and the first `bos-update`/pacman run
|
# isn't configured during install), and the first `bos-update`/pacman run
|
||||||
# then fails with confusing DNS/"could not resolve host" errors. If
|
# then fails with confusing DNS/"could not resolve host" errors. If
|
||||||
# NetworkManager reports we're not fully online, open nmtui so the user can join
|
# NetworkManager reports we're not fully online, open nmtui so the user can
|
||||||
# a network before anything else. Best-effort: missing nmcli/nmtui/kitty, or the
|
# join a network before anything else. This runs on EVERY login, not just
|
||||||
# user quitting nmtui, must never block the welcome below.
|
# the first, and isn't gated by any marker — it keeps re-prompting until the
|
||||||
|
# machine is actually online, then naturally stops (the "full" check below
|
||||||
|
# short-circuits). Best-effort: missing nmcli/nmtui/kitty, or the user
|
||||||
|
# quitting nmtui, must never block the welcome text below.
|
||||||
if command -v nmcli &>/dev/null; then
|
if command -v nmcli &>/dev/null; then
|
||||||
conn="$(nmcli networking connectivity check 2>/dev/null)"
|
conn="$(nmcli networking connectivity check 2>/dev/null)"
|
||||||
|
# NetworkManager may still be associating right at compositor start —
|
||||||
|
# give it a few short retries before concluding we're actually offline,
|
||||||
|
# so a machine with working Wi-Fi doesn't get a spurious nmtui popup.
|
||||||
|
tries=0
|
||||||
|
while [[ "$conn" != "full" && "$tries" -lt 3 ]]; do
|
||||||
|
sleep 1
|
||||||
|
conn="$(nmcli networking connectivity check 2>/dev/null)"
|
||||||
|
tries=$((tries + 1))
|
||||||
|
done
|
||||||
|
|
||||||
if [[ "$conn" != "full" ]]; then
|
if [[ "$conn" != "full" ]]; then
|
||||||
notify-send -u normal "BOS" "No internet yet — opening network setup so updates work." 2>/dev/null || true
|
notify-send -u normal "BOS" "No internet yet — opening network setup so updates work." 2>/dev/null || true
|
||||||
if command -v nmtui &>/dev/null; then
|
if command -v nmtui &>/dev/null; then
|
||||||
|
|
@ -27,8 +39,10 @@ if command -v nmcli &>/dev/null; then
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Mark welcomed only now, so an interrupted/aborted network step still re-prompts
|
# Welcome text: shown once ever, independent of network status above (an
|
||||||
# next login rather than being suppressed forever.
|
# offline machine still gets useful onboarding text — it just also keeps
|
||||||
touch "$marker"
|
# getting the network prompt on future logins until it connects).
|
||||||
|
[[ -f "$welcomed_marker" ]] && exit 0
|
||||||
|
touch "$welcomed_marker"
|
||||||
|
|
||||||
exec kitty --class bos-welcome --title "Welcome to BOS" -- less -R /usr/share/bos/welcome.txt
|
exec kitty --class bos-welcome --title "Welcome to BOS" -- less -R /usr/share/bos/welcome.txt
|
||||||
|
|
|
||||||
|
|
@ -10,14 +10,16 @@
|
||||||
SUPER + E files (nautilus)
|
SUPER + E files (nautilus)
|
||||||
SUPER + B browser (zen)
|
SUPER + B browser (zen)
|
||||||
SUPER + U notes / reminders (breadpad)
|
SUPER + U notes / reminders (breadpad)
|
||||||
SUPER + M package manager (breadman)
|
SUPER + M notes / task manager (breadman)
|
||||||
SUPER + , BOS Settings
|
SUPER + , BOS Settings
|
||||||
SUPER + / this keybind cheatsheet
|
SUPER + / this keybind cheatsheet
|
||||||
SUPER + L lock screen
|
SUPER + L lock screen
|
||||||
SUPER + Backspace close window
|
SUPER + Backspace close window
|
||||||
SUPER + F fullscreen
|
SUPER + F fullscreen
|
||||||
SUPER + V toggle floating
|
SUPER + I toggle floating
|
||||||
SUPER + Shift + V clipboard history
|
SUPER + P toggle pseudotile
|
||||||
|
SUPER + R resize mode
|
||||||
|
SUPER + V / Shift + V clipboard history (breadclip)
|
||||||
SUPER + T toggle split direction
|
SUPER + T toggle split direction
|
||||||
SUPER + Tab last window
|
SUPER + Tab last window
|
||||||
SUPER + N exit Hyprland (log out)
|
SUPER + N exit Hyprland (log out)
|
||||||
|
|
@ -45,6 +47,7 @@
|
||||||
|
|
||||||
MEDIA & HARDWARE KEYS
|
MEDIA & HARDWARE KEYS
|
||||||
volume / brightness / play-pause / next / prev (work on lock screen)
|
volume / brightness / play-pause / next / prev (work on lock screen)
|
||||||
|
calculator key opens gnome-calculator
|
||||||
|
|
||||||
──────────────────────────────────────────────────────────
|
──────────────────────────────────────────────────────────
|
||||||
Press q to close. Configure everything in BOS Settings (SUPER + ,).
|
Press q to close. Configure everything in BOS Settings (SUPER + ,).
|
||||||
|
|
|
||||||
5
iso/airootfs/usr/share/wayland-sessions/bos.desktop
Normal file
5
iso/airootfs/usr/share/wayland-sessions/bos.desktop
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
[Desktop Entry]
|
||||||
|
Name=BOS
|
||||||
|
Comment=Bread OS (Hyprland)
|
||||||
|
Exec=/usr/local/bin/bos-session
|
||||||
|
Type=Application
|
||||||
|
|
@ -11,14 +11,18 @@ amd-ucode
|
||||||
intel-ucode
|
intel-ucode
|
||||||
|
|
||||||
# Power management (vendor-neutral; works on Intel and AMD). tlp auto-tunes power
|
# Power management (vendor-neutral; works on Intel and AMD). tlp auto-tunes power
|
||||||
# by AC/battery and CPU driver; hypridle/hyprlock handle idle dim/off/lock/suspend
|
# by AC/battery and CPU driver; hypridle/breadlock handle idle dim/off/lock/suspend
|
||||||
# and the lock screen; upower exposes battery state. NOT power-profiles-daemon —
|
# and the lock screen; upower exposes battery state. NOT power-profiles-daemon —
|
||||||
# it conflicts with tlp.
|
# it conflicts with tlp.
|
||||||
tlp
|
tlp
|
||||||
tlp-rdw
|
tlp-rdw
|
||||||
upower
|
upower
|
||||||
hypridle
|
hypridle
|
||||||
hyprlock
|
# breadlock replaces hyprlock (locker) and tuigreet (greetd's greeter) — see
|
||||||
|
# /etc/greetd/config.toml and skel's hypridle.conf lock_cmd. cage hosts
|
||||||
|
# breadgreet as a single-client kiosk compositor under greetd.
|
||||||
|
breadlock
|
||||||
|
cage
|
||||||
|
|
||||||
# Bootloader + filesystem
|
# Bootloader + filesystem
|
||||||
grub
|
grub
|
||||||
|
|
@ -56,9 +60,8 @@ xdg-desktop-portal-hyprland
|
||||||
# and Firefox-based apps (Zen). Without it those apps get no file dialog.
|
# and Firefox-based apps (Zen). Without it those apps get no file dialog.
|
||||||
xdg-desktop-portal-gtk
|
xdg-desktop-portal-gtk
|
||||||
# Login manager for the installed system (Wayland-native; enabled by
|
# Login manager for the installed system (Wayland-native; enabled by
|
||||||
# post-install.sh, launches the Hyprland session via tuigreet → bos-session).
|
# post-install.sh, launches the Hyprland session via breadgreet → bos-session).
|
||||||
greetd
|
greetd
|
||||||
greetd-tuigreet
|
|
||||||
xdg-utils
|
xdg-utils
|
||||||
xdg-user-dirs
|
xdg-user-dirs
|
||||||
polkit
|
polkit
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue