refactor: improve robustness and consistency across all scripts
- kb-blackout: optimize trim(), fix log units, add jit fallback - ac-power: extract error handling helper, add capture_stderr, validate timeout, add security note, rename CONFIG to config - touch-gestures: add min_brightness config, optimize brightnessctl to single call, improve wpctl regex, add shutdown handler
This commit is contained in:
@@ -2,10 +2,12 @@
|
||||
touch-gestures.lua - tablet-friendly swipe controls for mpv.
|
||||
|
||||
Features:
|
||||
* Horizontal swipes anywhere seek forward/backward.
|
||||
* Vertical swipes on the right edge raise/lower system volume; swipes on the left edge adjust screen brightness via brightnessctl.
|
||||
* Gesture axis is locked on first significant movement, preventing accidental cross-axis actions.
|
||||
* Prefers PipeWire (`wpctl`) for volume, falls back to PulseAudio (`pactl`) automatically.
|
||||
* Configurable gesture zones, pixels-per-step sensitivity, per-control step sizes, and optional inverted direction.
|
||||
* Shows OSD feedback after each adjustment so you know the new volume/brightness values.
|
||||
* Shows OSD feedback after each adjustment.
|
||||
|
||||
Configuration:
|
||||
Edit the `config` table below to tune zone widths, step sizes, and OSD duration; script-opts files are ignored.
|
||||
@@ -17,10 +19,14 @@ local config = {
|
||||
deadzone_px = 4, -- ignore tiny jiggles before the first step (lower = snappier)
|
||||
volume_step = 5, -- percent per step
|
||||
brightness_step = 5, -- screen brightness percent per step
|
||||
seek_step = 5, -- seconds per seek step
|
||||
max_volume = 130, -- clamp volume to this ceiling
|
||||
invert_vertical = false, -- true flips the swipe direction
|
||||
min_brightness = 1, -- minimum brightness percent (avoid turning off screen)
|
||||
invert_vertical = false, -- true flips the vertical swipe direction
|
||||
invert_horizontal = false, -- true flips the horizontal swipe direction
|
||||
left_zone_ratio = 0.35, -- fraction of width reserved for brightness (left side)
|
||||
right_zone_ratio = 0.35, -- fraction of width reserved for volume (right side)
|
||||
top_exclude_ratio = 0, -- ignore top portion (e.g., title bar); 0 disables
|
||||
bottom_exclude_ratio = 0.15, -- ignore bottom portion (e.g., OSC area); 0 disables
|
||||
osd_duration = 1.2, -- seconds OSD stays visible; set 0 to disable
|
||||
log_debug = false, -- set true to log gesture details
|
||||
@@ -37,14 +43,17 @@ local mp = require("mp")
|
||||
local msg = require("mp.msg")
|
||||
local utils = require("mp.utils")
|
||||
|
||||
local show_osd
|
||||
|
||||
local state = {
|
||||
active = false,
|
||||
zone = nil,
|
||||
axis = nil, -- "horizontal" or "vertical", locked on first significant movement
|
||||
accum = 0,
|
||||
last_x = 0,
|
||||
last_y = 0,
|
||||
start_x = 0, -- initial touch position for axis detection
|
||||
start_y = 0,
|
||||
button_down = false, -- track if mouse button is held (actual touch vs cursor movement)
|
||||
forwarded_down = false, -- track if we forwarded a click to mpv (need to forward up too)
|
||||
volume_backend = nil, -- "wpctl" or "pactl"
|
||||
cached_volume_pct = nil,
|
||||
cached_brightness_pct = nil,
|
||||
@@ -118,10 +127,25 @@ local function brightness_args(extra)
|
||||
return args
|
||||
end
|
||||
|
||||
local function show_osd(label, value)
|
||||
if config.osd_duration > 0 then
|
||||
mp.osd_message(string.format("%s: %s", label, value), config.osd_duration)
|
||||
end
|
||||
end
|
||||
|
||||
local function current_brightness_percent()
|
||||
if state.cached_brightness_pct ~= nil then
|
||||
return state.cached_brightness_pct
|
||||
end
|
||||
local out = run_cmd(brightness_args({ "-m", "info" }))
|
||||
if out then
|
||||
local cur, max = out:match(",(%d+),(%d+),")
|
||||
local c, m = tonumber(cur), tonumber(max)
|
||||
if c and m and m > 0 then
|
||||
state.cached_brightness_pct = clamp((c / m) * 100, 0, 100)
|
||||
return state.cached_brightness_pct
|
||||
end
|
||||
end
|
||||
local cur = run_cmd(brightness_args({ "get" }))
|
||||
local max = run_cmd(brightness_args({ "max" }))
|
||||
if not cur or not max then
|
||||
@@ -137,7 +161,7 @@ local function current_brightness_percent()
|
||||
end
|
||||
|
||||
local function set_brightness_percent(pct)
|
||||
local target = clamp(pct, 0, 100)
|
||||
local target = clamp(pct, config.min_brightness, 100)
|
||||
local ok = run_cmd(brightness_args({ "set", string.format("%.2f%%", target) }))
|
||||
if ok then
|
||||
state.cached_brightness_pct = target
|
||||
@@ -155,7 +179,7 @@ local function current_volume_percent()
|
||||
if not out then
|
||||
return nil
|
||||
end
|
||||
local val = out:match("(%d?%.?%d+)")
|
||||
local val = out:match("Volume:%s*([%d%.]+)") or out:match("([%d%.]+)")
|
||||
val = val and tonumber(val)
|
||||
if not val then
|
||||
return nil
|
||||
@@ -219,10 +243,18 @@ local function pick_zone(x, y, width, height)
|
||||
if width <= 0 then
|
||||
return nil
|
||||
end
|
||||
if height and height > 0 and config.bottom_exclude_ratio > 0 then
|
||||
local cutoff = height * (1 - config.bottom_exclude_ratio)
|
||||
if y >= cutoff then
|
||||
return nil
|
||||
if height and height > 0 then
|
||||
if config.top_exclude_ratio > 0 then
|
||||
local top_cutoff = height * config.top_exclude_ratio
|
||||
if y <= top_cutoff then
|
||||
return nil
|
||||
end
|
||||
end
|
||||
if config.bottom_exclude_ratio > 0 then
|
||||
local bottom_cutoff = height * (1 - config.bottom_exclude_ratio)
|
||||
if y >= bottom_cutoff then
|
||||
return nil
|
||||
end
|
||||
end
|
||||
end
|
||||
local left_limit = width * config.left_zone_ratio
|
||||
@@ -236,12 +268,6 @@ local function pick_zone(x, y, width, height)
|
||||
return nil
|
||||
end
|
||||
|
||||
function show_osd(label, value)
|
||||
if config.osd_duration > 0 then
|
||||
mp.osd_message(string.format("%s: %s", label, value), config.osd_duration)
|
||||
end
|
||||
end
|
||||
|
||||
local function apply_steps(kind, steps)
|
||||
if steps == 0 then
|
||||
return
|
||||
@@ -260,6 +286,10 @@ local function apply_steps(kind, steps)
|
||||
end
|
||||
local next_value = current + (steps * config.brightness_step)
|
||||
set_brightness_percent(next_value)
|
||||
elseif kind == "seek" then
|
||||
local seconds = steps * config.seek_step
|
||||
mp.commandv("seek", seconds, "relative")
|
||||
show_osd("Seek", string.format("%+ds", seconds))
|
||||
end
|
||||
end
|
||||
|
||||
@@ -269,18 +299,33 @@ local function reset_state()
|
||||
end
|
||||
state.active = false
|
||||
state.zone = nil
|
||||
state.axis = nil
|
||||
state.accum = 0
|
||||
state.last_x = 0
|
||||
state.last_y = 0
|
||||
state.start_x = 0
|
||||
state.start_y = 0
|
||||
end
|
||||
|
||||
local function start_gesture(pos, zone)
|
||||
local function begin_gesture_tracking(pos)
|
||||
state.active = true
|
||||
state.zone = zone
|
||||
state.zone = nil
|
||||
state.axis = nil
|
||||
state.accum = 0
|
||||
state.last_x = pos.x
|
||||
state.last_y = pos.y
|
||||
dbg(string.format("start gesture zone=%s at (%.0f,%.0f)", zone, pos.x, pos.y))
|
||||
state.start_x = pos.x
|
||||
state.start_y = pos.y
|
||||
state.cached_volume_pct = nil
|
||||
state.cached_brightness_pct = nil
|
||||
dbg(string.format("begin tracking at (%.0f,%.0f)", pos.x, pos.y))
|
||||
end
|
||||
|
||||
local function lock_axis(axis, zone)
|
||||
state.axis = axis
|
||||
state.zone = zone
|
||||
state.accum = 0
|
||||
dbg(string.format("locked axis=%s zone=%s", axis, tostring(zone)))
|
||||
end
|
||||
|
||||
local function update_gesture(_, pos)
|
||||
@@ -288,48 +333,62 @@ local function update_gesture(_, pos)
|
||||
return
|
||||
end
|
||||
|
||||
if not state.active and config.start_on_move then
|
||||
local width, height = surface_size()
|
||||
local zone = pick_zone(pos.x, pos.y, width, height)
|
||||
if zone then
|
||||
start_gesture(pos, zone)
|
||||
else
|
||||
return
|
||||
end
|
||||
if not state.active and config.start_on_move and state.button_down then
|
||||
begin_gesture_tracking(pos)
|
||||
end
|
||||
|
||||
if not state.active then
|
||||
return
|
||||
end
|
||||
|
||||
local dx = pos.x - state.last_x
|
||||
local dy = pos.y - state.last_y
|
||||
if config.invert_vertical then
|
||||
dy = -dy
|
||||
end
|
||||
|
||||
state.last_x = pos.x
|
||||
state.last_y = pos.y
|
||||
|
||||
if math.abs(state.accum) < config.deadzone_px then
|
||||
state.accum = state.accum + dy
|
||||
if math.abs(state.accum) < config.deadzone_px then
|
||||
if not state.axis then
|
||||
local total_dx = math.abs(pos.x - state.start_x)
|
||||
local total_dy = math.abs(pos.y - state.start_y)
|
||||
|
||||
if total_dx >= config.deadzone_px or total_dy >= config.deadzone_px then
|
||||
if total_dx > total_dy then
|
||||
lock_axis("horizontal", "seek")
|
||||
else
|
||||
local width, height = surface_size()
|
||||
local zone = pick_zone(state.start_x, state.start_y, width, height)
|
||||
if zone then
|
||||
lock_axis("vertical", zone)
|
||||
else
|
||||
reset_state()
|
||||
return
|
||||
end
|
||||
end
|
||||
else
|
||||
return
|
||||
end
|
||||
-- once deadzone crossed, drop the deadzone offset so first step is clean
|
||||
if state.accum > 0 then
|
||||
state.accum = state.accum - config.deadzone_px
|
||||
else
|
||||
state.accum = state.accum + config.deadzone_px
|
||||
end
|
||||
|
||||
local delta
|
||||
if state.axis == "horizontal" then
|
||||
delta = dx
|
||||
if config.invert_horizontal then
|
||||
delta = -delta
|
||||
end
|
||||
else
|
||||
state.accum = state.accum + dy
|
||||
delta = dy
|
||||
if config.invert_vertical then
|
||||
delta = -delta
|
||||
end
|
||||
end
|
||||
|
||||
state.accum = state.accum + delta
|
||||
|
||||
local step_pixels = config.pixels_per_step
|
||||
if config.steps_per_screen and config.steps_per_screen > 0 then
|
||||
local _, surf_h = surface_size()
|
||||
if surf_h and surf_h > 0 then
|
||||
step_pixels = surf_h / config.steps_per_screen
|
||||
local surf_w, surf_h = surface_size()
|
||||
local ref_size = state.axis == "horizontal" and surf_w or surf_h
|
||||
if ref_size and ref_size > 0 then
|
||||
step_pixels = ref_size / config.steps_per_screen
|
||||
end
|
||||
end
|
||||
if step_pixels <= 0 then
|
||||
@@ -341,40 +400,86 @@ local function update_gesture(_, pos)
|
||||
|
||||
if whole_steps ~= 0 then
|
||||
dbg(
|
||||
string.format("zone=%s dy=%.2f accum=%.2f steps=%d", state.zone, dy, state.accum, whole_steps)
|
||||
string.format(
|
||||
"zone=%s axis=%s delta=%.2f accum=%.2f steps=%d",
|
||||
state.zone,
|
||||
state.axis,
|
||||
delta,
|
||||
state.accum,
|
||||
whole_steps
|
||||
)
|
||||
)
|
||||
apply_steps(state.zone, -whole_steps)
|
||||
local sign = state.axis == "horizontal" and 1 or -1
|
||||
apply_steps(state.zone, sign * whole_steps)
|
||||
state.accum = state.accum - (whole_steps * step_pixels)
|
||||
end
|
||||
end
|
||||
|
||||
local forwarding_click = false
|
||||
|
||||
local function in_excluded_zone(pos)
|
||||
local _, height = surface_size()
|
||||
if not height or height <= 0 then
|
||||
return false
|
||||
end
|
||||
if config.top_exclude_ratio > 0 then
|
||||
local top_cutoff = height * config.top_exclude_ratio
|
||||
if pos.y <= top_cutoff then
|
||||
return true
|
||||
end
|
||||
end
|
||||
if config.bottom_exclude_ratio > 0 then
|
||||
local bottom_cutoff = height * (1 - config.bottom_exclude_ratio)
|
||||
if pos.y >= bottom_cutoff then
|
||||
return true
|
||||
end
|
||||
end
|
||||
return false
|
||||
end
|
||||
|
||||
local function handle_touch(event)
|
||||
if forwarding_click then
|
||||
return
|
||||
end
|
||||
|
||||
if event.event == "down" then
|
||||
state.button_down = true
|
||||
state.forwarded_down = false
|
||||
local pos = mp.get_property_native("mouse-pos")
|
||||
if not pos or not pos.x or not pos.y then
|
||||
return
|
||||
end
|
||||
|
||||
local width, height = surface_size()
|
||||
local zone = pick_zone(pos.x, pos.y, width, height)
|
||||
if not zone then
|
||||
if in_excluded_zone(pos) then
|
||||
state.forwarded_down = true
|
||||
forwarding_click = true
|
||||
mp.commandv("keypress", "mouse_btn0")
|
||||
mp.commandv("keydown", "mouse_btn0")
|
||||
forwarding_click = false
|
||||
return
|
||||
end
|
||||
|
||||
start_gesture(pos, zone)
|
||||
begin_gesture_tracking(pos)
|
||||
elseif event.event == "up" then
|
||||
if state.forwarded_down then
|
||||
forwarding_click = true
|
||||
mp.commandv("keyup", "mouse_btn0")
|
||||
forwarding_click = false
|
||||
elseif state.active and not state.axis then
|
||||
forwarding_click = true
|
||||
mp.commandv("keypress", "mouse_btn0")
|
||||
forwarding_click = false
|
||||
end
|
||||
state.button_down = false
|
||||
state.forwarded_down = false
|
||||
reset_state()
|
||||
end
|
||||
end
|
||||
|
||||
mp.add_forced_key_binding("mouse_btn0", "touch-gestures", handle_touch, { complex = true })
|
||||
mp.observe_property("mouse-pos", "native", update_gesture)
|
||||
|
||||
mp.register_event("shutdown", function()
|
||||
reset_state()
|
||||
end)
|
||||
|
||||
pick_volume_backend()
|
||||
|
||||
Reference in New Issue
Block a user