Mouse App Shortcuts

@rebindv0.1.5

Side buttons mean back and forward in browsers, undo and redo in editors, previous and next sheet in spreadsheets.

Files

main.luau

-- Mouse App Shortcuts: two side buttons change meaning by focused app.
-- Anywhere not listed, the buttons pass through untouched.

local IS_MAC = _REBIND.platform == "macos"

local cfg = UI.Schema({
  browsers = UI.Text("chrome, firefox, brave, edge, safari, arc", {
    label = "Browsers (back / forward)",
    placeholder = "process names, comma separated",
    tab = "Apps",
  }),
  editors = UI.Text("code, cursor, sublime, notepad++, zed, textedit", {
    label = "Editors (undo / redo)",
    placeholder = "process names, comma separated",
    tab = "Apps",
  }),
  sheets = UI.Text("excel, numbers, soffice, libreoffice", {
    label = "Spreadsheets (previous / next sheet)",
    placeholder = "process names, comma separated",
    tab = "Apps",
  }),
  back = UI.Keybind("Mouse4", { label = "First side button", tab = "Apps" }),
  forward = UI.Keybind(
    "Mouse5",
    { label = "Second side button", tab = "Apps" }
  ),
  swap = UI.Toggle(false, {
    label = "Swap the two side buttons",
    tab = "Apps",
  }),
})

-- chords per class: { Mouse4, Mouse5 }
local CHORDS = {
  browsers = if IS_MAC
    then { "LWin+[", "LWin+]" }
    else { "LAlt+Left", "LAlt+Right" },
  editors = if IS_MAC
    then { "LWin+Z", "LWin+LShift+Z" }
    else { "LCtrl+Z", "LCtrl+Y" },
  sheets = if IS_MAC
    then { "LAlt+Left", "LAlt+Right" }
    else { "LCtrl+PageUp", "LCtrl+PageDown" },
  -- Figma redoes with Shift+Z on both systems
  figma = if IS_MAC
    then { "LWin+Z", "LWin+LShift+Z" }
    else { "LCtrl+Z", "LCtrl+LShift+Z" },
  -- Google Sheets: previous and next sheet (support.google.com/docs/answer/181110)
  gsheets = { "LAlt+Up", "LAlt+Down" },
}

local function names(list)
  local out = {}
  for name in list:gmatch("[^,]+") do
    name = name:gsub("^%s+", ""):gsub("%s+$", ""):lower()
    if name ~= "" then
      table.insert(out, name)
    end
  end
  return out
end

-- web apps run inside a browser, so their window title names them first
local function classOf(window)
  local title = window.title:lower()
  if title:find("google sheets", 1, true) then
    return "gsheets"
  end
  local process = window.process:lower()
  if title:find("%f[%w]figma%f[%W]") or process:find("figma", 1, true) then
    return "figma"
  end
  for _, class in { "browsers", "editors", "sheets" } do
    for _, name in names(cfg[class]) do
      if process:find(name, 1, true) then
        return class
      end
    end
  end
  return nil
end

local function bindSide(button, index)
  Bind(button, {
    when = function()
      return classOf(System.Window()) ~= nil
    end,
    action = function()
      local class = classOf(System.Window())
      local slot = if cfg.swap then 3 - index else index
      HID.Combo(CHORDS[class][slot])
      return nil
    end,
  })
end

bindSide(cfg.back, 1)
bindSide(cfg.forward, 2)

function OnStart()
  Log.Info(
    `{cfg.back} and {cfg.forward} follow the focused app. Elsewhere they pass through.`
  )
end