diff --git a/Cargo.lock b/Cargo.lock index 5fe87af..75b679c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4,7 +4,7 @@ version = 4 [[package]] name = "HyprBind" -version = "0.1.3" +version = "0.1.4-alpha" dependencies = [ "eframe", "egui", diff --git a/Cargo.toml b/Cargo.toml index 3af390f..ac54ddc 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "HyprBind" -version = "0.1.3" +version = "0.1.4-alpha" edition = "2024" license = "MIT AND OFL-1.1" authors = ["Ry2X <45420571+ry2x@users.noreply.github.com>"] @@ -12,16 +12,16 @@ categories = ["gui", "visualization"] [dependencies] eframe = { version = "0.33.0", default-features = false, features = [ - "glow", - "wayland", - "x11", + "glow", + "wayland", + "x11", ] } image = { version = "0.25", default-features = false, features = ["png"] } egui = { version = "0.33.0", default-features = false } egui_extras = { version = "0.33.0", default-features = false } serde = { version = "1.0", features = ["derive"] } serde_json = { version = "1.0.149", default-features = false, features = [ - "std", + "std", ] } [profile.release] diff --git a/PKGBUILD b/PKGBUILD index ad7805b..07fee0e 100644 --- a/PKGBUILD +++ b/PKGBUILD @@ -2,7 +2,7 @@ pkgname='hyprbind' _pkgname='HyprBind' -pkgver='0.1.3' +pkgver='0.1.4_alpha' pkgrel=1 pkgdesc='A GUI to display Hyprland keybindings' arch=('x86_64' 'aarch64') @@ -15,25 +15,25 @@ source=("$pkgname::git+file://$PWD") sha256sums=('SKIP') prepare() { - cd "$pkgname" - export CARGO_HOME="$srcdir/cargo-home" - cargo fetch --locked --target "$(rustc -vV | sed -n 's/host: //p')" + cd "$pkgname" + export CARGO_HOME="$srcdir/cargo-home" + cargo fetch --locked --target "$(rustc -vV | sed -n 's/host: //p')" } build() { - cd "$pkgname" - export CARGO_HOME="$srcdir/cargo-home" - CARGO_TARGET_DIR=target cargo build --release --locked --offline + cd "$pkgname" + export CARGO_HOME="$srcdir/cargo-home" + CARGO_TARGET_DIR=target cargo build --release --locked --offline } package() { - cd "$pkgname" - - install -Dm755 "target/release/$_pkgname" "$pkgdir/usr/bin/$pkgname" - - install -Dm644 "assets/logo_hyprbind.png" "$pkgdir/usr/share/pixmaps/$pkgname.png" - install -Dm644 "hyprbind.desktop" "$pkgdir/usr/share/applications/$pkgname.desktop" - - install -Dm644 "LICENSE-MIT" "$pkgdir/usr/share/licenses/$pkgname/LICENSE-MIT" - install -Dm644 "LICENSE-OFL" "$pkgdir/usr/share/licenses/$pkgname/LICENSE-OFL-1.1" + cd "$pkgname" + + install -Dm755 "target/release/$_pkgname" "$pkgdir/usr/bin/$pkgname" + + install -Dm644 "assets/logo_hyprbind.png" "$pkgdir/usr/share/pixmaps/$pkgname.png" + install -Dm644 "hyprbind.desktop" "$pkgdir/usr/share/applications/$pkgname.desktop" + + install -Dm644 "LICENSE-MIT" "$pkgdir/usr/share/licenses/$pkgname/LICENSE-MIT" + install -Dm644 "LICENSE-OFL" "$pkgdir/usr/share/licenses/$pkgname/LICENSE-OFL-1.1" } diff --git a/README.md b/README.md index 53c736d..6c6752b 100644 --- a/README.md +++ b/README.md @@ -96,6 +96,26 @@ bind = SUPER, F, exec, thunar # description is empty hyprbind --json ``` +## dmenu output + +- Print keybinds in dmenu-compatible format and exit: + + ```bash + cargo run -- --dmenu + ``` + + - or after installing: + + ```bash + hyprbind --dmenu + ``` + + - Usage with dmenu: + + ```bash + hyprbind --dmenu | dmenu -l 20 + ``` + ## Config - Config file: `$XDG_CONFIG_HOME/hyprbind/config.json` (fallback: `~/.config/hyprbind/config.json`) diff --git a/src/main.rs b/src/main.rs index 366ab28..be688a6 100644 --- a/src/main.rs +++ b/src/main.rs @@ -59,6 +59,18 @@ fn main() -> Result<(), eframe::Error> { } } } + if args.iter().any(|a| a == "--dmenu" || a == "-d") { + match parser::parse_hyprctl_binds() { + Ok(kb) => { + println!("{}", kb.to_dmenu()); + return Ok(()); + } + Err(e) => { + eprintln!("Failed to load keybindings: {}", e); + std::process::exit(1); + } + } + } let icon_data = load_icon(); diff --git a/src/models.rs b/src/models.rs index 19036b0..34328aa 100644 --- a/src/models.rs +++ b/src/models.rs @@ -1,3 +1,4 @@ +use crate::icons::get_icon; use serde::{Deserialize, Serialize}; /// Options for searching keybindings @@ -89,6 +90,38 @@ impl KeyBindings { pub fn to_json(&self) -> Result { serde_json::to_string_pretty(self) } + + /// Export as dmenu-compatible format with NERD FONT icons + pub fn to_dmenu(&self) -> String { + self.entries + .iter() + .map(|entry| { + // Get icon for each modifier and key then combine by + + let keybind = if entry.modifiers.is_empty() { + get_icon(&entry.key) + } else { + let modifiers: Vec<&str> = entry.modifiers.split('+').collect(); + let modifier_icons: Vec = + modifiers.iter().map(|m| get_icon(m.trim())).collect(); + let key_icon = get_icon(&entry.key); + format!("{} + {}", modifier_icons.join(" + "), key_icon) + }; + + // Get display text: description if available, otherwise command + let display_text = if !entry.description.is_empty() { + entry.description.clone() + } else if !entry.command.is_empty() { + entry.command.clone() + } else { + "".to_string() + }; + + // Output line "keybind : display_text" + format!("{} : {}", keybind, display_text) + }) + .collect::>() + .join("\n") + } } impl Default for KeyBindings { @@ -96,3 +129,59 @@ impl Default for KeyBindings { Self::new() } } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_to_dmenu() { + // 1. No modifier, with description + let entry1 = KeyBindEntry::new( + "".to_string(), + "Return".to_string(), + "exec kitty".to_string(), + "Terminal".to_string(), + ); + // 2. With modifiers, with description + let entry2 = KeyBindEntry::new( + "SUPER+SHIFT".to_string(), + "Q".to_string(), + "killactive".to_string(), + "Kill window".to_string(), + ); + // 3. With modifiers, no description + let entry3 = KeyBindEntry::new( + "CTRL+ALT".to_string(), + "F1".to_string(), + "exec firefox".to_string(), + "".to_string(), + ); + // 4. With modifiers, no description, no command + let entry4 = KeyBindEntry::new( + "CTRL+ALT".to_string(), + "F2".to_string(), + "".to_string(), + "".to_string(), + ); + + let kb = KeyBindings { + entries: vec![entry1, entry2, entry3, entry4], + }; + + let dmenu = kb.to_dmenu(); + let lines: Vec<&str> = dmenu.lines().collect(); + + // 1. No modifier, icon only + assert_eq!(lines[0], "󰌑 : Terminal"); + + // 2. Modifiers, icons + assert_eq!(lines[1], " + 󰘶 + Q : Kill window"); + + // 3. Modifiers, fallback to key text if not in icon table + assert_eq!(lines[2], "CTRL + ALT + F1 : exec firefox"); + + // 4. Modifiers, no description, no command + assert_eq!(lines[3], "CTRL + ALT + F2 : "); + } +}