marketplace/remapping

Home Row Navigation

Hold Caps Lock and the home row becomes arrows, Home, End and word movement. Shift still selects.

@rebindv0.1.0free

More install options
$rebind install @rebind/home-row-navigation

Gallery

Home Row Navigation preview

Readme

Hold Caps Lock and your right hand stays on the home row.

Held with the layer keySends
H J K LLeft Down Up Right
UHome
OEnd
NWord left
MWord right

Hold Shift as well to select. Release the layer key and every key types normally again.

Setup

  • Layer key defaults to Caps Lock. While the script runs, that key only holds the layer; it does not toggle Caps Lock.
  • Arrow keys offers hjkl or ijkl.
  • Repeat delay and Repeat interval control how a held arrow repeats, since the layer produces the repeats itself.
  • Pass through in these apps is a comma separated list of process names where the layer stays off, for games or terminals that want the raw keys.

Platform

Windows. On macOS, Caps Lock reports as a toggle rather than a held key, so the layer cannot follow it; a macOS build is planned once a layer key can be held there.

Limits

  • Word movement uses Ctrl+Left and Ctrl+Right. Apps that bind those chords differently will move differently.
  • While the layer is held, keys that have no mapping type normally.

Reviews

No reviews yet. Write a review in the app.

Source

Version 0.1.0 (current)

main.luau

-- Home Row Navigation: hold the layer key and hjkl become arrows.
-- The layer key is swallowed, so Caps Lock never toggles while this runs.

local IS_MAC = _REBIND.platform == "macos"
local WORD = if IS_MAC then "LAlt" else "LCtrl"

local LAYOUTS = {
  hjkl = { H = "Left", J = "Down", K = "Up", L = "Right" },
  ijkl = { J = "Left", K = "Down", I = "Up", L = "Right" },
}

local cfg = UI.Schema({
  layerKey = UI.Keybind("CapsLock", { label = "Layer key", tab = "Layout" }),
  layout = UI.Select(
    "hjkl",
    { "hjkl", "ijkl" },
    { label = "Arrow keys", tab = "Layout" }
  ),
  repeatDelay = UI.Slider(350, {
    min = 150,
    max = 800,
    step = 10,
    suffix = " ms",
    label = "Repeat delay",
    tab = "Layout",
  }),
  repeatRate = UI.Slider(30, {
    min = 15,
    max = 120,
    step = 5,
    suffix = " ms",
    label = "Repeat interval",
    tab = "Layout",
  }),
  exclude = UI.Text("", {
    label = "Pass through in these apps",
    placeholder = "process names, comma separated",
    tab = "Apps",
  }),
})

local layerHeld = false
-- key name -> repeat timer for every mapped key currently held
local held = {}

local function target(key)
  local arrows = LAYOUTS[cfg.layout] or LAYOUTS.hjkl
  if arrows[key] then
    return arrows[key]
  end
  if key == "U" then
    return "Home"
  elseif key == "O" then
    return "End"
  elseif key == "N" then
    return WORD .. "+Left"
  elseif key == "M" then
    return WORD .. "+Right"
  end
  return nil
end

local function excluded()
  local process = System.Window().process:lower()
  for name in cfg.exclude:gmatch("[^,]+") do
    name = name:gsub("^%s+", ""):gsub("%s+$", ""):lower()
    if name ~= "" and process:find(name, 1, true) then
      return true
    end
  end
  return false
end

-- Synthetic keys never autorepeat, so the layer repeats them itself: one tap
-- now, then taps at the configured interval after the delay until release.
-- Shift held by the user is forwarded as a real key, so a tap under it selects.
local function press(key, chord)
  HID.Combo(chord)
  held[key] = Timer.After(cfg.repeatDelay, function()
    held[key] = Timer.Every(cfg.repeatRate, function()
      HID.Combo(chord)
    end)
  end)
end

local function releaseAll()
  for key, timer in held do
    timer:Cancel()
    held[key] = nil
  end
end

function OnDown(key)
  if key == cfg.layerKey then
    layerHeld = true
    return false
  end
  if held[key] then
    return false
  end
  if not layerHeld then
    return true
  end
  local chord = target(key)
  if chord == nil or excluded() then
    return true
  end
  press(key, chord)
  return false
end

function OnUp(key)
  if key == cfg.layerKey then
    layerHeld = false
    releaseAll()
    return false
  end
  local timer = held[key]
  if timer then
    timer:Cancel()
    held[key] = nil
    return false
  end
  return true
end

function OnStart()
  Log.Info(
    `Hold {cfg.layerKey}: {cfg.layout} arrows, U Home, O End, N and M move by word.`
  )
end

function OnStop()
  releaseAll()
end

function OnError(message)
  Log.Error(message)
  releaseAll()
end