From 20eeb21c2507bbf12a30ffc11ea3548e2a32ef49 Mon Sep 17 00:00:00 2001 From: Ry2X <45420571+ry2x@users.noreply.github.com> Date: Mon, 19 Jan 2026 07:13:01 +0900 Subject: [PATCH 1/9] FEAT: Add dmenu export functionality for keybindings --- src/main.rs | 12 ++++++++++++ src/models.rs | 28 ++++++++++++++++++++++++++++ 2 files changed, 40 insertions(+) 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..7c035f3 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,33 @@ 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| { + 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)).collect(); + let key_icon = get_icon(&entry.key); + format!("{} + {}", modifier_icons.join(" + "), key_icon) + }; + + let display_text = if !entry.description.is_empty() { + entry.description.clone() + } else { + entry.command.clone() + }; + + format!("{} :{}", keybind, display_text) + }) + .collect::>() + .join("\n") + } } impl Default for KeyBindings { From 56e77c9e2a5a99f178587f32e5be88680c69dd1f Mon Sep 17 00:00:00 2001 From: Ry2X <45420571+ry2x@users.noreply.github.com> Date: Mon, 19 Jan 2026 07:13:16 +0900 Subject: [PATCH 2/9] FEAT: Add dmenu output description to README --- README.md | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) 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`) From f73294e972ed1b8b69307e86d2052701d0d9e2a1 Mon Sep 17 00:00:00 2001 From: Ry2X <45420571+ry2x@users.noreply.github.com> Date: Mon, 19 Jan 2026 07:15:44 +0900 Subject: [PATCH 3/9] CHORE: Bump version to 0.1.4 in Cargo files and PKGBUILD --- Cargo.lock | 2 +- Cargo.toml | 2 +- PKGBUILD | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 5fe87af..6dab2d4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4,7 +4,7 @@ version = 4 [[package]] name = "HyprBind" -version = "0.1.3" +version = "0.1.4" dependencies = [ "eframe", "egui", diff --git a/Cargo.toml b/Cargo.toml index 3af390f..e2a6602 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "HyprBind" -version = "0.1.3" +version = "0.1.4" edition = "2024" license = "MIT AND OFL-1.1" authors = ["Ry2X <45420571+ry2x@users.noreply.github.com>"] diff --git a/PKGBUILD b/PKGBUILD index ad7805b..30c70d1 100644 --- a/PKGBUILD +++ b/PKGBUILD @@ -2,7 +2,7 @@ pkgname='hyprbind' _pkgname='HyprBind' -pkgver='0.1.3' +pkgver='0.1.4' pkgrel=1 pkgdesc='A GUI to display Hyprland keybindings' arch=('x86_64' 'aarch64') From f3e2456495041278ff50bba2e6a9e19c67f0be7e Mon Sep 17 00:00:00 2001 From: Ry2X <45420571+ry2x@users.noreply.github.com> Date: Mon, 19 Jan 2026 07:30:09 +0900 Subject: [PATCH 4/9] CHORE: Change version to 0.1.4-alpha in project files I haven't done any tests on this option --- Cargo.lock | 2 +- Cargo.toml | 10 +++++----- PKGBUILD | 32 ++++++++++++++++---------------- 3 files changed, 22 insertions(+), 22 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 6dab2d4..75b679c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4,7 +4,7 @@ version = 4 [[package]] name = "HyprBind" -version = "0.1.4" +version = "0.1.4-alpha" dependencies = [ "eframe", "egui", diff --git a/Cargo.toml b/Cargo.toml index e2a6602..ac54ddc 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "HyprBind" -version = "0.1.4" +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 30c70d1..619a3f0 100644 --- a/PKGBUILD +++ b/PKGBUILD @@ -2,7 +2,7 @@ pkgname='hyprbind' _pkgname='HyprBind' -pkgver='0.1.4' +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" } From b42949907599ab7e3cb401bb0e458cea97f72dcf Mon Sep 17 00:00:00 2001 From: Ry2X <45420571+ry2x@users.noreply.github.com> Date: Mon, 19 Jan 2026 07:32:15 +0900 Subject: [PATCH 5/9] CHORE: Update pkgver format to use underscore --- PKGBUILD | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/PKGBUILD b/PKGBUILD index 619a3f0..07fee0e 100644 --- a/PKGBUILD +++ b/PKGBUILD @@ -2,7 +2,7 @@ pkgname='hyprbind' _pkgname='HyprBind' -pkgver='0.1.4-alpha' +pkgver='0.1.4_alpha' pkgrel=1 pkgdesc='A GUI to display Hyprland keybindings' arch=('x86_64' 'aarch64') From bf49eead340ce1b78cfdd0fab18333a3c64f0961 Mon Sep 17 00:00:00 2001 From: Ry2X <45420571+ry2x@users.noreply.github.com> Date: Mon, 19 Jan 2026 10:05:12 +0900 Subject: [PATCH 6/9] FEAT: Add unit test for dmenu export functionality in KeyBindings --- src/models.rs | 49 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 49 insertions(+) diff --git a/src/models.rs b/src/models.rs index 7c035f3..6417d40 100644 --- a/src/models.rs +++ b/src/models.rs @@ -124,3 +124,52 @@ 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(), + ); + + let kb = KeyBindings { + entries: vec![entry1, entry2, entry3], + }; + + let dmenu = kb.to_dmenu(); + let lines: Vec<&str> = dmenu.lines().collect(); + // 1. No modifier, icon only + assert!(lines[0].contains("󰌑")); // Return icon + assert!(lines[0].contains(":Terminal")); + // 2. Modifiers, icons + assert!(lines[1].contains("")); // SUPER icon + assert!(lines[1].contains("󰘶")); // SHIFT icon + assert!(lines[1].contains(":Kill window")); + // 3. Modifiers, fallback to key text if not in icon table + assert!(lines[2].contains("CTRL")); + assert!(lines[2].contains("ALT")); + assert!(lines[2].contains("F1")); + assert!(lines[2].contains(":exec firefox")); + } +} From 17b33bfe4edbb193667adc5d1533e2b792864edb Mon Sep 17 00:00:00 2001 From: Ry2X <45420571+ry2x@users.noreply.github.com> Date: Mon, 19 Jan 2026 10:12:15 +0900 Subject: [PATCH 7/9] FIX: Trim whitespace from modifier keys when generating dmenu icons https://github.com/ry2x/HyprBind/pull/17#discussion_r2702920842 --- src/models.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/models.rs b/src/models.rs index 6417d40..f9c91ce 100644 --- a/src/models.rs +++ b/src/models.rs @@ -101,7 +101,7 @@ impl KeyBindings { } else { let modifiers: Vec<&str> = entry.modifiers.split('+').collect(); let modifier_icons: Vec = - modifiers.iter().map(|m| get_icon(m)).collect(); + modifiers.iter().map(|m| get_icon(m.trim())).collect(); let key_icon = get_icon(&entry.key); format!("{} + {}", modifier_icons.join(" + "), key_icon) }; From e5bcd263088b315633f6cb2593faf34ac53874ff Mon Sep 17 00:00:00 2001 From: Ry2X <45420571+ry2x@users.noreply.github.com> Date: Mon, 19 Jan 2026 15:47:01 +0900 Subject: [PATCH 8/9] FEAT: Enhance dmenu output formatting and add test case https://github.com/ry2x/HyprBind/pull/17#discussion_r2702943638 https://github.com/ry2x/HyprBind/pull/17#discussion_r2702943655 https://github.com/ry2x/HyprBind/pull/17#discussion_r2702943651 --- src/models.rs | 38 ++++++++++++++++++++++++++------------ 1 file changed, 26 insertions(+), 12 deletions(-) diff --git a/src/models.rs b/src/models.rs index f9c91ce..952ccbe 100644 --- a/src/models.rs +++ b/src/models.rs @@ -96,6 +96,7 @@ impl KeyBindings { 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 { @@ -106,13 +107,17 @@ impl KeyBindings { 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 { + } else if !entry.command.is_empty() { entry.command.clone() + } else { + "".to_string() }; - format!("{} :{}", keybind, display_text) + // Output line "keybind : display_text" + format!("{} : {}", keybind, display_text) }) .collect::>() .join("\n") @@ -152,24 +157,33 @@ mod tests { "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], + entries: vec![entry1, entry2, entry3, entry4], }; let dmenu = kb.to_dmenu(); let lines: Vec<&str> = dmenu.lines().collect(); + + println!("Dmenu Output:\n{}", dmenu); + // 1. No modifier, icon only - assert!(lines[0].contains("󰌑")); // Return icon - assert!(lines[0].contains(":Terminal")); + assert!(lines[0] == "󰌑 : Terminal"); + // 2. Modifiers, icons - assert!(lines[1].contains("")); // SUPER icon - assert!(lines[1].contains("󰘶")); // SHIFT icon - assert!(lines[1].contains(":Kill window")); + assert!(lines[1] == " + 󰘶 + Q : Kill window"); + // 3. Modifiers, fallback to key text if not in icon table - assert!(lines[2].contains("CTRL")); - assert!(lines[2].contains("ALT")); - assert!(lines[2].contains("F1")); - assert!(lines[2].contains(":exec firefox")); + assert!(lines[2] == "CTRL + ALT + F1 : exec firefox"); + + // 4. Modifiers, no description, no command + assert!(lines[3] == "CTRL + ALT + F2 : "); } } From 0f8b3ea5335c0ebd47abc83dc75e4182d7fb6220 Mon Sep 17 00:00:00 2001 From: Ry2X <45420571+ry2x@users.noreply.github.com> Date: Mon, 19 Jan 2026 16:04:12 +0900 Subject: [PATCH 9/9] FIX: Use assert_eq! --- src/models.rs | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/src/models.rs b/src/models.rs index 952ccbe..34328aa 100644 --- a/src/models.rs +++ b/src/models.rs @@ -172,18 +172,16 @@ mod tests { let dmenu = kb.to_dmenu(); let lines: Vec<&str> = dmenu.lines().collect(); - println!("Dmenu Output:\n{}", dmenu); - // 1. No modifier, icon only - assert!(lines[0] == "󰌑 : Terminal"); + assert_eq!(lines[0], "󰌑 : Terminal"); // 2. Modifiers, icons - assert!(lines[1] == " + 󰘶 + Q : Kill window"); + assert_eq!(lines[1], " + 󰘶 + Q : Kill window"); // 3. Modifiers, fallback to key text if not in icon table - assert!(lines[2] == "CTRL + ALT + F1 : exec firefox"); + assert_eq!(lines[2], "CTRL + ALT + F1 : exec firefox"); // 4. Modifiers, no description, no command - assert!(lines[3] == "CTRL + ALT + F2 : "); + assert_eq!(lines[3], "CTRL + ALT + F2 : "); } }