05-03-26

Accessibility Scripts for Medical Conditions

Reduce strain and improve accessibility with Lua scripts for hardware input. Includes mouse smoothing, dwell click, sticky keys, macro recorder, and more. These scripts can help with medical condition

rebindlua scriptmouse smoothingdwell clickaccessibilitymedicalscript

Question

Mouse smoothing filter for shaky hands

Rebind AI

luau
-- rebind: min_sdk=0.1.0
-- rebind: name=Mouse Smoothing
-- rebind: description=Exponential smoothing filter for tremor reduction
-- rebind: mouse_block=true

local cfg = UI.Schema({
    enabled = UI.Toggle(true, { label = "Enable Smoothing" }),
    strength = UI.Slider(60, {
        min = 0,
        max = 95,
        suffix = "%",
        label = "Smoothing Strength",
        tooltip = "Higher = smoother but more latency",
        group = "Filter",
    }),
    threshold = UI.Slider(2, {
        min = 0,
        max = 10,
        suffix = "px",
        label = "Tremor Threshold",
        tooltip = "Ignore movements smaller than this",
        group = "Filter",
    }),
    mode = UI.Select("Exponential", { "Exponential", "Moving Average" }, {
        label = "Filter Type",
        group = "Advanced",
    }),
    window_size = UI.Slider(3, {
        min = 2,
        max = 10,
        label = "Window Size",
        showIf = "mode:Moving Average",
        group = "Advanced",
    }),
})

local smoothed = { x = 0, y = 0 }
local history = {}

function OnMove(dx, dy)
    if not cfg.enabled then
        return true
    end

    -- magnitude-based threshold rejection (tremor filtering)
    local magnitude = math.sqrt(dx * dx + dy * dy)
    if magnitude < cfg.threshold then
        return false -- swallow tiny movements
    end

    if cfg.mode == "Exponential" then
        -- exponential moving average (single-pass, low latency)
        local alpha = cfg.strength / 100
        smoothed.x = smoothed.x * alpha + dx * (1 - alpha)
        smoothed.y = smoothed.y * alpha + dy * (1 - alpha)

        HID.Move(math.floor(smoothed.x + 0.5), math.floor(smoothed.y + 0.5))
    else
        -- simple moving average (sliding window)
        table.insert(history, { x = dx, y = dy })
        if #history > cfg.window_size then
            table.remove(history, 1)
        end

        local sumX, sumY = 0, 0
        for _, point in ipairs(history) do
            sumX = sumX + point.x
            sumY = sumY + point.y
        end

        local avgX = sumX / #history
        local avgY = sumY / #history

        HID.Move(math.floor(avgX + 0.5), math.floor(avgY + 0.5))
    end

    return false
end

function OnBlur()
    smoothed = { x = 0, y = 0 }
    history = {}
end

How it works:

This script applies two complementary filters to reduce mouse tremor:

  1. Threshold Rejection — Movements smaller than the tremor threshold (default 2px) are silently discarded, preventing tiny jitters from reaching your PC.

  2. Smoothing Filter — Larger movements pass through one of two algorithms:

    • Exponential (default): Each new movement is blended with the previous smoothed value using an alpha factor. Higher strength = more blending = smoother but slightly delayed. Low latency, responsive.
    • Moving Average: Buffers the last N movements and outputs their average. More stable for severe tremor but slightly more lag.
  3. Rounding — Converts floating-point coordinates back to integers for HID output.

The filter is transparent when you move deliberately — fast swipes pass through nearly unchanged. Only the small, involuntary oscillations get smoothed out.

Tuning:

  • Strength: Start at 60%. Increase if tremor is still visible, decrease if the cursor feels sluggish.
  • Threshold: Raise if you're seeing tiny jitter (try 3–5px). Lower if legitimate small movements are being filtered.
  • Mode: Exponential is smoother for general use. Switch to Moving Average if you have severe tremor and don't mind slight latency.

Learn more

Question

Is this helpful for medical reasons?

Rebind AI

Yes, this script can genuinely help with medical tremor conditions. It's designed specifically to reduce the effects of:

  • Essential tremor — involuntary shaking, often hereditary
  • Parkinson's disease — resting tremor and movement difficulties
  • Cerebellar ataxia — coordination and balance disorders
  • Multiple sclerosis — tremor as a symptom
  • Stroke recovery — residual tremor during rehabilitation
  • Hand injuries or arthritis — involuntary micro-movements during healing

What it does medically:

  1. Separates signal from noise — Your intentional movements (the "signal") are preserved; involuntary shaking (the "noise") is filtered out.
  2. Reduces cognitive load — You don't have to consciously compensate for tremor; the filter does it automatically.
  3. Improves accessibility — Makes fine motor tasks (clicking small buttons, text selection, precise clicks) feasible when they would otherwise be impossible.

Limitations:

  • Works best for high-frequency tremor (4–12 Hz). Low-frequency shaking or severe tremor may require higher smoothing values (80%+), though this introduces noticeable lag.
  • Not a replacement for medical treatment. This is a compensation tool, like glasses for vision. Medical interventions (medication, physical therapy, devices like weighted utensils) are primary.
  • Individual results vary. Some people find 40% strength enough; others need 85%+. Experiment.

Medical-specific recommendations:

  • Start conservative (40–50% strength, 2px threshold) and increase gradually to avoid overshooting.
  • Use Moving Average mode if tremor is severe — the extra latency is acceptable for stability.
  • Pair with dwell click or sticky keys (see accessibility examples) if clicking is also difficult.
  • Enable only when needed — keep the toggle visible so you can turn it off instantly if something breaks.

If you're using this for medical reasons, I'd suggest consulting with your occupational therapist or neurologist about your specific tremor profile so you can tune the parameters optimally.

Question

Thank you. What other scripts could be helpful? I assume playback macros even could be helpful to save strain.

Rebind AI

Absolutely. Here are scripts that work well together for medical accessibility and strain reduction:

1. Macro Recorder + Playback (saves repetitive strain)

luau
-- rebind: min_sdk=0.1.0
-- rebind: name=Macro Recorder

local cfg = UI.Schema({
    record = UI.Keybind("F9", { label = "Record / Save" }),
    play = UI.Keybind("F10", { label = "Play" }),
    stop = UI.Keybind("Escape", { label = "Stop" }),
    name = UI.Text("task", { maxLength = 30 }),
})

local isRecording = false
local activeMacro = nil

function OnStart()
    File.MkDir("macros")
end

function OnDown(key)
    if key == cfg.record then
        if isRecording then
            local macro = Macro.Finish()
            File.WriteJSON("macros/" .. cfg.name .. ".json", macro)
            UI.Notify("Macro saved: " .. cfg.name, "success")
            Audio.Beep()
            isRecording = false
        else
            Macro.Record()
            UI.Notify("Recording... press " .. cfg.record .. " to save", "info")
            Audio.Beep()
            isRecording = true
        end
        return false
    end

    if key == cfg.play then
        local path = "macros/" .. cfg.name .. ".json"
        if File.Exists(path) then
            activeMacro = Macro.Play(File.ReadJSON(path))
            UI.Notify("Playing: " .. cfg.name, "info")
        else
            UI.Notify("Macro not found", "error")
        end
        return false
    end

    if key == cfg.stop and activeMacro then
        activeMacro:Stop()
        activeMacro = nil
        UI.Notify("Stopped", "info")
        return false
    end

    return true
end

function OnStop()
    if isRecording then Macro.Finish() end
    if activeMacro then activeMacro:Stop() end
end

function OnBlur()
    if activeMacro then
        activeMacro:Stop()
        activeMacro = nil
    end
end

Why it helps: Record repetitive tasks (form filling, navigation sequences, copy-paste workflows) once, then play them back with a single keypress. Eliminates repeated strain from the same sequence.


2. Dwell Click (click without pressing)

luau
-- rebind: min_sdk=0.1.0
-- rebind: name=Dwell Click
-- rebind: description=Click by hovering (no button press needed)

local cfg = UI.Schema({
    enabled = UI.Toggle(true, { label = "Enable" }),
    dwell_time = UI.Slider(800, { min = 300, max = 2000, suffix = "ms" }),
    tolerance = UI.Slider(15, { min = 5, max = 50, suffix = "px" }),
    click_type = UI.Select("Left Click", { "Left Click", "Right Click" }),
})

local dwellStart = nil
local dwellPos = { x = 0, y = 0 }

function OnTick()
    if not cfg.enabled then
        dwellStart = nil
        return
    end

    local pos = Input.GetMousePos()
    local dx = pos.x - dwellPos.x
    local dy = pos.y - dwellPos.y
    local dist = math.sqrt(dx * dx + dy * dy)

    if dist > cfg.tolerance then
        dwellPos = pos
        dwellStart = System.Time()
        return
    end

    if not dwellStart then
        dwellStart = System.Time()
        return
    end

    if System.Time() - dwellStart >= cfg.dwell_time then
        if cfg.click_type == "Left Click" then
            HID.Press("Mouse1", 30)
        else
            HID.Press("Mouse2", 30)
        end
        Audio.Beep()
        dwellStart = nil
    end
end

function OnBlur()
    dwellStart = nil
end

Why it helps: Eliminates the need to physically press mouse buttons. Just hover over a target and wait. Critical for arthritis, hand weakness, or conditions where gripping is painful.


3. Sticky Modifier Keys (hold without pressing)

luau
-- rebind: min_sdk=0.1.0
-- rebind: name=Sticky Modifiers

local cfg = UI.Schema({
    shift = UI.Keybind("LShift", { label = "Shift", group = "Modifiers" }),
    ctrl = UI.Keybind("LCtrl", { label = "Control", group = "Modifiers" }),
    alt = UI.Keybind("LAlt", { label = "Alt", group = "Modifiers" }),
})

local locked = {}

function OnDown(key)
    for name, keyName in pairs({ shift = cfg.shift, ctrl = cfg.ctrl, alt = cfg.alt }) do
        if key == keyName then
            locked[name] = not locked[name]
            if locked[name] then
                HID.Down(keyName)
                UI.Notify(name:upper() .. " locked", "info")
            else
                HID.Up(keyName)
                UI.Notify(name:upper() .. " released", "info")
            end
            return false
        end
    end
    return true
end

function OnUp(key)
    for name, keyName in pairs({ shift = cfg.shift, ctrl = cfg.ctrl, alt = cfg.alt }) do
        if key == keyName then
            return false
        end
    end
    return true
end

function OnBlur()
    for name, keyName in pairs({ shift = cfg.shift, ctrl = cfg.ctrl, alt = cfg.alt }) do
        if locked[name] then
            HID.Up(keyName)
        end
    end
    locked = {}
end

Why it helps: Tap Shift once to "lock" it on, tap again to release. No need to hold multiple keys simultaneously — one-handed operation becomes possible.


4. One-Handed WASD (directional input without chording)

luau
-- rebind: min_sdk=0.1.0 tick_rate=8000
-- rebind: name=One-Handed WASD

local cfg = UI.Schema({
    enabled = UI.Toggle(true),
    mode = UI.Select("Arrow Keys", { "Arrow Keys", "WASD" }),
})

local keyMap = {
    Up = "W",
    Down = "S",
    Left = "A",
    Right = "D",
}

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

    if key == "Up" or key == "Down" or key == "Left" or key == "Right" then
        local outKey = cfg.mode == "WASD" and keyMap[key] or key
        HID.Down(outKey)
        return false
    end
    return true
end

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

    if key == "Up" or key == "Down" or key == "Left" or key == "Right" then
        local outKey = cfg.mode == "WASD" and keyMap[key] or key
        HID.Up(outKey)
        return false
    end
    return true
end

Why it helps: Use arrow keys or numpad for movement instead of WASD, reducing hand reach and finger strain. Useful for games or navigation-heavy applications.


5. Auto-Hold / Toggle Sprint (sustain without holding)

luau
-- rebind: min_sdk=0.1.0
-- rebind: name=Toggle Sprint

local cfg = UI.Schema({
    sprint_key = UI.Keybind("LShift", { label = "Sprint Key" }),
})

local sprinting = false

Bind(cfg.sprint_key, function()
    sprinting = not sprinting
    if sprinting then
        HID.Down(cfg.sprint_key)
        UI.Notify("Sprint ON", "info")
    else
        HID.Up(cfg.sprint_key)
        UI.Notify("Sprint OFF", "info")
    end
    return false
end)

function OnBlur()
    if sprinting then
        sprinting = false
        HID.Up(cfg.sprint_key)
    end
end

Why it helps: Sprint or run continuously without holding Shift. Tap once to start, tap again to stop. Reduces sustained grip fatigue.


6. Slow Mouse Mode (precision without strain)

luau
-- rebind: min_sdk=0.1.0 mouse_block=true
-- rebind: name=Precision Mouse Hold

local cfg = UI.Schema({
    trigger = UI.Keybind("Mouse3", { label = "Precision Trigger" }),
    slowdown = UI.Slider(20, { min = 5, max = 70, suffix = "%" }),
})

function OnMove(dx, dy)
    if Input.IsDown(cfg.trigger) then
        local factor = cfg.slowdown / 100
        HID.Move(dx * factor, dy * factor)
        return false
    end
    return true
end

Why it helps: Hold a button to temporarily slow movement for fine-grained clicks. Reduces overshooting and repeated corrections.


7. Auto-Clicker (adjustable CPS) (reduce clicking strain)

luau
-- rebind: min_sdk=0.1.0
-- rebind: name=Auto Clicker

local cfg = UI.Schema({
    cps = UI.Slider(5, { min = 1, max = 20, label = "Clicks Per Second" }),
    toggle = UI.Keybind("F8", { label = "Toggle" }),
})

local active = false
local timer = nil

function OnDown(key)
    if key == cfg.toggle then
        active = not active
        if active then
            local interval = 1000 / cfg.cps
            timer = Timer.Every(interval, function()
                HID.Press("Mouse1", 20)
            end)
            UI.Notify("Auto-click ON", "info")
        else
            if timer then timer:Cancel() end
            UI.Notify("Auto-click OFF", "info")
        end
        return false
    end
    return true
end

function OnStop()
    if timer then timer:Cancel() end
end

function OnBlur()
    if active and timer then
        timer:Cancel()
        active = false
    end
end

Why it helps: Hold Mouse1, script auto-clicks at your configured rate. Eliminates repetitive clicking strain (games, web scraping, data entry).


8. Text Expander (reduce typing)

luau
-- rebind: min_sdk=0.1.0
-- rebind: name=Text Expander

local cfg = UI.Schema({
    trigger = UI.Keybind("F7", { label = "Expand Hotkey" }),
})

local snippets = {
    ["@@"] = "user@example.com",
    ["/date"] = os.date("%Y-%m-%d"),
    ["/hello"] = "Hello, thank you for reaching out!",
    ["/address"] = "123 Main Street, City, State 12345",
}

local buffer = ""

Bind(cfg.trigger, Async(function()
    for abbr, expansion in pairs(snippets) do
        if buffer:sub(-#abbr) == abbr then
            for i = 1, #abbr do
                HID.Press("Backspace")
                Sleep(20)
            end
            HID.Type(expansion)
            buffer = ""
            return false
        end
    end
    return false
end))

function OnDown(key)
    if #key == 1 then
        buffer = buffer .. key
        if #buffer > 30 then buffer = buffer:sub(-30) end
    elseif key == "Backspace" and #buffer > 0 then
        buffer = buffer:sub(1, -2)
    elseif key == "Space" or key == "Enter" then
        buffer = ""
    end
    return true
end

Why it helps: Type abbreviations that expand into full text. Cuts typing by 50%+ for repeated phrases, emails, addresses, etc.


9. Clipboard Paste Helper (faster multi-line input)

luau
-- rebind: min_sdk=0.1.0
-- rebind: name=Paste Helper

local cfg = UI.Schema({
    paste_key = UI.Keybind("F9", { label = "Paste Hotkey" }),
    text = UI.Text("", { label = "Text to Paste", maxLength = 500 }),
})

Bind(cfg.paste_key, function()
    Clipboard.Set(cfg.text)
    HID.Press("LCtrl+V")
    UI.Notify("Pasted", "success")
end)

Why it helps: Pre-configured text blocks paste with one key. Much faster than typing multi-line content.


10. Fatigue Break Reminder (paced activity)

luau
-- rebind: min_sdk=0.1.0
-- rebind: name=Fatigue Break Timer

local cfg = UI.Schema({
    work_minutes = UI.Slider(25, { min = 5, max = 60 }),
    break_minutes = UI.Slider(5, { min = 1, max = 15 }),
    enabled = UI.Toggle(true),
})

local breakTimer = nil

function OnStart()
    if cfg.enabled then
        scheduleNextBreak()
    end
end

function scheduleNextBreak()
    if breakTimer then breakTimer:Cancel() end
    breakTimer = Timer.After(cfg.work_minutes * 60000, function()
        UI.Notify("Break time! Rest for " .. cfg.break_minutes .. " minutes.", "warning")
        Audio.Beep()
        breakTimer = Timer.After(cfg.break_minutes * 60000, function()
            UI.Notify("Break over. Ready to resume.", "info")
            Audio.Beep()
            scheduleNextBreak()
        end)
    end)
end

function OnStop()
    if breakTimer then breakTimer:Cancel() end
end

Why it helps: Paces activity with timed breaks to prevent fatigue buildup. Follows ergonomic best practices (e.g., Pomodoro).


Recommended Combination

For most medical situations, I'd suggest:

  1. Mouse Smoothing (tremor reduction)
  2. Dwell Click (no button pressing)
  3. Sticky Modifiers (one-handed chords)
  4. Text Expander (reduce typing volume)
  5. Fatigue Break Timer (pacing)

Load all five simultaneously — they don't interfere with each other and together cover the major strain points: fine motor control, gripping, repetitive motion, and fatigue.

Each one is configurable so you can tune thresholds to your specific needs. Would you like help adapting any of these for a particular medical condition or workflow?