Seamless navigation between Neovim and Ghostty splits using Ctrl+hjkl. Inspired by vim-tmux-navigator.
demo.mp4
Using lazy.nvim:
{
"tmm/ghostty-navigator.nvim",
build = "make",
opts = {},
}The build step compiles a Swift daemon, installs a LaunchAgent, and starts the daemon automatically. (For other plugin managers, clone the repo into your Neovim packages and run make manually.)
On first install, grant Accessibility permissions to the daemon binary in System Settings > Privacy & Security > Accessibility, then restart it:
launchctl stop dev.tmm.ghostty-navigator && launchctl start dev.tmm.ghostty-navigator- Neovim >= 0.9
- Ghostty >= 1.3.0 (macOS only, requires AppleScript support)
- Accessibility permissions for the background daemon (System Settings > Privacy & Security > Accessibility)
A Swift daemon intercepts Ctrl+hjkl at the OS level via a CGEventTap when Ghostty is the focused application. The Neovim plugin coordinates with the daemon using a flag file (/tmp/ghostty-nvim-active).
Press Ctrl+h (Ghostty is focused)
└─ Swift daemon intercepts key
├─ nvim is active (flag file exists) → pass key through
│ └─ Neovim handles it
│ ├─ Another nvim split exists → wincmd navigates
│ ├─ A keymap handles it (popup, etc.) → keymap runs
│ └─ At edge, no keymap → osascript goto_split → Ghostty moves focus
└─ shell is active (no flag file) → goto_split → Ghostty moves focus
Navigation works in normal mode and terminal mode (:terminal buffers).
Note
Ghostty's keybindings can trigger actions like goto_split, but can't conditionally execute based on the process running in the focused terminal. The AppleScript API is only available for external automation. The daemon bridges this gap by intercepting Ctrl+hjkl via a CGEventTap and deciding whether to pass the key through to Neovim or call goto_split via AppleScript.
Default options:
{
-- Keymaps to set up. Keys are the lhs, values are the wincmd direction.
keys = {
["<C-h>"] = "h",
["<C-j>"] = "j",
["<C-k>"] = "k",
["<C-l>"] = "l",
},
}Run :checkhealth ghostty-navigator to verify the daemon is installed, running, and permissions are granted.
If the daemon isn't working, check its logs:
cat ~/.local/share/ghostty-navigator.nvim/stderr.logRemoving the plugin from your Neovim config does not clean up the daemon. Run :GhosttyNavigatorUninstall in Neovim before removing the plugin, or run make uninstall from the plugin directory.
This stops the daemon, removes the LaunchAgent plist, and cleans up the binary and logs.
MIT