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", "fullscreen" },})
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 = { { "ctrl", "alt" }, "space" }, swap_main = { { "ctrl", "alt" }, "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_fullscreen = { { "ctrl", "alt" }, "4" }, set_layout_floating = { { "ctrl", "alt" }, "0" },})Ratio adjustments
Brackets for main area ratio and master count:
spoon.Shoji:bindHotkeys({ increase_main_ratio = { { "ctrl", "alt" }, "]" }, decrease_main_ratio = { { "ctrl", "alt" }, "[" }, increase_nmaster = { { "ctrl", "alt", "shift" }, "]" }, decrease_nmaster = { { "ctrl", "alt", "shift" }, "[" },})Magnified tall for focus
The magnifier modifier enlarges the focused window. Combined with
retile_on_focus, this creates a dwm-style zooming effect:
local Modifiers = spoon.Shoji.Modifierslocal Tall = spoon.Shoji.Layouts.Talllocal Magnifier = Modifiers.Magnifier
local magTall = Modifiers.wrap(Tall, Magnifier.create({ ratio = 1.3 }), { name = "tall-mag", displayName = "Tall + Magnify",})
spoon.Shoji:configure({ layouts = { magTall }, enabled_layouts = { "tall", "tall-mag", "bsp" }, retile_on_focus = true, -- Retile when focus changes to update magnifier})With retile_on_focus enabled, changing focus triggers a retile that
recalculates the magnifier for the newly focused window.