From dc765bb526c2921b935f22c5627b2e4cdb5c90e9 Mon Sep 17 00:00:00 2001 From: Breadway Date: Thu, 16 Jul 2026 18:05:49 +0800 Subject: [PATCH] can't be bothered writing a commit message --- README.md | 2 +- packaging/PKGBUILD | 4 +- src/ui/sidebar.rs | 15 +- src/ui/views/breadcrumbs.rs | 7 +- src/ui/views/breadpaper.rs | 250 ++++++++++++++++----- src/ui/views/keybinds.rs | 423 ++++++++++++++++++++++++++++++++++++ src/ui/views/mod.rs | 1 + src/ui/views/network.rs | 6 +- src/ui/widgets.rs | 17 +- src/ui/window.rs | 1 + 10 files changed, 654 insertions(+), 72 deletions(-) create mode 100644 src/ui/views/keybinds.rs diff --git a/README.md b/README.md index 7c0f179..08161fe 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # bos-settings -System settings app for [BOS (Bread Operating System)](https://github.com/Breadway/bos) — GTK4, configures every bread\* app's config plus core system settings (network, sound, power, users, firewall, snapshots, packages, AUR, firmware, Hyprland display/appearance/autostart) non-destructively. +System settings app for [BOS (Bread Operating System)](https://git.breadway.dev/Breadway/bos) — GTK4, configures every bread\* app's config plus core system settings (network, sound, power, users, firewall, snapshots, packages, AUR, firmware, Hyprland display/appearance/autostart) non-destructively. Split out of the `bos` repo into its own repo so a bos-settings release doesn't require a BOS ISO release, and vice versa. Still the only pacman-packaged (not bakery-managed) bread app — see `packaging/README.md`. diff --git a/packaging/PKGBUILD b/packaging/PKGBUILD index 3079663..805df59 100644 --- a/packaging/PKGBUILD +++ b/packaging/PKGBUILD @@ -1,11 +1,11 @@ -# Maintainer: Breadway +# Maintainer: Breadway pkgname=bos-settings pkgver=0.1.0 pkgrel=1 pkgdesc="System settings app for Bread OS" arch=('x86_64') -url="https://github.com/Breadway/bos-settings" +url="https://git.breadway.dev/Breadway/bos-settings" license=('MIT') # Some Rust deps (ring/mlua) build vendored C/asm into static archives; makepkg's # default -flto=auto emits GCC LTO bitcode the Rust (lld) link cannot read, diff --git a/src/ui/sidebar.rs b/src/ui/sidebar.rs index 6261506..b4859a7 100644 --- a/src/ui/sidebar.rs +++ b/src/ui/sidebar.rs @@ -39,6 +39,7 @@ pub const SYSTEM_ITEMS: &[SidebarItem] = &[ item("power", "Power", "battery-good-symbolic"), item("datetime", "Date & Time", "preferences-system-time-symbolic"), item_sub("hyprland", "Display", "monitors.json", "video-display-symbolic"), + item_sub("keybinds", "Keybinds", "binds.json", "input-keyboard-symbolic"), item_sub("autostart", "Startup Apps", "autostart.json", "system-run-symbolic"), item("users", "Users", "system-users-symbolic"), ]; @@ -94,7 +95,19 @@ pub fn build(default_id: &str) -> (GBox, ListBox) { } } - vbox.append(&list); + // Unscrolled, this list's ~30 rows (4 sections' worth of items plus + // headers) force a minimum window height that exceeds the *logical* + // screen height at Hyprland scale factors above 1.0 (e.g. a 1200px-tall + // panel becomes 800 logical px at scale 1.5) — GTK can't shrink below a + // widget's natural size, so the window (and anything below the fold, + // like a panel's Save button) gets clipped by the compositor with no way + // to reach it. Scrolling the nav list independently of the content pane + // lets the window's minimum height drop to whatever a single row needs. + let scroll = gtk4::ScrolledWindow::new(); + scroll.set_vexpand(true); + scroll.set_hscrollbar_policy(gtk4::PolicyType::Never); + scroll.set_child(Some(&list)); + vbox.append(&scroll); (vbox, list) } diff --git a/src/ui/views/breadcrumbs.rs b/src/ui/views/breadcrumbs.rs index a54b946..78318cb 100644 --- a/src/ui/views/breadcrumbs.rs +++ b/src/ui/views/breadcrumbs.rs @@ -11,7 +11,7 @@ use std::rc::Rc; use gtk4::prelude::*; use gtk4::{ - Box as GBox, Button, Entry, Label, ListBox, ListBoxRow, Orientation, Switch, + Box as GBox, Button, Entry, Label, ListBox, ListBoxRow, Orientation, PasswordEntry, Switch, }; use toml_edit::{value, Array, ArrayOfTables, DocumentMut, Item, Table}; @@ -78,11 +78,10 @@ fn rebuild_networks(list: &ListBox, model: &Rc>>) { ssid.set_width_chars(16); ssid.set_placeholder_text(Some("SSID")); - let pass = Entry::new(); + let pass = PasswordEntry::new(); pass.set_text(&n.password); pass.set_hexpand(true); - pass.set_visibility(false); - pass.set_input_purpose(gtk4::InputPurpose::Password); + pass.set_show_peek_icon(true); pass.set_placeholder_text(Some("password")); let hidden = Switch::new(); diff --git a/src/ui/views/breadpaper.rs b/src/ui/views/breadpaper.rs index 8068762..012739a 100644 --- a/src/ui/views/breadpaper.rs +++ b/src/ui/views/breadpaper.rs @@ -5,17 +5,105 @@ //! discoverable home in BOS Settings instead of only being reachable from a //! terminal. -use std::path::PathBuf; +use std::path::{Path, PathBuf}; use std::process::Command; use gtk4::prelude::*; use gtk4::{ - Box as GBox, Button, FileChooserAction, FileChooserDialog, Image, Label, Orientation, - ResponseType, + Box as GBox, Button, FileDialog, FileFilter, FlowBox, Image, Label, Orientation, Picture, }; use crate::ui::widgets as w; +/// Extensions breadpaper's own `validate()` accepts — narrower than what +/// GdkPixbuf can decode (svg/tiff/etc.), so both the file picker's filter and +/// the library scan below stay in sync with what `breadpaper set` will +/// actually take instead of offering images it'll reject. +const WALLPAPER_EXTS: &[&str] = &["png", "jpg", "jpeg", "webp", "gif", "bmp"]; + +/// Caps how many thumbnails the library grid ever builds. Each one decodes a +/// scaled `Pixbuf` synchronously on the main thread (see `thumbnail`) — fine +/// for a bounded, explicitly-triggered scan (same "costs seconds, gated +/// behind a button" posture as network.rs's Wi-Fi scan), not fine as an +/// unbounded walk of someone's whole Pictures folder. +const MAX_LIBRARY_ITEMS: usize = 80; +const MAX_SCAN_DEPTH: usize = 4; + +fn wallpaper_library_dir() -> PathBuf { + let home = std::env::var("HOME").unwrap_or_else(|_| "/root".to_string()); + PathBuf::from(home).join("Pictures/Backgrounds") +} + +fn is_wallpaper_file(path: &Path) -> bool { + path.extension() + .and_then(|e| e.to_str()) + .map(|e| WALLPAPER_EXTS.iter().any(|ext| ext.eq_ignore_ascii_case(e))) + .unwrap_or(false) +} + +/// Recursively collects image paths under `dir` (sorted, depth- and +/// count-bounded) — the library is organized in subfolders (e.g. by show/ +/// series), not a flat directory, so a non-recursive scan would find nothing. +fn scan_wallpapers(dir: &Path) -> Vec { + fn walk(dir: &Path, depth: usize, out: &mut Vec) { + if depth == 0 || out.len() >= MAX_LIBRARY_ITEMS { + return; + } + let Ok(entries) = std::fs::read_dir(dir) else { return }; + let mut entries: Vec<_> = entries.flatten().collect(); + entries.sort_by_key(|e| e.file_name()); + for entry in entries { + if out.len() >= MAX_LIBRARY_ITEMS { + return; + } + let path = entry.path(); + if path.is_dir() { + walk(&path, depth - 1, out); + } else if is_wallpaper_file(&path) { + out.push(path); + } + } + } + let mut out = Vec::new(); + walk(dir, MAX_SCAN_DEPTH, &mut out); + out +} + +/// A clickable thumbnail: scaled-decode preview + filename, wrapped in a +/// plain `Button` so the whole card is the click target. Returns `None` for +/// files GdkPixbuf can't decode (corrupt/unsupported) rather than showing a +/// broken-image placeholder for every miss. +fn thumbnail(path: &Path, on_pick: impl Fn(PathBuf) + 'static) -> Option