Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

10 changes: 5 additions & 5 deletions Cargo.toml
Original file line number Diff line number Diff line change
@@ -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>"]
Expand All @@ -12,16 +12,16 @@ categories = ["gui", "visualization"]

[dependencies]
eframe = { version = "0.33.0", default-features = false, features = [
"glow",
"wayland",
"x11",
"glow",
"wayland",
"x11",
] }
Comment thread
ry2x marked this conversation as resolved.
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",
] }
Comment thread
ry2x marked this conversation as resolved.

[profile.release]
Expand Down
32 changes: 16 additions & 16 deletions PKGBUILD
Original file line number Diff line number Diff line change
Expand Up @@ -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')
Expand All @@ -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')"
Comment thread
ry2x marked this conversation as resolved.
}

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
Comment thread
ry2x marked this conversation as resolved.
}

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"
Comment thread
ry2x marked this conversation as resolved.
}
20 changes: 20 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`)
Expand Down
12 changes: 12 additions & 0 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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();

Expand Down
89 changes: 89 additions & 0 deletions src/models.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
use crate::icons::get_icon;
use serde::{Deserialize, Serialize};

/// Options for searching keybindings
Expand Down Expand Up @@ -89,10 +90,98 @@ impl KeyBindings {
pub fn to_json(&self) -> Result<String, serde_json::Error> {
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<String> =
modifiers.iter().map(|m| get_icon(m.trim())).collect();
let key_icon = get_icon(&entry.key);
Comment thread
ry2x marked this conversation as resolved.
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)
Comment thread
ry2x marked this conversation as resolved.
})
.collect::<Vec<String>>()
.join("\n")
}
}

impl Default for KeyBindings {
fn default() -> Self {
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
Comment thread
ry2x marked this conversation as resolved.
assert_eq!(lines[2], "CTRL + ALT + F1 : exec firefox");

// 4. Modifiers, no description, no command
assert_eq!(lines[3], "CTRL + ALT + F2 : ");
}
}