Vim workflow
Window management with hjkl navigation and familiar Vim patterns.
Basic Vim keybindings
Directional navigation and window swapping:
hs.loadSpoon("Shoji")
spoon.Shoji:configure({ default_layout = "tall", enabled_layouts = { "tall", "wide", "bsp", "monocle" },})
spoon.Shoji:bindHotkeys({ -- Navigation hotkeys focus_left = { { "ctrl", "alt" }, "h" }, focus_down = { { "ctrl", "alt" }, "j" }, focus_up = { { "ctrl", "alt" }, "k" }, focus_right = { { "ctrl", "alt" }, "l" },
-- Swap hotkeys swap_left = { { "ctrl", "alt", "shift" }, "h" }, swap_down = { { "ctrl", "alt", "shift" }, "j" }, swap_up = { { "ctrl", "alt", "shift" }, "k" }, swap_right = { { "ctrl", "alt", "shift" }, "l" },
-- Layout control cycle_layout_forward = { { "ctrl", "alt" }, "space" }, swap_main = { { "ctrl", "alt", "shift" }, "return" },})
spoon.Shoji:start()Leader key pattern
Hammerspoon’s modal bindings can emulate Vim’s leader key. Press Ctrl+W to enter window mode, then a single key to act:
-- Create a modal for window managementlocal windowMode = hs.hotkey.modal.new({ "ctrl" }, "w")
function windowMode:entered() hs.alert.show("Window mode")end
function windowMode:exited() hs.alert.show("Normal mode")end
-- Exit modal with EscapewindowMode:bind({}, "escape", function() windowMode:exit() end)
-- Window navigationwindowMode:bind({}, "h", function() spoon.Shoji.actions.focusLeft() windowMode:exit()end)windowMode:bind({}, "j", function() spoon.Shoji.actions.focusDown() windowMode:exit()end)windowMode:bind({}, "k", function() spoon.Shoji.actions.focusUp() windowMode:exit()end)windowMode:bind({}, "l", function() spoon.Shoji.actions.focusRight() windowMode:exit()end)
-- Swap with shift modifierwindowMode:bind({ "shift" }, "h", function() spoon.Shoji.actions.swapLeft() windowMode:exit()end)Ctrl+W h/j/k/l navigates. Ctrl+W Shift+h/j/k/l swaps.
Neovim integration
Navigate seamlessly between Neovim splits and Shoji windows. When a terminal is focused, navigation keys go to Neovim. When Neovim reaches the edge of its splits, it can pass focus back to Shoji.
This requires setup on both sides. The Hammerspoon configuration:
-- In ~/.hammerspoon/init.lua
-- Check if focused app is a terminal (customize for your terminal)local function isTerminal() local app = hs.application.frontmostApplication() if not app then return false end local bundleID = app:bundleID() return bundleID == "com.apple.Terminal" or bundleID == "com.googlecode.iterm2" or bundleID == "io.alacritty" or bundleID == "com.mitchellh.ghostty"end
local directionKeys = { left = "h", right = "l", up = "k", down = "j",}
local function neovimAwareFocus(direction) if isTerminal() then -- Send to terminal (Neovim handles it if running) hs.eventtap.keyStroke({ "ctrl" }, directionKeys[direction]) else -- Use Shoji for window navigation if direction == "left" then spoon.Shoji.actions.focusLeft() elseif direction == "right" then spoon.Shoji.actions.focusRight() elseif direction == "up" then spoon.Shoji.actions.focusUp() elseif direction == "down" then spoon.Shoji.actions.focusDown() end endend
-- Bind navigation keysfor direction, _ in pairs(directionKeys) do hs.hotkey.bind({ "ctrl", "alt" }, directionKeys[direction], function() neovimAwareFocus(direction) end)endFor the Neovim side, use a plugin like
vim-tmux-navigator or write
custom mappings that call hs -c 'spoon.Shoji:cmd("focus", "left")' via
jobstart() when at a split boundary.
Quick layout switching
Number keys for direct layout access:
spoon.Shoji:bindHotkeys({ set_layout_tall = { { "ctrl", "alt" }, "1" }, set_layout_wide = { { "ctrl", "alt" }, "2" }, set_layout_bsp = { { "ctrl", "alt" }, "3" }, set_layout_monocle = { { "ctrl", "alt" }, "4" }, set_layout_floating = { { "ctrl", "alt" }, "0" },})Ratio adjustments
Brackets for main area ratio and main window count:
spoon.Shoji:bindHotkeys({ increase_main_ratio = { { "ctrl", "alt" }, "]" }, decrease_main_ratio = { { "ctrl", "alt" }, "[" }, increase_main_count = { { "ctrl", "alt", "shift" }, "]" }, decrease_main_count = { { "ctrl", "alt", "shift" }, "[" },})