From 8b0ef84f38660e0e338b45a307df5c66c01a1a4b Mon Sep 17 00:00:00 2001 From: Giovanni Harting <539@idlegandalf.com> Date: Sat, 8 Nov 2025 23:50:17 +0100 Subject: [PATCH] feat: relight keyboard on activity --- scripts/kb-blackout.lua | 140 ++++++++++++++++++++++++++++++++++++++-- 1 file changed, 136 insertions(+), 4 deletions(-) diff --git a/scripts/kb-blackout.lua b/scripts/kb-blackout.lua index f1a46a4..6ed599b 100644 --- a/scripts/kb-blackout.lua +++ b/scripts/kb-blackout.lua @@ -5,24 +5,25 @@ * Listens to pause/playback/core-idle so lights only stay off when media is actively playing. * Captures the session's starting brightness, dims via `brightnessctl`, and restores exactly that value when playback stops. * Debounces rapid state flips, auto-discovers an LED device (`brightnessctl --list`), and surfaces warnings when Linux, device, or permission prerequisites fail. - * Optionally restores the keyboard backlight whenever mpv loses window focus so other apps retain your preferred lighting. + * Instantly relights on keyboard activity (while keeping mpv's bindings intact) and optionally restores the keyboard backlight whenever mpv loses window focus. Requirements: * Linux + mpv 0.36+ with the bundled Lua 5.2 runtime. * `brightnessctl` accessible in PATH or via `brightnessctl_path`, with permission to control the target LED class device. Configuration: - Edit the `config` table below to tune timeouts, restore behavior, minimum brightness, LED device detection, focus handling, and the `brightnessctl` binary path. + Edit the `config` table below to adjust the inactivity timeout, restore behavior, minimum brightness, LED device detection, focus handling, input-triggered relight, and the `brightnessctl` binary path. ]] local config = { - timeout_ms = 3000, + timeout_ms = 10000, restore_on_pause = true, restore_on_unfocus = true, minimum_brightness = 0, led_path = "", debounce_ms = 250, brightnessctl_path = "brightnessctl", + relight_on_input = true, } -- No edits are required below unless you want to change script behavior. @@ -48,11 +49,44 @@ local function coerce_boolean(value, default) return default end -config.timeout_ms = tonumber(config.timeout_ms) or 3000 +config.timeout_ms = tonumber(config.timeout_ms) or 10000 config.minimum_brightness = math.max(0, math.floor(tonumber(config.minimum_brightness) or 0)) config.debounce_ms = math.max(0, tonumber(config.debounce_ms) or 250) config.restore_on_pause = coerce_boolean(config.restore_on_pause, true) config.restore_on_unfocus = coerce_boolean(config.restore_on_unfocus, true) +config.relight_on_input = coerce_boolean(config.relight_on_input, true) + +local INPUT_ACTIVITY_KEYS = { + "ANY_UNICODE", + "SPACE", + "ENTER", + "KP_ENTER", + "BS", + "TAB", + "ESC", + "UP", + "DOWN", + "LEFT", + "RIGHT", + "PGUP", + "PGDWN", + "HOME", + "END", + "INS", + "DEL", +} + +for i = 1, 12 do + table.insert(INPUT_ACTIVITY_KEYS, "F" .. tostring(i)) +end + +for i = 0, 9 do + table.insert(INPUT_ACTIVITY_KEYS, "KP" .. tostring(i)) +end + +for _, extra in ipairs({ "KP_DIVIDE", "KP_MULTIPLY", "KP_SUBTRACT", "KP_ADD" }) do + table.insert(INPUT_ACTIVITY_KEYS, extra) +end local function seconds(ms) return (tonumber(ms) or 0) / 1000 @@ -72,6 +106,8 @@ local state = { first_frame_seen = false, pending_timer = false, window_focused = true, + input_bindings_registered = false, + forwarding_key_event = false, } local function detect_keyboard_device() @@ -349,6 +385,96 @@ local function restore_backlight(reason) state.original_brightness = nil end +local function event_key_identifier(event) + if not event then + return nil + end + if event.key_name and event.key_name ~= "" then + return event.key_name + end + if event.key_text and event.key_text ~= "" then + return event.key_text + end + return nil +end + +local function forward_key_event(event) + if not event or state.forwarding_key_event then + return + end + + local key_name = event_key_identifier(event) + if not key_name then + return + end + + state.forwarding_key_event = true + local ok, err + if event.event == "down" then + ok, err = pcall(mp.commandv, "keydown", key_name) + elseif event.event == "up" then + ok, err = pcall(mp.commandv, "keyup", key_name) + else + if event.scale and event.scale ~= 1 then + ok, err = pcall(mp.commandv, "keypress", key_name, tostring(event.scale)) + else + ok, err = pcall(mp.commandv, "keypress", key_name) + end + end + state.forwarding_key_event = false + if not ok then + msg.error(("Failed to forward key '%s': %s"):format(key_name, err or "unknown")) + end +end + +local function handle_user_activity(source) + if state.control_blocked then + return + end + + if state.is_dark then + restore_backlight(source or "input") + end + + if not state.playback_active then + return + end + + if mp.get_property_native("pause") then + return + end + + if state.pending_timer and not state.first_frame_seen then + return + end + + schedule_timer() +end + +local function on_key_activity(event) + if state.forwarding_key_event then + return + end + + if config.relight_on_input and event and event.event ~= "up" then + handle_user_activity("key-input") + end + + forward_key_event(event) +end + +local function register_input_activity_bindings() + if state.input_bindings_registered then + return + end + state.input_bindings_registered = true + + for index, key in ipairs(INPUT_ACTIVITY_KEYS) do + local name = string.format("kb-blackout-activity-%d", index) + mp.add_forced_key_binding(key, name, on_key_activity, { complex = true }) + end +end + local function handle_play_event(source) if state.control_blocked or not probe_environment() then return @@ -460,6 +586,12 @@ local function on_focus(_, value) handle_focus_event("focus", value) end +if config.relight_on_input then + register_input_activity_bindings() +else + msg.verbose("relight_on_input disabled; skipping key activity bindings.") +end + mp.observe_property("pause", "bool", on_pause) mp.observe_property("playback-time", "native", on_playback_time) mp.observe_property("filename", "string", on_filename)