05-03-26
Window-Specific Keybinds with Rebind Lua
Learn how to create window-specific keybinds using Lua scripts and hardware input with Rebind. This guide covers multiple methods, including modeline targeting, runtime checks, and conditional binds,
Question
Can you show more examples of using Hooks?
Rebind AI
Hooks Examples
Here are practical examples demonstrating each input hook and lifecycle hook.
Input Hooks
OnDown / OnUp — Basic Key Detection
-- rebind: name=Hook Demo - OnDown/OnUp
function OnDown(key)
Log.Info("KEY PRESSED: " .. key)
return true -- pass through
end
function OnUp(key, duration)
Log.Info("KEY RELEASED: " .. key .. " (held " .. duration .. "ms)")
return true -- pass through
endBlock specific keys:
function OnDown(key)
if key == "CapsLock" then
Log.Info("CapsLock blocked")
return false -- swallow it
end
return true
end
function OnUp(key)
if key == "CapsLock" then
return false -- still blocked on release
end
return true
endOnMove — Mouse Tracking
-- rebind: name=Hook Demo - OnMove
function OnMove(dx, dy)
-- dx, dy are relative movement in pixels
Log.Info(string.format("Mouse moved: dx=%d, dy=%d", dx, dy))
return true
endTrack cursor position in real-time:
local lastLog = 0
function OnTick(delta)
local now = System.Time()
if now - lastLog > 500 then -- log every 500ms
local mx, my = System.Mouse()
Log.Info(string.format("Cursor at: %d, %d", mx, my))
lastLog = now
end
endAccelerate mouse movement:
-- rebind: mouse_block=true
-- rebind: name=Mouse Acceleration
local cfg = UI.Schema({
multiplier = UI.Slider(1.5, { min = 1, max = 3, label = "Speed" }),
})
function OnMove(dx, dy)
local magnitude = math.sqrt(dx * dx + dy * dy)
local factor = 1 + (magnitude / 100) * (cfg.multiplier - 1)
HID.Move(dx * factor, dy * factor)
return false -- consume the original move
endOnScroll — Scroll Detection and Modification
-- rebind: name=Hook Demo - OnScroll
function OnScroll(delta)
-- delta > 0 = scroll up, delta < 0 = scroll down
Log.Info("Scroll: " .. (delta > 0 and "UP" or "DOWN") .. " (" .. math.abs(delta) .. " notches)")
return true
endInvert scroll:
function OnScroll(delta)
HID.Scroll(-delta)
return false -- block original
endScroll zoom (Ctrl+Scroll):
function OnScroll(delta)
if Input.IsDown("LAlt") then
HID.Down("LCtrl")
HID.Scroll(delta)
HID.Up("LCtrl")
return false
end
return true
endLifecycle Hooks
OnStart — Script Initialization
-- rebind: name=Hook Demo - OnStart
local gameWindow = nil
local startTime = nil
function OnStart()
Log.Info("Script loaded!")
startTime = System.Time()
-- find a specific window
gameWindow = Window.Find("Counter-Strike")
if gameWindow then
Log.Info("Game window found")
end
-- initialize audio
Audio.Beep()
-- set up a persistent timer
Timer.Every(5000, function()
Log.Info("5 seconds elapsed")
end)
endLoad configuration:
function OnStart()
local cfg = UI.Schema({
sensitivity = UI.Slider(50, { min = 0, max = 100 }),
})
Log.Info("Sensitivity set to: " .. cfg.sensitivity)
endOnStop — Cleanup
-- rebind: name=Hook Demo - OnStop
local activeTask = nil
local timer = nil
function OnStart()
activeTask = Run(function()
while true do
Log.Info("Running...")
Sleep(1000)
end
end)
timer = Timer.Every(2000, function()
Log.Info("Timer tick")
end)
end
function OnStop()
Log.Info("Script stopping - cleaning up...")
-- cancel long-running tasks
if activeTask and activeTask:IsRunning() then
activeTask:Cancel()
end
-- cancel timers
if timer then
timer:Cancel()
end
-- release held keys
HID.Up("W")
HID.Up("LCtrl")
-- stop sounds
Audio.StopAll()
Log.Info("Cleanup complete")
endOnFocus / OnBlur — Window Targeting
-- rebind: name=Hook Demo - OnFocus/OnBlur
-- rebind: process=Photoshop.exe
-- rebind: process=blender.exe
function OnFocus(window)
Log.Info("Focused window: " .. window.title)
Log.Info("Process: " .. window.process)
Log.Info("Position: " .. window.x .. ", " .. window.y)
Log.Info("Size: " .. window.width .. "x" .. window.height)
if window.process:lower():find("photoshop") then
Log.Info("Photoshop activated - loading Photoshop shortcuts")
elseif window.process:lower():find("blender") then
Log.Info("Blender activated - loading Blender shortcuts")
end
end
function OnBlur(window)
Log.Info("Lost focus, new window: " .. window.title)
Log.Info("Deactivating app-specific shortcuts")
endSetup and teardown per app:
-- rebind: process=VS Code
-- rebind: process=Sublime Text
-- rebind: name=Code Editor Shortcuts
local editorTask = nil
function OnFocus(window)
editorTask = Timer.Every(100, function()
-- periodic syntax checking, etc
end)
Log.Info("Code editor active")
end
function OnBlur()
if editorTask then
editorTask:Cancel()
editorTask = nil
end
Log.Info("Code editor deactivated")
endOnTick — Per-Frame Updates
-- rebind: name=Hook Demo - OnTick
-- rebind: tick_rate=1000
local frameCount = 0
local lastSecond = System.Time()
function OnTick(delta)
frameCount = frameCount + 1
-- log FPS every second
local now = System.Time()
if now - lastSecond >= 1000 then
Log.Info("FPS: " .. frameCount)
frameCount = 0
lastSecond = now
end
endPolling-based state machine:
-- rebind: tick_rate=8000
-- rebind: name=Hold Detection
local holdStart = nil
local HOLD_THRESHOLD = 500 -- 500ms
function OnDown(key)
if key == "Mouse1" then
holdStart = System.Time()
end
return true
end
function OnUp(key)
if key == "Mouse1" then
holdStart = nil
end
return true
end
function OnTick(delta)
if holdStart then
local elapsed = System.Time() - holdStart
if elapsed > HOLD_THRESHOLD then
Log.Info("Long hold detected: " .. elapsed .. "ms")
end
end
endContinuous monitoring (e.g., track active window):
-- rebind: tick_rate=2000
-- rebind: name=Window Monitor
local lastWindow = nil
function OnTick()
local win = System.Window()
if not lastWindow or lastWindow.title ~= win.title then
Log.Info("Active window: " .. win.title .. " [" .. win.process .. "]")
lastWindow = win
end
endCombined Hook Patterns
Double-Tap Detection
-- rebind: name=Double Tap Hook Pattern
local lastTap = 0
local TAP_WINDOW = 250 -- 250ms to register double-tap
function OnDown(key)
if key == "W" then
local now = System.Time()
if now - lastTap < TAP_WINDOW then
Log.Info("DOUBLE TAP DETECTED")
HID.Press("Space") -- execute dash or ability
lastTap = 0 -- reset to prevent triple-tap
return false
end
lastTap = now
end
return true
endModifier + Input Combination
-- rebind: name=Modifier Combo Hook Pattern
function OnDown(key)
local ctrl = Input.IsDown("LCtrl")
local shift = Input.IsDown("LShift")
local alt = Input.IsDown("LAlt")
if ctrl and shift and key == "S" then
Log.Info("Ctrl+Shift+S detected")
return false
end
if alt and key == "Tab" then
Log.Info("Alt+Tab detected")
return false
end
return true
endSequence Detection
-- rebind: name=Sequence Hook Pattern
local sequence = {}
local MAX_SEQUENCE = 5
function OnDown(key)
if not (key:len() == 1 or key == "Space") then
return true
end
table.insert(sequence, key)
if #sequence > MAX_SEQUENCE then
table.remove(sequence, 1)
end
local seq = table.concat(sequence)
Log.Info("Sequence: " .. seq)
if seq == "HADOKEN" then
Log.Info("HADOKEN DETECTED!")
return false
end
return true
endHold-Loop Inside OnDown/OnUp
-- rebind: name=Hold Loop Hook Pattern
local task = nil
function OnDown(key)
if key == "Mouse1" then
task = Run(function()
while Input.IsDown("Mouse1") do
HID.Press("LCtrl+Z", 30) -- rapid undo while holding
Sleep(100)
end
Log.Info("Released")
end)
return false
end
return true
end
function OnUp(key)
if key == "Mouse1" and task then
task:Cancel()
task = nil
end
return true
endWindow-Specific Input Handling
-- rebind: name=Context-Aware Hook Pattern
function OnDown(key)
local win = System.Window()
local isBrowser = win.process:lower():find("chrome") or win.process:lower():find("firefox")
local isVS = win.process:lower():find("code")
if isBrowser and key == "Mouse4" then
HID.Press("LCtrl+PageUp") -- previous tab
return false
end
if isVS and key == "Mouse4" then
HID.Press("LCtrl+PageUp") -- previous file
return false
end
return true
endRate-Limited OnTick
-- rebind: tick_rate=8000
-- rebind: name=Rate-Limited Polling
local lastCheck = 0
local CHECK_INTERVAL = 100 -- check every 100ms
function OnTick(delta)
local now = System.Time()
if now - lastCheck < CHECK_INTERVAL then
return
end
lastCheck = now
-- expensive operation here (screen capture, window check, etc)
local win = System.Window()
if win.title:find("game") then
Log.Info("Game detected")
end
endQuestion
Is there a different way to do window-specific keybinds?
Rebind AI
Window-Specific Keybinds
Yes! There are several approaches. Here are the main patterns:
1. Modeline Targeting (Recommended for Simple Cases)
Use window= or process= in the modeline. The script only activates when that window is focused — zero overhead otherwise.
-- rebind: name=Photoshop Shortcuts
-- rebind: process=Photoshop.exe
function OnDown(key)
if key == "Mouse4" then
HID.Press("LCtrl+Z") -- undo
return false
end
if key == "Mouse5" then
HID.Press("LCtrl+LShift+Z") -- redo
return false
end
return true
endThis is the cleanest approach — the script is completely dormant when Photoshop isn't focused.
Multiple process targeting:
-- rebind: name=Browser Shortcuts
-- rebind: process=chrome.exe
-- rebind: process=firefox.exe
-- rebind: process=msedge.exe
function OnDown(key)
if key == "Mouse4" then
HID.Press("LCtrl+PageUp")
return false
end
if key == "Mouse5" then
HID.Press("LCtrl+PageDown")
return false
end
return true
end2. Runtime Detection in OnDown (Global Script)
Keep the script always-on and check the window at runtime:
-- rebind: name=Workflow Hub
function OnDown(key)
local win = System.Window()
if win.process:lower():find("photoshop") then
if key == "Mouse4" then
HID.Press("LCtrl+Z")
return false
end
end
if win.process:lower():find("chrome") or win.process:lower():find("firefox") then
if key == "Mouse4" then
HID.Press("LCtrl+PageUp")
return false
end
end
return true
endTradeoff: This runs on every keypress, but lets you handle multiple apps in one script.
3. Bind with Condition (Modern Approach)
Use Bind with a when guard to conditionally activate:
-- rebind: name=Context-Aware Binds
local function isPhotoshop()
return System.Window().process:lower():find("photoshop")
end
local function isBrowser()
local proc = System.Window().process:lower()
return proc:find("chrome") or proc:find("firefox") or proc:find("msedge")
end
-- Only works when Photoshop is active
Bind("Mouse4", {
when = isPhotoshop,
action = function()
HID.Press("LCtrl+Z")
end,
})
-- Only works in browsers
Bind("Mouse5", {
when = isBrowser,
action = function()
HID.Press("LCtrl+PageDown")
end,
})This is cleaner than OnDown for binding-based logic.
4. OnFocus/OnBlur Setup/Teardown
Activate/deactivate different binds based on focus:
-- rebind: name=Dynamic Bind Switching
local binds = {}
function OnStart()
-- these binds are always loaded but disabled by default
binds.photoshop = {
Bind("Mouse4", { action = function() HID.Press("LCtrl+Z") end }),
Bind("Mouse5", { action = function() HID.Press("LCtrl+LShift+Z") end }),
}
binds.browser = {
Bind("Mouse4", { action = function() HID.Press("LCtrl+PageUp") end }),
Bind("Mouse5", { action = function() HID.Press("LCtrl+PageDown") end }),
}
-- disable all at start
for _, bind in ipairs(binds.photoshop) do bind:disable() end
for _, bind in ipairs(binds.browser) do bind:disable() end
end
function OnFocus(window)
if window.process:lower():find("photoshop") then
for _, bind in ipairs(binds.photoshop) do bind:enable() end
for _, bind in ipairs(binds.browser) do bind:disable() end
elseif window.process:lower():find("chrome") or window.process:lower():find("firefox") then
for _, bind in ipairs(binds.browser) do bind:enable() end
for _, bind in ipairs(binds.photoshop) do bind:disable() end
end
endAdvanced but overkill for most cases. Useful if you need to swap 10+ binds at once.
5. Comparison Table
| Method | Use Case | Overhead | Complexity |
|--------|----------|----------|------------|
| Modeline targeting | App-specific script | Zero when inactive | Simplest — one script per app |
| OnDown check | Multi-app in one script | Per-keypress | Simple — inline checks |
| Bind with when | Modern multi-app | Per-keypress (only for that bind) | Medium — declarative |
| OnFocus/OnBlur | Dynamic bind swapping | One check per focus | Advanced — overkill usually |
Recommended Setup for Multiple Apps
Combine modeline targeting with a "hub" script:
-- photoshop-shortcuts.luau
-- rebind: process=Photoshop.exe
function OnDown(key)
if key == "Mouse4" then
HID.Press("LCtrl+Z")
return false
end
end-- browser-shortcuts.luau
-- rebind: process=chrome.exe
-- rebind: process=firefox.exe
function OnDown(key)
if key == "Mouse4" then
HID.Press("LCtrl+PageUp")
return false
end
endEach script loads only when its target app is active. No runtime overhead. No conflicts. Easiest to maintain.