Compare commits

..

4 commits
v0.3.2 ... main

Author SHA1 Message Date
Breadway
993dc4a525 Fix cargo clippy warnings across the workspace
All checks were successful
dev release / build (push) Successful in 4m4s
- Derive Default for Config instead of manually mirroring each
  sub-config's Default impl
- Use std::io::Error::other instead of Error::new(ErrorKind::Other, ..)
- Drop the unused mut on the store lock in full_reindex
- Remove Store::dim, a field that was set once and never read
- Use char_indices().enumerate() instead of a hand-rolled loop counter
  in split_by_chars, and is_multiple_of() for the chunk boundary check
- Use strip_prefix instead of manual slicing in expand_home

(cherry picked from commit b740af38e17d04bd83db93bbb0585ed5744a436e)
2026-08-23 14:57:59 +08:00
Breadway
4bfcc694bd Adopt gtk_popup overlay helpers and singleton::toggle_or_kill
Some checks failed
check / check (push) Failing after 1m16s
dev release / build (push) Successful in 3m17s
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.
2026-08-23 14:39:11 +08:00
Breadway
1ce5f72819 Bump version to v0.3.3
All checks were successful
beta (rc) release / build (push) Has been skipped
dev release / build (push) Successful in 6m7s
release / build (push) Successful in 5m34s
2026-08-16 14:09:28 +08:00
Breadway
60d1d81777 Bind the window to the current monitor's bread-theme palette
All checks were successful
dev release / build (push) Successful in 11m24s
Pin bread-theme to v0.7.4.
2026-08-16 13:23:15 +08:00
9 changed files with 323 additions and 421 deletions

546
Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

@ -1,6 +1,6 @@
[package] [package]
name = "breadmill" name = "breadmill"
version = "0.3.1" version = "0.3.3"
edition = "2021" edition = "2021"
license = "MIT" license = "MIT"

View file

@ -81,10 +81,9 @@ fn split_by_chars(chunk: Chunk, max_chars: usize) -> Vec<Chunk> {
let text = &chunk.text; let text = &chunk.text;
let mut result = Vec::new(); let mut result = Vec::new();
let mut seg_start = 0usize; let mut seg_start = 0usize;
let mut count = 0usize;
for (byte_idx, _) in text.char_indices() { for (count, (byte_idx, _)) in text.char_indices().enumerate() {
if count > 0 && count % max_chars == 0 { if count > 0 && count.is_multiple_of(max_chars) {
result.push(Chunk { result.push(Chunk {
text: text[seg_start..byte_idx].to_string(), text: text[seg_start..byte_idx].to_string(),
start: chunk.start + seg_start, start: chunk.start + seg_start,
@ -92,7 +91,6 @@ fn split_by_chars(chunk: Chunk, max_chars: usize) -> Vec<Chunk> {
}); });
seg_start = byte_idx; seg_start = byte_idx;
} }
count += 1;
} }
if seg_start < text.len() { if seg_start < text.len() {
result.push(Chunk { result.push(Chunk {

View file

@ -64,7 +64,7 @@ impl Indexer {
pub fn full_reindex(&self) { pub fn full_reindex(&self) {
eprintln!("breadmill: full reindex triggered"); eprintln!("breadmill: full reindex triggered");
{ {
let mut store = self.state.store.lock_recover(); let store = self.state.store.lock_recover();
// Clear all state // Clear all state
let _ = store.conn.execute_batch("DELETE FROM chunks; DELETE FROM files;"); let _ = store.conn.execute_batch("DELETE FROM chunks; DELETE FROM files;");
let _ = store.index.reserve(4096); let _ = store.index.reserve(4096);
@ -416,9 +416,9 @@ fn sha256_str(bytes: &[u8]) -> String {
} }
pub fn expand_home(path: &str) -> PathBuf { pub fn expand_home(path: &str) -> PathBuf {
if path.starts_with("~/") { if let Some(rest) = path.strip_prefix("~/") {
let home = std::env::var("HOME").unwrap_or_else(|_| "/tmp".into()); let home = std::env::var("HOME").unwrap_or_else(|_| "/tmp".into());
PathBuf::from(home).join(&path[2..]) PathBuf::from(home).join(rest)
} else { } else {
PathBuf::from(path) PathBuf::from(path)
} }

View file

@ -6,7 +6,6 @@ use usearch::{Index, IndexOptions, MetricKind, ScalarKind, new_index};
pub struct Store { pub struct Store {
pub conn: Connection, pub conn: Connection,
pub index: Index, pub index: Index,
pub dim: usize,
} }
// usearch::Index wraps a raw C++ pointer; access is serialized by the Mutex<Store>. // usearch::Index wraps a raw C++ pointer; access is serialized by the Mutex<Store>.
@ -87,7 +86,7 @@ impl Store {
index.reserve(4096).map_err(|e| e.to_string())?; index.reserve(4096).map_err(|e| e.to_string())?;
} }
Ok(Self { conn, index, dim }) Ok(Self { conn, index })
} }
// ---- file state --------------------------------------------------------- // ---- file state ---------------------------------------------------------

View file

@ -1,6 +1,6 @@
[package] [package]
name = "breadsearch-shared" name = "breadsearch-shared"
version = "0.3.1" version = "0.3.3"
edition = "2021" edition = "2021"
license = "MIT" license = "MIT"

View file

@ -43,7 +43,7 @@ pub fn socket_path() -> PathBuf {
// ---- Config ----------------------------------------------------------------- // ---- Config -----------------------------------------------------------------
#[derive(Debug, Clone, Serialize, Deserialize)] #[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct Config { pub struct Config {
#[serde(default)] #[serde(default)]
pub index: IndexConfig, pub index: IndexConfig,
@ -157,16 +157,6 @@ 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)] #[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PowerConfig { pub struct PowerConfig {
@ -255,8 +245,7 @@ pub struct StatusInfo {
pub fn send_request(req: &Request) -> std::io::Result<Response> { pub fn send_request(req: &Request) -> std::io::Result<Response> {
let mut stream = UnixStream::connect(socket_path())?; let mut stream = UnixStream::connect(socket_path())?;
let mut line = serde_json::to_string(req) let mut line = serde_json::to_string(req).map_err(std::io::Error::other)?;
.map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, e))?;
line.push('\n'); line.push('\n');
stream.write_all(line.as_bytes())?; stream.write_all(line.as_bytes())?;
stream.flush()?; stream.flush()?;

View file

@ -1,6 +1,6 @@
[package] [package]
name = "breadsearch" name = "breadsearch"
version = "0.3.1" version = "0.3.3"
edition = "2021" edition = "2021"
license = "MIT" license = "MIT"
@ -10,9 +10,9 @@ path = "src/main.rs"
[dependencies] [dependencies]
breadsearch-shared = { path = "../breadsearch-shared" } breadsearch-shared = { path = "../breadsearch-shared" }
bread-theme = { git = "https://git.breadway.dev/Breadway/bread-ecosystem", tag = "v0.7.2", features = ["gtk"] } 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 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. # Capture primitives for `--screenshot` mode — see src/screenshot.rs.
bread-screenshots = { git = "https://git.breadway.dev/Breadway/bread-ecosystem", tag = "v0.7.2" } bread-screenshots = { git = "https://git.breadway.dev/Breadway/bread-ecosystem", tag = "v0.7.2" }
gtk4 = { version = "0.11", features = ["v4_12"] } gtk4 = { version = "0.11", features = ["v4_12"] }

View file

@ -1,22 +1,12 @@
use bread_theme::{hex_to_rgba, ink_on, load_palette, Palette}; use bread_theme::{hex_to_rgba, ink_on, load_palette, Palette};
use breadsearch_shared::{Hit, Request, Response}; use breadsearch_shared::{Hit, Request, Response};
use std::{ use std::{cell::RefCell, process::Command, rc::Rc, sync::mpsc};
cell::RefCell,
env, fs,
path::PathBuf,
process::Command,
rc::Rc,
sync::mpsc,
};
use gtk4::{ use gtk4::{
glib, glib, pango::EllipsizeMode, prelude::*, Application, Box as GBox, CssProvider,
pango::EllipsizeMode, EventControllerKey, Image, Label, ListBox, Orientation, PolicyType, ScrolledWindow,
prelude::*, SearchEntry, SelectionMode,
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 bread_events;
mod listen; mod listen;
@ -43,47 +33,14 @@ fn build_css(p: &Palette) -> String {
.hit-snippet {{ opacity: 0.75; font-size: 11px; font-style: italic; }}\ .hit-snippet {{ opacity: 0.75; font-size: 11px; font-style: italic; }}\
.hit-score {{ opacity: 0.5; font-size: 11px; }}\ .hit-score {{ opacity: 0.5; font-size: 11px; }}\
image {{ margin-right: 8px; }}", image {{ margin-right: 8px; }}",
bg_panel = bg_panel, bg_panel = bg_panel,
surface = p.color0, surface = p.color0,
accent = p.color4, accent = p.color4,
on_bg = ink_on(&p.background), on_bg = ink_on(&p.background),
on_surface = ink_on(&p.color0), 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::<u32>() {
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 ------------------------------------------------------------ // ---- Row builder ------------------------------------------------------------
fn make_hit_row(hit: &Hit) -> gtk4::ListBoxRow { fn make_hit_row(hit: &Hit) -> gtk4::ListBoxRow {
@ -238,20 +195,13 @@ fn run_ui(screenshot_req: Option<screenshot::ScreenshotRequest>) {
bread_theme::gtk::apply_user_css(&user_css_path, &user_cell); bread_theme::gtk::apply_user_css(&user_css_path, &user_cell);
} }
let window = ApplicationWindow::builder().application(app).build(); // Full-screen transparent overlay; panel widget is positioned inside it.
window.init_layer_shell(); let window = bread_utils::gtk_popup::new_overlay_window(app, "breadsearch");
window.set_namespace(Some("breadsearch")); bread_theme::gtk::bind_window_auto(&window);
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);
let close_all: Rc<dyn Fn()> = Rc::new({ let close_all: Rc<dyn Fn()> = Rc::new({
let w = window.clone(); let w = window.clone();
move || { move || {
cleanup_pid();
w.close(); w.close();
} }
}); });
@ -307,7 +257,10 @@ fn run_ui(screenshot_req: Option<screenshot::ScreenshotRequest>) {
let (tx, rx) = mpsc::sync_channel::<std::io::Result<Response>>(1); let (tx, rx) = mpsc::sync_channel::<std::io::Result<Response>>(1);
std::thread::spawn(move || { 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)); let _ = tx.send(breadsearch_shared::send_request(&req));
}); });
@ -367,36 +320,11 @@ fn run_ui(screenshot_req: Option<screenshot::ScreenshotRequest>) {
glib::Propagation::Stop glib::Propagation::Stop
} }
Key::Down => { Key::Down => {
let cur = list_k.selected_row().map(|r| r.index()).unwrap_or(-1); bread_utils::gtk_popup::select_next_visible(&list_k);
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 glib::Propagation::Stop
} }
Key::Up => { Key::Up => {
let cur = list_k.selected_row().map(|r| r.index()).unwrap_or(0); bread_utils::gtk_popup::select_prev_visible(&list_k);
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::Stop
} }
_ => glib::Propagation::Proceed, _ => glib::Propagation::Proceed,
@ -414,24 +342,10 @@ fn run_ui(screenshot_req: Option<screenshot::ScreenshotRequest>) {
}); });
// Click outside launcher panel → close // Click outside launcher panel → close
let close_outside = Rc::clone(&close_all); {
let vbox_ref = vbox.clone(); let close_outside = Rc::clone(&close_all);
let win_ref = window.clone(); bread_utils::gtk_popup::close_on_outside_click(&window, &vbox, move || close_outside());
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() { if let Some(req) = screenshot_req.clone() {
screenshot::dispatch(&window, req); screenshot::dispatch(&window, req);
@ -464,11 +378,29 @@ fn main() {
let cli = screenshot::Cli::parse(); let cli = screenshot::Cli::parse();
let screenshot_req = cli.screenshot_request(); let screenshot_req = cli.screenshot_request();
// The PID-file toggle kills whatever's holding the file — a real, // `toggle_or_kill` kills whatever's holding the single-instance lock —
// already-running breadsearch instance included. A screenshot run must // a real, already-running breadsearch included. A screenshot run must
// never touch it: it's a separate, disposable instance by design. // never touch it: it's a separate, disposable instance by design (same
if screenshot_req.is_none() && !toggle_or_continue() { // reasoning as breadbar's `allow_multiple_instances`), not a toggle of
return; // 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); run_ui(screenshot_req);
} }