05-03-26

Movement Key Rebinder Per Window

This Lua script remaps WASD keys to arrow keys or custom bindings, with per-application profiles. It uses the Rebind SDK for hardware input and offers different approaches to key remapping.

rebindlua scriptmovementkeybindremaphardware inputscript

Question

I need a script to remap WASD keys on a per-application basis.

Rebind AI

Movement Key Rebinder Per Window

lua
-- rebind: name=Movement Key Rebinder Per Window
-- rebind: description=Remap WASD to arrow keys or custom bindings per application

local cfg = UI.Schema({
    enabled = UI.Toggle(true, { label = "Enable" }),

    -- Default global bindings
    w_key = UI.Keybind("Up", { label = "W → ", group = "Global" }),
    a_key = UI.Keybind("Left", { label = "A → ", group = "Global" }),
    s_key = UI.Keybind("Down", { label = "S → ", group = "Global" }),
    d_key = UI.Keybind("Right", { label = "D → ", group = "Global" }),

    -- Per-app overrides (stored as app name, e.g. "minecraft", "valorant")
    use_profiles = UI.Toggle(false, { label = "Enable Per-App Profiles", group = "Profiles" }),
})

-- Profile storage: { [processName] = { w, a, s, d }, ... }
local profiles = {}
local PROFILE_FILE = "movement_profiles.json"

local function loadProfiles()
    if File.Exists(PROFILE_FILE) then
        profiles = File.ReadJSON(PROFILE_FILE)
    else
        profiles = {}
    end
end

local function saveProfiles()
    File.WriteJSON(PROFILE_FILE, profiles)
end

local function getProcessName()
    local win = System.Window()
    return win.process:lower()
end

local function getBindings()
    if not cfg.use_profiles then
        return {
            w = cfg.w_key,
            a = cfg.a_key,
            s = cfg.s_key,
            d = cfg.d_key,
        }
    end

    local proc = getProcessName()
    if profiles[proc] then
        return profiles[proc]
    end

    -- fallback to global
    return {
        w = cfg.w_key,
        a = cfg.a_key,
        s = cfg.s_key,
        d = cfg.d_key,
    }
end

local function setProfile(processName, bindings)
    profiles[processName] = bindings
    saveProfiles()
end

function OnStart()
    loadProfiles()
    Log.Info("Movement rebinder loaded")
end

function OnDown(key)
    if not cfg.enabled then
        return true
    end

    local bindings = getBindings()
    local remapped = nil

    if key == "W" then
        remapped = bindings.w
    elseif key == "A" then
        remapped = bindings.a
    elseif key == "S" then
        remapped = bindings.s
    elseif key == "D" then
        remapped = bindings.d
    end

    if remapped then
        HID.Down(remapped)
        return false
    end

    return true
end

function OnUp(key)
    if not cfg.enabled then
        return true
    end

    local bindings = getBindings()
    local remapped = nil

    if key == "W" then
        remapped = bindings.w
    elseif key == "A" then
        remapped = bindings.a
    elseif key == "S" then
        remapped = bindings.s
    elseif key == "D" then
        remapped = bindings.d
    end

    if remapped then
        HID.Up(remapped)
        return false
    end

    return true
end

How It Works

This script remaps WASD movement keys on a per-application basis:

Global Mode (default):

  • All WASD presses are remapped to the configured keys (default: arrow keys)
  • Works the same way in every application

Per-App Profiles:

  • Enable "Enable Per-App Profiles" to save different bindings for each game/app
  • Profiles are stored in movement_profiles.json next to your script
  • Each process name gets its own keybinding configuration
  • Falls back to global settings for unmapped applications

How to use:

  1. Keep "Enable Per-App Profiles" off to remap WASD globally
  2. Configure your preferred keys (W→Up, A→Left, S→Down, D→Right by default)
  3. Enable per-app profiles if you need different bindings per game
  4. The script automatically detects which app is active and applies the right bindings

For per-app setup:

  • Focus your game/app
  • Edit the generated movement_profiles.json or modify keys in the UI
  • The script automatically uses app-specific bindings when that process is in focus

Learn more

Question

Are there alternative ways to implement this, such as using multiple scripts?

Rebind AI

Yes, there are several alternative approaches:

1. Multiple Targeted Scripts (Cleanest)

Create separate scripts, each targeting a specific application:

wasd_global.luau (no targeting, always active)

lua
-- rebind: name=WASD → Arrows (Global)

local cfg = UI.Schema({
    enabled = UI.Toggle(true),
})

Bind.Remap("W", "Up")
Bind.Remap("A", "Left")
Bind.Remap("S", "Down")
Bind.Remap("D", "Right")

wasd_minecraft.luau (Minecraft only)

lua
-- rebind: name=WASD → IJKL (Minecraft)
-- rebind: process=javaw.exe

Bind.Remap("W", "I")
Bind.Remap("A", "J")
Bind.Remap("S", "K")
Bind.Remap("D", "L")

wasd_valorant.luau (Valorant only)

lua
-- rebind: name=WASD Rebinder (Valorant)
-- rebind: process=valorant.exe

local cfg = UI.Schema({
    w_key = UI.Keybind("W"),
    a_key = UI.Keybind("A"),
    s_key = UI.Keybind("S"),
    d_key = UI.Keybind("D"),
})

function OnDown(key)
    if key == "W" then HID.Down(cfg.w_key) return false end
    if key == "A" then HID.Down(cfg.a_key) return false end
    if key == "S" then HID.Down(cfg.s_key) return false end
    if key == "D" then HID.Down(cfg.d_key) return false end
    return true
end

function OnUp(key)
    if key == "W" then HID.Up(cfg.w_key) return false end
    if key == "A" then HID.Up(cfg.a_key) return false end
    if key == "S" then HID.Up(cfg.s_key) return false end
    if key == "D" then HID.Up(cfg.d_key) return false end
    return true
end

Pros:

  • Zero overhead when not in target app (targeting eliminates the script entirely)
  • Simple, focused code per script
  • Easy to enable/disable individual game configs
  • No complex routing logic needed
  • Best performance

Cons:

  • More files to manage
  • Duplicate code

2. One Script with Runtime App Detection

Single script that checks the active window on every keypress:

lua
-- rebind: name=Movement Rebinder (Smart)

local cfg = UI.Schema({
    enabled = UI.Toggle(true, { label = "Enable" }),
})

local MAPPINGS = {
    ["javaw.exe"] = { w = "I", a = "J", s = "K", d = "L" },
    ["valorant.exe"] = { w = "W", a = "A", s = "S", d = "D" }, -- no remap
    ["cs2.exe"] = { w = "W", a = "A", s = "S", d = "D" },
    -- default for unlisted apps
    ["_default"] = { w = "Up", a = "Left", s = "Down", d = "Right" },
}

local function getBindings()
    local proc = System.Window().process:lower()
    return MAPPINGS[proc] or MAPPINGS["_default"]
end

function OnDown(key)
    if not cfg.enabled then return true end

    local bindings = getBindings()
    local remapped = bindings[key:lower()]

    if remapped then
        HID.Down(remapped)
        return false
    end
    return true
end

function OnUp(key)
    if not cfg.enabled then return true end

    local bindings = getBindings()
    local remapped = bindings[key:lower()]

    if remapped then
        HID.Up(remapped)
        return false
    end
    return true
end

Pros:

  • One script to manage
  • Easy to add/remove games (just edit the MAPPINGS table)
  • Works globally (runs in all apps)

Cons:

  • Slight overhead even when not needed (checking on every key)
  • Single UI for all games (less flexible per-game config)

3. Modular with Shared Code

Create a shared module, then simple scripts that use it:

lib/movement.luau (shared logic)

lua
local M = {}

M.Mappings = {
    ["javaw.exe"] = { w = "I", a = "J", s = "K", d = "L" },
    ["valorant.exe"] = { w = "W", a = "A", s = "S", d = "D" },
    ["_default"] = { w = "Up", a = "Left", s = "Down", d = "Right" },
}

function M.getBindings(processName)
    return M.Mappings[processName:lower()] or M.Mappings["_default"]
end

function M.bindMovement()
    return {
        onDown = function(key, bindings)
            local remapped = bindings[key:lower()]
            if remapped then
                HID.Down(remapped)
                return false
            end
            return true
        end,
        onUp = function(key, bindings)
            local remapped = bindings[key:lower()]
            if remapped then
                HID.Up(remapped)
                return false
            end
            return true
        end,
    }
end

return M

movement_main.luau (minimal script using the module)

lua
-- rebind: name=Movement Rebinder (Modular)

local movement = require("./lib/movement")

local cfg = UI.Schema({
    enabled = UI.Toggle(true),
})

local handlers = movement.bindMovement()

function OnDown(key)
    if not cfg.enabled then return true end
    local bindings = movement.getBindings(System.Window().process)
    return handlers.onDown(key, bindings)
end

function OnUp(key)
    if not cfg.enabled then return true end
    local bindings = movement.getBindings(System.Window().process)
    return handlers.onUp(key, bindings)
end

Pros:

  • Reusable module code
  • Central place to manage all mappings
  • Can be used by multiple scripts

Cons:

  • Still runs globally (some overhead)
  • Slightly more complex setup

Recommendation

| Use Case | Best Approach | |----------|--------------| | 2-3 games | Multiple targeted scripts (simplest, best perf) | | Many games | Single app-detection script (easier to maintain) | | Want to share code | Modular with lib (reusable across scripts) |

For most use cases, multiple targeted scripts is ideal because:

  • ✅ Zero overhead when not in target app
  • ✅ Simple, focused code per script
  • ✅ Easy to enable/disable per game
  • ✅ Can have different UIs per game

Just create one script per game you care about, each with its own -- rebind: process= line targeting that specific executable.

Question

Should I run it at 8K or is the normal 1K fine?

Rebind AI

1K (1000 Hz) is fine for movement remapping.

Here's why:

| Tick Rate | Use Case | Why | |-----------|----------|-----| | 1K (default) | Movement remapping, most scripts | Plenty fast. Key presses are discrete events, not continuous polling. | | 8K (8000 Hz) | Precision mouse control, aim assist, tremor smoothing | Needed for sub-millisecond mouse movement accuracy. |

For your movement rebinder:

Stick with 1K. You're just remapping key presses, which fire instantly regardless of tick rate. There's no benefit to 8K here.

Only use 8K if you're also:

  • Doing mouse movement math (acceleration, aim snapping, etc.)
  • Detecting pixel colors in OnTick
  • Doing rapid polling of input state for continuous corrections

Example: When 8K matters

lua
-- rebind: tick_rate=8000 mouse_block=true
-- This one needs 8K for smooth mouse control

function OnMove(dx, dy)
    -- apply aim acceleration
    local factor = calculateAcceleration(dx, dy)
    HID.Move(dx * factor, dy * factor)
    return false
end

vs your movement script:

lua
-- rebind: tick_rate=1000 (default, no need to specify)
-- This one is fine at 1K

function OnDown(key)
    if key == "W" then
        HID.Down("Up")
        return false
    end
end

TL;DR: Use 1K for movement remapping. Save 8K for mouse precision scripts. You'll get the same responsiveness either way since keys are event-driven, not polled.

Question

Ok thank you!

Rebind AI

You're welcome! Good luck with your movement rebinder. Feel free to ask if you need help tweaking it or run into any issues. Happy scripting! 🎮