diff --git a/Cargo.lock b/Cargo.lock index 77a2b16..60a3e79 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -230,8 +230,6 @@ 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/breadmill/src/chunk.rs b/breadmill/src/chunk.rs index 7468f89..dcddca8 100644 --- a/breadmill/src/chunk.rs +++ b/breadmill/src/chunk.rs @@ -81,9 +81,10 @@ fn split_by_chars(chunk: Chunk, max_chars: usize) -> Vec { let text = &chunk.text; let mut result = Vec::new(); let mut seg_start = 0usize; + let mut count = 0usize; - for (count, (byte_idx, _)) in text.char_indices().enumerate() { - if count > 0 && count.is_multiple_of(max_chars) { + for (byte_idx, _) in text.char_indices() { + if count > 0 && count % max_chars == 0 { result.push(Chunk { text: text[seg_start..byte_idx].to_string(), start: chunk.start + seg_start, @@ -91,6 +92,7 @@ fn split_by_chars(chunk: Chunk, max_chars: usize) -> Vec { }); seg_start = byte_idx; } + count += 1; } if seg_start < text.len() { result.push(Chunk { diff --git a/breadmill/src/indexer.rs b/breadmill/src/indexer.rs index 67c7694..539b58c 100644 --- a/breadmill/src/indexer.rs +++ b/breadmill/src/indexer.rs @@ -64,7 +64,7 @@ impl Indexer { pub fn full_reindex(&self) { eprintln!("breadmill: full reindex triggered"); { - let store = self.state.store.lock_recover(); + let mut store = self.state.store.lock_recover(); // Clear all state let _ = store.conn.execute_batch("DELETE FROM chunks; DELETE FROM files;"); let _ = store.index.reserve(4096); @@ -416,9 +416,9 @@ fn sha256_str(bytes: &[u8]) -> String { } pub fn expand_home(path: &str) -> PathBuf { - if let Some(rest) = path.strip_prefix("~/") { + if path.starts_with("~/") { let home = std::env::var("HOME").unwrap_or_else(|_| "/tmp".into()); - PathBuf::from(home).join(rest) + PathBuf::from(home).join(&path[2..]) } else { PathBuf::from(path) } diff --git a/breadmill/src/store.rs b/breadmill/src/store.rs index 106e4c5..ed1392a 100644 --- a/breadmill/src/store.rs +++ b/breadmill/src/store.rs @@ -6,6 +6,7 @@ use usearch::{Index, IndexOptions, MetricKind, ScalarKind, new_index}; pub struct Store { pub conn: Connection, pub index: Index, + pub dim: usize, } // usearch::Index wraps a raw C++ pointer; access is serialized by the Mutex. @@ -86,7 +87,7 @@ impl Store { index.reserve(4096).map_err(|e| e.to_string())?; } - Ok(Self { conn, index }) + Ok(Self { conn, index, dim }) } // ---- file state --------------------------------------------------------- diff --git a/breadsearch-shared/src/lib.rs b/breadsearch-shared/src/lib.rs index 5ed69c4..fdea6f5 100644 --- a/breadsearch-shared/src/lib.rs +++ b/breadsearch-shared/src/lib.rs @@ -43,7 +43,7 @@ pub fn socket_path() -> PathBuf { // ---- Config ----------------------------------------------------------------- -#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[derive(Debug, Clone, Serialize, Deserialize)] pub struct Config { #[serde(default)] pub index: IndexConfig, @@ -157,6 +157,16 @@ impl Default for ModelConfig { } } +impl Default for Config { + fn default() -> Self { + Self { + index: IndexConfig::default(), + search: SearchConfig::default(), + model: ModelConfig::default(), + power: PowerConfig::default(), + } + } +} #[derive(Debug, Clone, Serialize, Deserialize)] pub struct PowerConfig { @@ -245,7 +255,8 @@ pub struct StatusInfo { pub fn send_request(req: &Request) -> std::io::Result { let mut stream = UnixStream::connect(socket_path())?; - let mut line = serde_json::to_string(req).map_err(std::io::Error::other)?; + let mut line = serde_json::to_string(req) + .map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, e))?; line.push('\n'); stream.write_all(line.as_bytes())?; stream.flush()?; diff --git a/breadsearch/Cargo.toml b/breadsearch/Cargo.toml index 2c11e28..4b200b7 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", "gtk"] } +bread-utils = { git = "https://git.breadway.dev/Breadway/bread-ecosystem", tag = "v0.7.2", features = ["bread-client"] } # 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 53302ec..d4d4786 100644 --- a/breadsearch/src/main.rs +++ b/breadsearch/src/main.rs @@ -1,12 +1,22 @@ use bread_theme::{hex_to_rgba, ink_on, load_palette, Palette}; use breadsearch_shared::{Hit, Request, Response}; -use std::{cell::RefCell, process::Command, rc::Rc, sync::mpsc}; +use std::{ + cell::RefCell, + env, fs, + path::PathBuf, + process::Command, + rc::Rc, + sync::mpsc, +}; use gtk4::{ - glib, pango::EllipsizeMode, prelude::*, Application, Box as GBox, CssProvider, - EventControllerKey, Image, Label, ListBox, Orientation, PolicyType, ScrolledWindow, - SearchEntry, SelectionMode, + glib, + pango::EllipsizeMode, + prelude::*, + Application, ApplicationWindow, 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; @@ -33,14 +43,47 @@ 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 { @@ -195,13 +238,21 @@ fn run_ui(screenshot_req: Option) { bread_theme::gtk::apply_user_css(&user_css_path, &user_cell); } - // Full-screen transparent overlay; panel widget is positioned inside it. - let window = bread_utils::gtk_popup::new_overlay_window(app, "breadsearch"); + 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); bread_theme::gtk::bind_window_auto(&window); let close_all: Rc = Rc::new({ let w = window.clone(); move || { + cleanup_pid(); w.close(); } }); @@ -257,10 +308,7 @@ 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)); }); @@ -320,11 +368,36 @@ fn run_ui(screenshot_req: Option) { glib::Propagation::Stop } Key::Down => { - bread_utils::gtk_popup::select_next_visible(&list_k); + 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, + } + } glib::Propagation::Stop } Key::Up => { - bread_utils::gtk_popup::select_prev_visible(&list_k); + 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, + } + } glib::Propagation::Stop } _ => glib::Propagation::Proceed, @@ -342,10 +415,24 @@ fn run_ui(screenshot_req: Option) { }); // Click outside launcher panel → close - { - let close_outside = Rc::clone(&close_all); - bread_utils::gtk_popup::close_on_outside_click(&window, &vbox, move || close_outside()); - } + 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()); if let Some(req) = screenshot_req.clone() { screenshot::dispatch(&window, req); @@ -378,29 +465,11 @@ fn main() { let cli = screenshot::Cli::parse(); let screenshot_req = cli.screenshot_request(); - // `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 - } - } - }; - + // 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; + } run_ui(screenshot_req); }