From 3494b1329f0e2d32eb8310ae6ee396ab833a7281 Mon Sep 17 00:00:00 2001 From: Jan Vlcinsky Date: Sun, 21 Jun 2026 22:45:42 +0200 Subject: [PATCH 1/4] Fix Ctrl+hjkl navigation for herdr 0.7.0 - Fix pane ID format: use public_pane_numbers to generate correct IDs (wB:p3 format) instead of deprecated wB-1 format - Add focus_pane_direction() using new pane.focus_direction API (herdr 0.7.0+) - Update focus_adjacent() to try new API first, fall back to computed approach for backward compatibility with older herdr versions Fixes #1 --- src/main.rs | 35 +++++++++++++++++++++++++++++++---- 1 file changed, 31 insertions(+), 4 deletions(-) diff --git a/src/main.rs b/src/main.rs index 10d59b8..d6f0c83 100644 --- a/src/main.rs +++ b/src/main.rs @@ -205,16 +205,21 @@ fn active_workspace_and_tab() -> Result<(Value, Value), String> { Ok((workspace, tab)) } -fn pane_public_mapping(workspace: &Value, tab: &Value) -> Result, String> { +fn pane_public_mapping(workspace: &Value, _tab: &Value) -> Result, String> { let workspace_id = workspace .get("id") .and_then(Value::as_str) .ok_or_else(|| "active Herdr workspace has no id".to_string())?; - let panes = tab + let panes = _tab .get("panes") .and_then(Value::as_object) .ok_or_else(|| "active Herdr tab has no panes object".to_string())?; + let public_pane_numbers = workspace + .get("public_pane_numbers") + .and_then(Value::as_object) + .ok_or_else(|| "active Herdr workspace has no public_pane_numbers".to_string())?; + let mut internal_ids = panes .keys() .filter_map(|pane_id| pane_id.parse::().ok()) @@ -223,8 +228,13 @@ fn pane_public_mapping(workspace: &Value, tab: &Value) -> Result Result<(), String> { + result( + "pane.focus_direction", + json!({ + "pane_id": pane_id, + "direction": direction, + }), + )?; + Ok(()) +} + fn focus_pane(pane_id: &str) -> Result<(), String> { let seq = now_ns(); let normalized = pane_id.replace(['-', ':'], "_"); @@ -578,6 +599,12 @@ fn focus_adjacent(direction: &str, current_pane_id: Option) -> Result Date: Sun, 21 Jun 2026 22:59:01 +0200 Subject: [PATCH 2/4] Fallback to herdr pane current when env vars missing When HERDR_ACTIVE_PANE_ID or HERDR_PANE_ID are not set in the environment, query herdr's pane.current API to get the current pane ID instead of returning nil. This ensures pane registration works even when herdr doesn't inject the environment variables. --- lua/herdr/navigator.lua | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/lua/herdr/navigator.lua b/lua/herdr/navigator.lua index 6f05866..b57aad5 100644 --- a/lua/herdr/navigator.lua +++ b/lua/herdr/navigator.lua @@ -104,7 +104,21 @@ local function run_helper(args) end local function pane_id() - return vim.env.HERDR_ACTIVE_PANE_ID or vim.env.HERDR_PANE_ID + local id = vim.env.HERDR_ACTIVE_PANE_ID or vim.env.HERDR_PANE_ID + if id and id ~= "" then + return id + end + + local handle = io.popen("herdr pane current 2>/dev/null") + if handle then + local result = handle:read("*a") + handle:close() + local json = vim.json and vim.json.decode(result) or vim.fn.json_decode(result) + if json and json.result and json.result.pane then + return json.result.pane.pane_id + end + end + return nil end local function nvim_panes_dir() From 98a2fec4721526dbf79feed34d4304548a16c654 Mon Sep 17 00:00:00 2001 From: Jan Vlcinsky Date: Sun, 21 Jun 2026 23:27:01 +0200 Subject: [PATCH 3/4] Use pane.send_keys with key-combo strings for herdr 0.7.0 Herdr 0.7.0 (#613) added support for key-combo strings like 'ctrl+h' in pane.send_keys. Switch from pane.send_text with raw control bytes to pane.send_keys with key-combo strings for better compatibility and to avoid keyboard protocol re-encoding issues. --- src/main.rs | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/src/main.rs b/src/main.rs index d6f0c83..49abb07 100644 --- a/src/main.rs +++ b/src/main.rs @@ -673,25 +673,25 @@ fn pane_is_registered_nvim(pane_id: &str) -> Result { Ok(dir.join(&public_pane_id).exists()) } -fn ctrl_text(direction: &str) -> Option<&'static str> { +fn ctrl_key(direction: &str) -> Option<&'static str> { match direction { - "left" => Some("\u{0008}"), - "down" => Some("\u{000a}"), - "up" => Some("\u{000b}"), - "right" => Some("\u{000c}"), + "left" => Some("ctrl+h"), + "down" => Some("ctrl+j"), + "up" => Some("ctrl+k"), + "right" => Some("ctrl+l"), _ => None, } } fn send_ctrl_to_pane(pane_id: &str, direction: &str) -> Result<(), String> { - let Some(text) = ctrl_text(direction) else { + let Some(key) = ctrl_key(direction) else { return Err(format!("unsupported direction {direction}")); }; result( - "pane.send_text", + "pane.send_keys", json!({ "pane_id": pane_id, - "text": text, + "keys": [key], }), )?; Ok(()) @@ -852,13 +852,13 @@ fn run() -> Result { match (command, direction) { ("register", _) => register(), ("release", _) => release(), - ("focus", Some(direction)) if ctrl_text(direction).is_some() => { + ("focus", Some(direction)) if ctrl_key(direction).is_some() => { focus_adjacent(direction, None) } ("split", Some(direction)) if matches!(direction, "right" | "down") => { split_pane(direction) } - ("dispatch", Some(direction)) if ctrl_text(direction).is_some() => dispatch(direction), + ("dispatch", Some(direction)) if ctrl_key(direction).is_some() => dispatch(direction), _ => { usage(); Ok(2) From 0cdb26a315c54620ce1ee08b366da59e1d755256 Mon Sep 17 00:00:00 2001 From: Jan Vlcinsky Date: Mon, 22 Jun 2026 00:14:10 +0200 Subject: [PATCH 4/4] docs: add LazyVim/distro compatibility configuration Document the keybinding conflict with LazyVim and similar distributions that set Ctrl+h/j/k/l after plugin setup. Provide a working configuration that ensures herdr.nvim keymaps take precedence by setting them after VimEnter with a delay. --- README.md | 37 +++++++++++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/README.md b/README.md index 2688eba..a10a4f4 100644 --- a/README.md +++ b/README.md @@ -60,12 +60,49 @@ return { ## Lazy.nvim +### Basic setup + +```lua +{ + "devxplay/herdr.nvim", +} +``` + +### LazyVim / NvChad / Distro compatibility + +LazyVim and similar Neovim distributions set default `Ctrl+h/j/k/l` keymaps for window navigation that load after plugins. To ensure herdr.nvim's keymaps take precedence, use this configuration: + ```lua { "devxplay/herdr.nvim", + lazy = false, + priority = 1000, + config = function() + require("herdr").setup({ + set_keymaps = false, -- We'll set them after LazyVim loads + }) + + -- Set keymaps after LazyVim's defaults + vim.api.nvim_create_autocmd("VimEnter", { + callback = function() + vim.defer_fn(function() + vim.keymap.set("n", "", function() require("herdr.navigator").navigate("left") end, { desc = "Navigate left" }) + vim.keymap.set("n", "", function() require("herdr.navigator").navigate("down") end, { desc = "Navigate down" }) + vim.keymap.set("n", "", function() require("herdr.navigator").navigate("up") end, { desc = "Navigate up" }) + vim.keymap.set("n", "", function() require("herdr.navigator").navigate("right") end, { desc = "Navigate right" }) + end, 100) + end, + }) + end, } ``` +This configuration: +- Loads the plugin immediately (`lazy = false`) +- Loads early with high priority (`priority = 1000`) +- Waits for Neovim to finish starting, then sets the keymaps after a short delay +- Ensures herdr.nvim's navigation keymaps override the distribution's defaults + ## Herdr config Install `herdr-navigator` somewhere on your `PATH` for Herdr shell keybindings: