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,

rebindlua scriptwindow-specifickeybindshardware inputphotoshopchrome

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

luau
-- 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
end

Block specific keys:

luau
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
end

OnMove — Mouse Tracking

luau
-- 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
end

Track cursor position in real-time:

luau
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
end

Accelerate mouse movement:

luau
-- 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
end

OnScroll — Scroll Detection and Modification

luau
-- 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
end

Invert scroll:

luau
function OnScroll(delta)
    HID.Scroll(-delta)
    return false -- block original
end

Scroll zoom (Ctrl+Scroll):

luau
function OnScroll(delta)
    if Input.IsDown("LAlt") then
        HID.Down("LCtrl")
        HID.Scroll(delta)
        HID.Up("LCtrl")
        return false
    end
    return true
end

Lifecycle Hooks

OnStart — Script Initialization

luau
-- 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)
end

Load configuration:

luau
function OnStart()
    local cfg = UI.Schema({
        sensitivity = UI.Slider(50, { min = 0, max = 100 }),
    })
    
    Log.Info("Sensitivity set to: " .. cfg.sensitivity)
end

OnStop — Cleanup

luau
-- 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")
end

OnFocus / OnBlur — Window Targeting

luau
-- 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")
end

Setup and teardown per app:

luau
-- 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")
end

OnTick — Per-Frame Updates

luau
-- 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
end

Polling-based state machine:

luau
-- 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
end

Continuous monitoring (e.g., track active window):

luau
-- 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
end

Combined Hook Patterns

Double-Tap Detection

luau
-- 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
end

Modifier + Input Combination

luau
-- 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
end

Sequence Detection

luau
-- 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
end

Hold-Loop Inside OnDown/OnUp

luau
-- 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
end

Window-Specific Input Handling

luau
-- 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
end

Rate-Limited OnTick

luau
-- 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
end

Question

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.

luau
-- 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
end

This is the cleanest approach — the script is completely dormant when Photoshop isn't focused.

Multiple process targeting:

luau
-- 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
end

2. Runtime Detection in OnDown (Global Script)

Keep the script always-on and check the window at runtime:

luau
-- 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
end

Tradeoff: 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:

luau
-- 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:

luau
-- 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
end

Advanced 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:

luau
-- photoshop-shortcuts.luau
-- rebind: process=Photoshop.exe
function OnDown(key)
    if key == "Mouse4" then
        HID.Press("LCtrl+Z")
        return false
    end
end
luau
-- 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
end

Each script loads only when its target app is active. No runtime overhead. No conflicts. Easiest to maintain.


Learn more