Migrate to bread-utils: Hyprland IPC, single-instance lock, popup scaffold
Replaces three hand-rolled pieces of code with the new shared bread-utils crate (path dependency for now, see the TODO in breadbox/Cargo.toml): - get_active_workspace's raw socket1 client -> bread_utils::hypr - toggle_or_continue's TOCTOU-prone PID-file dance -> bread_utils::singleton (flock-based, no read-then-write race) - the layer-shell window setup, Up/Down visible-row navigation, and click-outside-close gesture -> bread_utils::gtk_popup Builds and tests clean across the whole breadbox workspace.
This commit is contained in:
parent
a6bba2c362
commit
50e5a63528
3 changed files with 31 additions and 113 deletions
12
Cargo.lock
generated
12
Cargo.lock
generated
|
|
@ -37,11 +37,23 @@ dependencies = [
|
||||||
"serde_json",
|
"serde_json",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "bread-utils"
|
||||||
|
version = "0.2.3"
|
||||||
|
dependencies = [
|
||||||
|
"dirs",
|
||||||
|
"gtk4",
|
||||||
|
"gtk4-layer-shell",
|
||||||
|
"serde",
|
||||||
|
"serde_json",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "breadbox"
|
name = "breadbox"
|
||||||
version = "0.2.7"
|
version = "0.2.7"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bread-theme",
|
"bread-theme",
|
||||||
|
"bread-utils",
|
||||||
"breadbox-shared",
|
"breadbox-shared",
|
||||||
"gtk4",
|
"gtk4",
|
||||||
"gtk4-layer-shell",
|
"gtk4-layer-shell",
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,8 @@ path = "src/main.rs"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
bread-theme = { git = "https://github.com/Breadway/bread-ecosystem", tag = "v0.2.10", features = ["gtk"] }
|
bread-theme = { git = "https://github.com/Breadway/bread-ecosystem", tag = "v0.2.10", features = ["gtk"] }
|
||||||
|
# TODO(owner): switch to tag-pinned git dependency once bread-utils is merged and tagged, matching the bread-theme pattern
|
||||||
|
bread-utils = { path = "../../bread-ecosystem-fix-worktree/bread-utils", features = ["gtk"] }
|
||||||
breadbox-shared = { path = "../breadbox-shared" }
|
breadbox-shared = { path = "../breadbox-shared" }
|
||||||
gtk4 = { version = "0.11", features = ["v4_12"] }
|
gtk4 = { version = "0.11", features = ["v4_12"] }
|
||||||
gtk4-layer-shell = "0.8"
|
gtk4-layer-shell = "0.8"
|
||||||
|
|
|
||||||
|
|
@ -4,8 +4,6 @@ use std::{
|
||||||
collections::HashMap,
|
collections::HashMap,
|
||||||
env,
|
env,
|
||||||
fs,
|
fs,
|
||||||
io::{Read, Write},
|
|
||||||
os::unix::net::UnixStream,
|
|
||||||
path::{Path, PathBuf},
|
path::{Path, PathBuf},
|
||||||
process::{Command, Stdio},
|
process::{Command, Stdio},
|
||||||
rc::Rc,
|
rc::Rc,
|
||||||
|
|
@ -18,28 +16,9 @@ use gtk4::{
|
||||||
glib,
|
glib,
|
||||||
pango::EllipsizeMode,
|
pango::EllipsizeMode,
|
||||||
prelude::*,
|
prelude::*,
|
||||||
Application, ApplicationWindow, Box as GBox, CssProvider, EventControllerKey, Label,
|
Application, Box as GBox, CssProvider, EventControllerKey, Label,
|
||||||
ListBox, Orientation, PolicyType, ScrolledWindow, SearchEntry, SelectionMode,
|
ListBox, Orientation, PolicyType, ScrolledWindow, SearchEntry, SelectionMode,
|
||||||
};
|
};
|
||||||
use gtk4_layer_shell::{Edge, KeyboardMode, Layer, LayerShell};
|
|
||||||
|
|
||||||
// ---- Hyprland IPC -----------------------------------------------------------
|
|
||||||
|
|
||||||
fn get_active_workspace() -> Option<String> {
|
|
||||||
let sig = env::var("HYPRLAND_INSTANCE_SIGNATURE").ok()?;
|
|
||||||
let rt = env::var("XDG_RUNTIME_DIR").ok()?;
|
|
||||||
let socket_path = format!("{}/hypr/{}/.socket.sock", rt, sig);
|
|
||||||
|
|
||||||
let mut stream = UnixStream::connect(&socket_path).ok()?;
|
|
||||||
stream.write_all(b"j/activeworkspace").ok()?;
|
|
||||||
stream.shutdown(std::net::Shutdown::Write).ok()?;
|
|
||||||
|
|
||||||
let mut response = String::new();
|
|
||||||
stream.read_to_string(&mut response).ok()?;
|
|
||||||
|
|
||||||
let v: serde_json::Value = serde_json::from_str(&response).ok()?;
|
|
||||||
v["name"].as_str().map(|s| s.to_string())
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---- Manifest ---------------------------------------------------------------
|
// ---- Manifest ---------------------------------------------------------------
|
||||||
|
|
||||||
|
|
@ -288,40 +267,6 @@ fn match_tier(query: &str, entry: &DesktopEntry) -> Option<u32> {
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---- PID file toggle --------------------------------------------------------
|
|
||||||
|
|
||||||
fn pid_file() -> PathBuf {
|
|
||||||
env::var("XDG_RUNTIME_DIR")
|
|
||||||
.map(PathBuf::from)
|
|
||||||
.unwrap_or_else(|_| PathBuf::from("/tmp"))
|
|
||||||
.join("breadbox.pid")
|
|
||||||
}
|
|
||||||
|
|
||||||
fn is_breadbox_pid(pid: u32) -> bool {
|
|
||||||
fs::read_to_string(format!("/proc/{}/comm", pid))
|
|
||||||
.map(|s| s.trim() == "breadbox")
|
|
||||||
.unwrap_or(false)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Returns false if an existing instance was killed (caller should exit).
|
|
||||||
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_breadbox_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());
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---- UI ---------------------------------------------------------------------
|
// ---- UI ---------------------------------------------------------------------
|
||||||
|
|
||||||
fn get_row_entry(row: >k4::ListBoxRow) -> Option<DesktopEntry> {
|
fn get_row_entry(row: >k4::ListBoxRow) -> Option<DesktopEntry> {
|
||||||
|
|
@ -354,20 +299,11 @@ fn run_ui(entries: Vec<DesktopEntry>, history: LaunchHistory) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Full-screen transparent window; clicks outside the launcher panel close it.
|
// Full-screen transparent window; clicks outside the launcher panel close it.
|
||||||
let window = ApplicationWindow::builder().application(app).build();
|
let window = bread_utils::gtk_popup::new_overlay_window(app, "breadbox");
|
||||||
window.init_layer_shell();
|
|
||||||
window.set_namespace(Some("breadbox"));
|
|
||||||
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();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
@ -513,36 +449,11 @@ fn run_ui(entries: Vec<DesktopEntry>, history: LaunchHistory) {
|
||||||
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_visible() => {
|
|
||||||
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_visible() => {
|
|
||||||
list_k.select_row(Some(&r));
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
Some(_) => i -= 1,
|
|
||||||
None => break,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
glib::Propagation::Stop
|
glib::Propagation::Stop
|
||||||
}
|
}
|
||||||
_ => glib::Propagation::Proceed,
|
_ => glib::Propagation::Proceed,
|
||||||
|
|
@ -564,23 +475,8 @@ fn run_ui(entries: Vec<DesktopEntry>, history: LaunchHistory) {
|
||||||
|
|
||||||
// Click outside launcher panel → close
|
// Click outside launcher panel → close
|
||||||
let close_outside = Rc::clone(&close_all);
|
let close_outside = Rc::clone(&close_all);
|
||||||
let vbox_ref = vbox.clone();
|
bread_utils::gtk_popup::close_on_outside_click(&window, &vbox, move || close_outside());
|
||||||
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());
|
|
||||||
window.present();
|
window.present();
|
||||||
search.grab_focus();
|
search.grab_focus();
|
||||||
});
|
});
|
||||||
|
|
@ -591,12 +487,20 @@ fn run_ui(entries: Vec<DesktopEntry>, history: LaunchHistory) {
|
||||||
// ---- Main -------------------------------------------------------------------
|
// ---- Main -------------------------------------------------------------------
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
if !toggle_or_continue() {
|
// Kept alive for the rest of `main` — dropping it releases the
|
||||||
return;
|
// single-instance lock and removes the pid file, which happens
|
||||||
|
// naturally once `run_ui` returns (after the window closes).
|
||||||
|
let _singleton_guard = match bread_utils::singleton::toggle_or_kill("breadbox") {
|
||||||
|
Ok(bread_utils::singleton::Toggle::Started(guard)) => Some(guard),
|
||||||
|
Ok(bread_utils::singleton::Toggle::KilledExisting) => return,
|
||||||
|
Err(e) => {
|
||||||
|
eprintln!("breadbox: single-instance lock unavailable ({e}); continuing without it");
|
||||||
|
None
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
|
||||||
let config = Config::load();
|
let config = Config::load();
|
||||||
let workspace = get_active_workspace().unwrap_or_default();
|
let workspace = bread_utils::hypr::active_workspace_name().unwrap_or_default();
|
||||||
let priority = config
|
let priority = config
|
||||||
.context_for(&workspace)
|
.context_for(&workspace)
|
||||||
.map(|c| c.priority.clone())
|
.map(|c| c.priority.clone())
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue