From 4bfcc694bd1d075c3171f7b421a0c909d437606a Mon Sep 17 00:00:00 2001 From: Breadway Date: Sun, 23 Aug 2026 14:39:11 +0800 Subject: [PATCH] Adopt gtk_popup overlay helpers and singleton::toggle_or_kill Replace the TOCTOU pid-file toggle with bread_utils::singleton and the hand-rolled layer-shell overlay/Up-Down/click-outside with gtk_popup, matching breadclip. Screenshot runs still skip the lock. --- Cargo.lock | 2 + breadsearch/Cargo.toml | 2 +- breadsearch/src/main.rs | 157 +++++++++++----------------------------- 3 files changed, 47 insertions(+), 114 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 60a3e79..77a2b16 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -230,6 +230,8 @@ source = "git+https://git.breadway.dev/Breadway/bread-ecosystem?tag=v0.7.2#30517 dependencies = [ "bread-shared", "dirs", + "gtk4", + "gtk4-layer-shell", "serde", "serde_json", ] diff --git a/breadsearch/Cargo.toml b/breadsearch/Cargo.toml index 4b200b7..2c11e28 100644 --- a/breadsearch/Cargo.toml +++ b/breadsearch/Cargo.toml @@ -12,7 +12,7 @@ path = "src/main.rs" breadsearch-shared = { path = "../breadsearch-shared" } bread-theme = { git = "https://git.breadway.dev/Breadway/bread-ecosystem", tag = "v0.7.4", features = ["gtk"] } # Bread event fabric client — emit bread.search.* (fail-silent if breadd is down). -bread-utils = { git = "https://git.breadway.dev/Breadway/bread-ecosystem", tag = "v0.7.2", features = ["bread-client"] } +bread-utils = { git = "https://git.breadway.dev/Breadway/bread-ecosystem", tag = "v0.7.2", features = ["bread-client", "gtk"] } # Capture primitives for `--screenshot` mode — see src/screenshot.rs. bread-screenshots = { git = "https://git.breadway.dev/Breadway/bread-ecosystem", tag = "v0.7.2" } gtk4 = { version = "0.11", features = ["v4_12"] } diff --git a/breadsearch/src/main.rs b/breadsearch/src/main.rs index d4d4786..53302ec 100644 --- a/breadsearch/src/main.rs +++ b/breadsearch/src/main.rs @@ -1,22 +1,12 @@ use bread_theme::{hex_to_rgba, ink_on, load_palette, Palette}; use breadsearch_shared::{Hit, Request, Response}; -use std::{ - cell::RefCell, - env, fs, - path::PathBuf, - process::Command, - rc::Rc, - sync::mpsc, -}; +use std::{cell::RefCell, process::Command, rc::Rc, sync::mpsc}; use gtk4::{ - glib, - pango::EllipsizeMode, - prelude::*, - Application, ApplicationWindow, Box as GBox, CssProvider, EventControllerKey, Image, Label, - ListBox, Orientation, PolicyType, ScrolledWindow, SearchEntry, SelectionMode, + glib, pango::EllipsizeMode, prelude::*, Application, Box as GBox, CssProvider, + EventControllerKey, Image, Label, ListBox, Orientation, PolicyType, ScrolledWindow, + SearchEntry, SelectionMode, }; -use gtk4_layer_shell::{Edge, KeyboardMode, Layer, LayerShell}; mod bread_events; mod listen; @@ -43,47 +33,14 @@ fn build_css(p: &Palette) -> String { .hit-snippet {{ opacity: 0.75; font-size: 11px; font-style: italic; }}\ .hit-score {{ opacity: 0.5; font-size: 11px; }}\ image {{ margin-right: 8px; }}", - bg_panel = bg_panel, - surface = p.color0, - accent = p.color4, - on_bg = ink_on(&p.background), + bg_panel = bg_panel, + surface = p.color0, + accent = p.color4, + on_bg = ink_on(&p.background), on_surface = ink_on(&p.color0), ) } -// ---- PID file toggle -------------------------------------------------------- - -fn pid_file() -> PathBuf { - env::var("XDG_RUNTIME_DIR") - .map(PathBuf::from) - .unwrap_or_else(|_| PathBuf::from("/tmp")) - .join("breadsearch.pid") -} - -fn is_breadsearch_pid(pid: u32) -> bool { - fs::read_to_string(format!("/proc/{}/comm", pid)) - .map(|s| s.trim() == "breadsearch") - .unwrap_or(false) -} - -fn toggle_or_continue() -> bool { - let pf = pid_file(); - if let Ok(content) = fs::read_to_string(&pf) { - if let Ok(pid) = content.trim().parse::() { - if is_breadsearch_pid(pid) { - let _ = Command::new("kill").arg(pid.to_string()).status(); - return false; - } - } - } - let _ = fs::write(&pf, std::process::id().to_string()); - true -} - -fn cleanup_pid() { - let _ = fs::remove_file(pid_file()); -} - // ---- Row builder ------------------------------------------------------------ fn make_hit_row(hit: &Hit) -> gtk4::ListBoxRow { @@ -238,21 +195,13 @@ fn run_ui(screenshot_req: Option) { bread_theme::gtk::apply_user_css(&user_css_path, &user_cell); } - let window = ApplicationWindow::builder().application(app).build(); - window.init_layer_shell(); - window.set_namespace(Some("breadsearch")); - window.set_layer(Layer::Overlay); - window.set_keyboard_mode(KeyboardMode::Exclusive); - for edge in [Edge::Top, Edge::Bottom, Edge::Left, Edge::Right] { - window.set_anchor(edge, true); - } - window.set_exclusive_zone(0); + // Full-screen transparent overlay; panel widget is positioned inside it. + let window = bread_utils::gtk_popup::new_overlay_window(app, "breadsearch"); bread_theme::gtk::bind_window_auto(&window); let close_all: Rc = Rc::new({ let w = window.clone(); move || { - cleanup_pid(); w.close(); } }); @@ -308,7 +257,10 @@ fn run_ui(screenshot_req: Option) { let (tx, rx) = mpsc::sync_channel::>(1); std::thread::spawn(move || { - let req = Request::Query { query: q, limit: 10 }; + let req = Request::Query { + query: q, + limit: 10, + }; let _ = tx.send(breadsearch_shared::send_request(&req)); }); @@ -368,36 +320,11 @@ fn run_ui(screenshot_req: Option) { glib::Propagation::Stop } Key::Down => { - let cur = list_k.selected_row().map(|r| r.index()).unwrap_or(-1); - let mut i = cur + 1; - loop { - match list_k.row_at_index(i) { - Some(r) if r.is_selectable() => { - list_k.select_row(Some(&r)); - break; - } - Some(_) => i += 1, - None => break, - } - } + bread_utils::gtk_popup::select_next_visible(&list_k); glib::Propagation::Stop } Key::Up => { - let cur = list_k.selected_row().map(|r| r.index()).unwrap_or(0); - let mut i = cur - 1; - loop { - if i < 0 { - break; - } - match list_k.row_at_index(i) { - Some(r) if r.is_selectable() => { - list_k.select_row(Some(&r)); - break; - } - Some(_) => i -= 1, - None => break, - } - } + bread_utils::gtk_popup::select_prev_visible(&list_k); glib::Propagation::Stop } _ => glib::Propagation::Proceed, @@ -415,24 +342,10 @@ fn run_ui(screenshot_req: Option) { }); // Click outside launcher panel → close - let close_outside = Rc::clone(&close_all); - let vbox_ref = vbox.clone(); - let win_ref = window.clone(); - let outside_click = gtk4::GestureClick::new(); - outside_click.connect_pressed(move |_, _, x, y| { - if let Some(b) = vbox_ref.compute_bounds(&win_ref) { - if x < b.x() as f64 - || x > (b.x() + b.width()) as f64 - || y < b.y() as f64 - || y > (b.y() + b.height()) as f64 - { - close_outside(); - } - } - }); - window.add_controller(outside_click); - - window.connect_destroy(|_| cleanup_pid()); + { + let close_outside = Rc::clone(&close_all); + bread_utils::gtk_popup::close_on_outside_click(&window, &vbox, move || close_outside()); + } if let Some(req) = screenshot_req.clone() { screenshot::dispatch(&window, req); @@ -465,11 +378,29 @@ fn main() { let cli = screenshot::Cli::parse(); let screenshot_req = cli.screenshot_request(); - // The PID-file toggle kills whatever's holding the file — a real, - // already-running breadsearch instance included. A screenshot run must - // never touch it: it's a separate, disposable instance by design. - if screenshot_req.is_none() && !toggle_or_continue() { - return; - } + // `toggle_or_kill` kills whatever's holding the single-instance lock — + // a real, already-running breadsearch included. A screenshot run must + // never touch it: it's a separate, disposable instance by design (same + // reasoning as breadbar's `allow_multiple_instances`), not a toggle of + // the operator's real search panel. + // + // Kept alive for the rest of `main` — dropping it releases the + // single-instance lock and removes the pid file, which happens + // naturally once `run_ui` returns (after the window closes). + let _singleton_guard = if screenshot_req.is_some() { + None + } else { + match bread_utils::singleton::toggle_or_kill("breadsearch") { + Ok(bread_utils::singleton::Toggle::Started(guard)) => Some(guard), + Ok(bread_utils::singleton::Toggle::KilledExisting) => return, + Err(e) => { + eprintln!( + "breadsearch: single-instance lock unavailable ({e}); continuing without it" + ); + None + } + } + }; + run_ui(screenshot_req); }