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:
@@ -7,10 +7,14 @@
|
|||||||
* Survives flaky networks with capped exponential backoff, optional HUD error messages, and redraws of the last known value after OSD clears.
|
* Survives flaky networks with capped exponential backoff, optional HUD error messages, and redraws of the last known value after OSD clears.
|
||||||
|
|
||||||
Configuration:
|
Configuration:
|
||||||
Edit the CONFIG table below to match your Home Assistant deployment; the script ignores script-opts files.
|
Edit the config table below to match your Home Assistant deployment; the script ignores script-opts files.
|
||||||
|
|
||||||
|
Security:
|
||||||
|
The ha_token is a long-lived access token. Avoid committing it to version control.
|
||||||
|
Consider storing this script outside of shared repositories or using a separate config file.
|
||||||
]]
|
]]
|
||||||
|
|
||||||
local CONFIG = {
|
local config = {
|
||||||
sensor_id = "sensor.ac_power", -- Home Assistant entity id
|
sensor_id = "sensor.ac_power", -- Home Assistant entity id
|
||||||
ha_base_url = "http://homeassistant.local:8123",
|
ha_base_url = "http://homeassistant.local:8123",
|
||||||
ha_token = "REPLACE_WITH_LONG_LIVED_TOKEN",
|
ha_token = "REPLACE_WITH_LONG_LIVED_TOKEN",
|
||||||
@@ -60,58 +64,60 @@ end
|
|||||||
local function build_url()
|
local function build_url()
|
||||||
return string.format(
|
return string.format(
|
||||||
"%s/api/states/%s",
|
"%s/api/states/%s",
|
||||||
trim_trailing_slash(CONFIG.ha_base_url),
|
trim_trailing_slash(config.ha_base_url),
|
||||||
CONFIG.sensor_id
|
config.sensor_id
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
local function build_request()
|
local function build_request()
|
||||||
local args = { CONFIG.curl_binary, "-sS", "-m", tostring(CONFIG.http_timeout) }
|
local timeout = tonumber(config.http_timeout) or 2
|
||||||
if CONFIG.insecure_ssl then
|
local args = { config.curl_binary, "-sS", "-m", tostring(timeout) }
|
||||||
|
if config.insecure_ssl then
|
||||||
table.insert(args, "-k")
|
table.insert(args, "-k")
|
||||||
end
|
end
|
||||||
table.insert(args, "-H")
|
table.insert(args, "-H")
|
||||||
table.insert(args, "Authorization: Bearer " .. CONFIG.ha_token)
|
table.insert(args, "Authorization: Bearer " .. config.ha_token)
|
||||||
table.insert(args, build_url())
|
table.insert(args, build_url())
|
||||||
return {
|
return {
|
||||||
name = "subprocess",
|
name = "subprocess",
|
||||||
playback_only = false,
|
playback_only = false,
|
||||||
capture_stdout = true,
|
capture_stdout = true,
|
||||||
|
capture_stderr = true,
|
||||||
args = args,
|
args = args,
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
local function format_value(value)
|
local function format_value(value)
|
||||||
local fmt = "%0." .. tostring(CONFIG.decimal_places) .. "f"
|
local fmt = "%0." .. tostring(config.decimal_places) .. "f"
|
||||||
return string.format(fmt, value)
|
return string.format(fmt, value)
|
||||||
end
|
end
|
||||||
|
|
||||||
local function sensor_units(payload)
|
local function sensor_units(payload)
|
||||||
if CONFIG.use_sensor_unit and payload and payload.attributes then
|
if config.use_sensor_unit and payload and payload.attributes then
|
||||||
local unit = payload.attributes.unit_of_measurement
|
local unit = payload.attributes.unit_of_measurement
|
||||||
if unit and unit ~= "" then
|
if unit and unit ~= "" then
|
||||||
return unit
|
return unit
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
return CONFIG.units_label
|
return config.units_label
|
||||||
end
|
end
|
||||||
|
|
||||||
local function show_message(text, duration)
|
local function show_message(text, duration)
|
||||||
local timeout = duration
|
local timeout = duration
|
||||||
if timeout == nil then
|
if timeout == nil then
|
||||||
timeout = CONFIG.display_timeout
|
timeout = config.display_timeout
|
||||||
end
|
end
|
||||||
mp.osd_message(text, timeout)
|
mp.osd_message(text, timeout)
|
||||||
last_display_ts = mp.get_time()
|
last_display_ts = mp.get_time()
|
||||||
end
|
end
|
||||||
|
|
||||||
local function show_value(value_text, unit)
|
local function show_value(value_text, unit)
|
||||||
show_message(string.format("%s: %s %s", CONFIG.display_label, value_text, unit or ""))
|
show_message(string.format("%s: %s %s", config.display_label, value_text, unit or ""))
|
||||||
end
|
end
|
||||||
|
|
||||||
local function show_error(err)
|
local function show_error(err)
|
||||||
if CONFIG.show_errors then
|
if config.show_errors then
|
||||||
show_message(string.format("%s: -- (%s)", CONFIG.display_label, err))
|
show_message(string.format("%s: -- (%s)", config.display_label, err))
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -125,6 +131,20 @@ local function ensure_poll_timer(delay, fn)
|
|||||||
end)
|
end)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
local poll_sensor
|
||||||
|
|
||||||
|
local function schedule_retry()
|
||||||
|
backoff_failures = backoff_failures + 1
|
||||||
|
local next_delay = math.min(config.max_backoff, config.poll_interval * (2 ^ backoff_failures))
|
||||||
|
ensure_poll_timer(next_delay, poll_sensor)
|
||||||
|
end
|
||||||
|
|
||||||
|
local function handle_poll_error(message)
|
||||||
|
log("error", message)
|
||||||
|
show_error(message)
|
||||||
|
schedule_retry()
|
||||||
|
end
|
||||||
|
|
||||||
local function meets_delta_threshold(value)
|
local function meets_delta_threshold(value)
|
||||||
if not value then
|
if not value then
|
||||||
return false
|
return false
|
||||||
@@ -134,11 +154,11 @@ local function meets_delta_threshold(value)
|
|||||||
end
|
end
|
||||||
|
|
||||||
local delta = math.abs(value - last_watts)
|
local delta = math.abs(value - last_watts)
|
||||||
if delta >= CONFIG.min_delta_watts then
|
if delta >= config.min_delta_watts then
|
||||||
return true
|
return true
|
||||||
end
|
end
|
||||||
|
|
||||||
if CONFIG.min_delta_percent > 0 then
|
if config.min_delta_percent > 0 then
|
||||||
local baseline = math.abs(last_watts)
|
local baseline = math.abs(last_watts)
|
||||||
if baseline < 1e-6 then
|
if baseline < 1e-6 then
|
||||||
baseline = math.abs(value)
|
baseline = math.abs(value)
|
||||||
@@ -147,7 +167,7 @@ local function meets_delta_threshold(value)
|
|||||||
baseline = 1
|
baseline = 1
|
||||||
end
|
end
|
||||||
local percent_delta = (delta / baseline) * 100
|
local percent_delta = (delta / baseline) * 100
|
||||||
if percent_delta >= CONFIG.min_delta_percent then
|
if percent_delta >= config.min_delta_percent then
|
||||||
return true
|
return true
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -156,7 +176,7 @@ local function meets_delta_threshold(value)
|
|||||||
end
|
end
|
||||||
|
|
||||||
local function thresholds_enabled()
|
local function thresholds_enabled()
|
||||||
return (CONFIG.min_delta_watts or 0) > 0 or (CONFIG.min_delta_percent or 0) > 0
|
return (config.min_delta_watts or 0) > 0 or (config.min_delta_percent or 0) > 0
|
||||||
end
|
end
|
||||||
|
|
||||||
local function handle_success(value, payload)
|
local function handle_success(value, payload)
|
||||||
@@ -164,14 +184,14 @@ local function handle_success(value, payload)
|
|||||||
local now = mp.get_time()
|
local now = mp.get_time()
|
||||||
local printable = value and format_value(value) or "--"
|
local printable = value and format_value(value) or "--"
|
||||||
local unit = sensor_units(payload)
|
local unit = sensor_units(payload)
|
||||||
local force_due = CONFIG.force_refresh_interval > 0
|
local force_due = config.force_refresh_interval > 0
|
||||||
and (now - last_display_ts) >= CONFIG.force_refresh_interval
|
and (now - last_display_ts) >= config.force_refresh_interval
|
||||||
local significant_change = meets_delta_threshold(value)
|
local significant_change = meets_delta_threshold(value)
|
||||||
|
|
||||||
local should_render = significant_change or last_watts == nil
|
local should_render = significant_change or last_watts == nil
|
||||||
if not should_render and force_due then
|
if not should_render and force_due then
|
||||||
local allow_force = true
|
local allow_force = true
|
||||||
if thresholds_enabled() and CONFIG.force_refresh_obeys_threshold then
|
if thresholds_enabled() and config.force_refresh_obeys_threshold then
|
||||||
allow_force = false
|
allow_force = false
|
||||||
end
|
end
|
||||||
if allow_force then
|
if allow_force then
|
||||||
@@ -191,15 +211,15 @@ local function redraw_last_value()
|
|||||||
show_value(format_value(last_watts), last_unit)
|
show_value(format_value(last_watts), last_unit)
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
if CONFIG.initial_message and CONFIG.initial_message ~= "" then
|
if config.initial_message and config.initial_message ~= "" then
|
||||||
show_message(
|
show_message(
|
||||||
string.format("%s: %s", CONFIG.display_label, CONFIG.initial_message),
|
string.format("%s: %s", config.display_label, config.initial_message),
|
||||||
CONFIG.initial_message_duration
|
config.initial_message_duration
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
local function poll_sensor()
|
poll_sensor = function()
|
||||||
if request_inflight then
|
if request_inflight then
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
@@ -209,42 +229,25 @@ local function poll_sensor()
|
|||||||
request_inflight = false
|
request_inflight = false
|
||||||
|
|
||||||
if not success or not result then
|
if not success or not result then
|
||||||
backoff_failures = backoff_failures + 1
|
handle_poll_error(err or "request failed")
|
||||||
local message = err or "request failed"
|
|
||||||
log("error", message)
|
|
||||||
show_error(message)
|
|
||||||
local next_delay = math.min(CONFIG.max_backoff, CONFIG.poll_interval * (2 ^ backoff_failures))
|
|
||||||
ensure_poll_timer(next_delay, poll_sensor)
|
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
|
|
||||||
if result.status ~= 0 then
|
if result.status ~= 0 then
|
||||||
backoff_failures = backoff_failures + 1
|
|
||||||
local stderr = (result.stderr and result.stderr ~= "" and result.stderr)
|
local stderr = (result.stderr and result.stderr ~= "" and result.stderr)
|
||||||
or ("curl exit " .. tostring(result.status))
|
or ("curl exit " .. tostring(result.status))
|
||||||
log("error", stderr)
|
handle_poll_error(stderr)
|
||||||
show_error(stderr)
|
|
||||||
local next_delay = math.min(CONFIG.max_backoff, CONFIG.poll_interval * (2 ^ backoff_failures))
|
|
||||||
ensure_poll_timer(next_delay, poll_sensor)
|
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
|
|
||||||
if not result.stdout or result.stdout == "" then
|
if not result.stdout or result.stdout == "" then
|
||||||
backoff_failures = backoff_failures + 1
|
handle_poll_error("empty response from Home Assistant")
|
||||||
log("error", "empty response from Home Assistant")
|
|
||||||
show_error("empty response")
|
|
||||||
local next_delay = math.min(CONFIG.max_backoff, CONFIG.poll_interval * (2 ^ backoff_failures))
|
|
||||||
ensure_poll_timer(next_delay, poll_sensor)
|
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
|
|
||||||
local payload = utils.parse_json(result.stdout)
|
local payload = utils.parse_json(result.stdout)
|
||||||
if not payload then
|
if not payload then
|
||||||
backoff_failures = backoff_failures + 1
|
handle_poll_error("invalid JSON payload")
|
||||||
log("error", "invalid JSON payload")
|
|
||||||
show_error("invalid JSON")
|
|
||||||
local next_delay = math.min(CONFIG.max_backoff, CONFIG.poll_interval * (2 ^ backoff_failures))
|
|
||||||
ensure_poll_timer(next_delay, poll_sensor)
|
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -257,22 +260,22 @@ local function poll_sensor()
|
|||||||
end
|
end
|
||||||
|
|
||||||
backoff_failures = 0
|
backoff_failures = 0
|
||||||
ensure_poll_timer(CONFIG.poll_interval, poll_sensor)
|
ensure_poll_timer(config.poll_interval, poll_sensor)
|
||||||
end)
|
end)
|
||||||
end
|
end
|
||||||
|
|
||||||
local function validate_config()
|
local function validate_config()
|
||||||
if CONFIG.ha_base_url == "" then
|
if config.ha_base_url == "" then
|
||||||
log("error", "ha_base_url is required")
|
log("error", "ha_base_url is required")
|
||||||
show_error("missing base url")
|
show_error("missing base url")
|
||||||
return false
|
return false
|
||||||
end
|
end
|
||||||
if CONFIG.sensor_id == "" then
|
if config.sensor_id == "" then
|
||||||
log("error", "sensor_id is required")
|
log("error", "sensor_id is required")
|
||||||
show_error("missing sensor id")
|
show_error("missing sensor id")
|
||||||
return false
|
return false
|
||||||
end
|
end
|
||||||
if CONFIG.ha_token == "" or CONFIG.ha_token == "REPLACE_WITH_LONG_LIVED_TOKEN" then
|
if config.ha_token == "" or config.ha_token == "REPLACE_WITH_LONG_LIVED_TOKEN" then
|
||||||
log("error", "ha_token must be set to a Home Assistant long-lived token")
|
log("error", "ha_token must be set to a Home Assistant long-lived token")
|
||||||
show_error("missing HA token")
|
show_error("missing HA token")
|
||||||
return false
|
return false
|
||||||
@@ -284,10 +287,10 @@ local function start()
|
|||||||
if not validate_config() then
|
if not validate_config() then
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
if CONFIG.initial_message and CONFIG.initial_message ~= "" then
|
if config.initial_message and config.initial_message ~= "" then
|
||||||
show_message(
|
show_message(
|
||||||
string.format("%s: %s", CONFIG.display_label, CONFIG.initial_message),
|
string.format("%s: %s", config.display_label, config.initial_message),
|
||||||
CONFIG.initial_message_duration
|
config.initial_message_duration
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
ensure_poll_timer(0.1, poll_sensor)
|
ensure_poll_timer(0.1, poll_sensor)
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ local config = {
|
|||||||
local mp = require("mp")
|
local mp = require("mp")
|
||||||
local msg = require("mp.msg")
|
local msg = require("mp.msg")
|
||||||
local utils = require("mp.utils")
|
local utils = require("mp.utils")
|
||||||
local jit = rawget(_G, "jit")
|
local jit = rawget(_G, "jit") or {}
|
||||||
|
|
||||||
local function coerce_boolean(value, default)
|
local function coerce_boolean(value, default)
|
||||||
if type(value) == "boolean" then
|
if type(value) == "boolean" then
|
||||||
@@ -137,7 +137,7 @@ if unsupported_platform() then
|
|||||||
end
|
end
|
||||||
|
|
||||||
local function trim(str)
|
local function trim(str)
|
||||||
return (str and str:gsub("^%s+", ""):gsub("%s+$", "")) or ""
|
return str and str:match("^%s*(.-)%s*$") or ""
|
||||||
end
|
end
|
||||||
|
|
||||||
local function build_brightnessctl_args(extra)
|
local function build_brightnessctl_args(extra)
|
||||||
@@ -315,8 +315,9 @@ end
|
|||||||
|
|
||||||
local function schedule_timer()
|
local function schedule_timer()
|
||||||
cancel_timer()
|
cancel_timer()
|
||||||
state.timer = mp.add_timeout(seconds(config.timeout_ms), arm_disable_callback)
|
local delay = seconds(config.timeout_ms)
|
||||||
msg.verbose(("Inactivity timer armed for %.0f ms"):format(config.timeout_ms))
|
state.timer = mp.add_timeout(delay, arm_disable_callback)
|
||||||
|
msg.verbose(("Inactivity timer armed for %.1f s"):format(delay))
|
||||||
end
|
end
|
||||||
|
|
||||||
local function restore_backlight(reason)
|
local function restore_backlight(reason)
|
||||||
|
|||||||
@@ -2,10 +2,12 @@
|
|||||||
touch-gestures.lua - tablet-friendly swipe controls for mpv.
|
touch-gestures.lua - tablet-friendly swipe controls for mpv.
|
||||||
|
|
||||||
Features:
|
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.
|
* 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.
|
* 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.
|
* 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:
|
Configuration:
|
||||||
Edit the `config` table below to tune zone widths, step sizes, and OSD duration; script-opts files are ignored.
|
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)
|
deadzone_px = 4, -- ignore tiny jiggles before the first step (lower = snappier)
|
||||||
volume_step = 5, -- percent per step
|
volume_step = 5, -- percent per step
|
||||||
brightness_step = 5, -- screen brightness 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
|
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)
|
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)
|
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
|
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
|
osd_duration = 1.2, -- seconds OSD stays visible; set 0 to disable
|
||||||
log_debug = false, -- set true to log gesture details
|
log_debug = false, -- set true to log gesture details
|
||||||
@@ -37,14 +43,17 @@ local mp = require("mp")
|
|||||||
local msg = require("mp.msg")
|
local msg = require("mp.msg")
|
||||||
local utils = require("mp.utils")
|
local utils = require("mp.utils")
|
||||||
|
|
||||||
local show_osd
|
|
||||||
|
|
||||||
local state = {
|
local state = {
|
||||||
active = false,
|
active = false,
|
||||||
zone = nil,
|
zone = nil,
|
||||||
|
axis = nil, -- "horizontal" or "vertical", locked on first significant movement
|
||||||
accum = 0,
|
accum = 0,
|
||||||
last_x = 0,
|
last_x = 0,
|
||||||
last_y = 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"
|
volume_backend = nil, -- "wpctl" or "pactl"
|
||||||
cached_volume_pct = nil,
|
cached_volume_pct = nil,
|
||||||
cached_brightness_pct = nil,
|
cached_brightness_pct = nil,
|
||||||
@@ -118,10 +127,25 @@ local function brightness_args(extra)
|
|||||||
return args
|
return args
|
||||||
end
|
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()
|
local function current_brightness_percent()
|
||||||
if state.cached_brightness_pct ~= nil then
|
if state.cached_brightness_pct ~= nil then
|
||||||
return state.cached_brightness_pct
|
return state.cached_brightness_pct
|
||||||
end
|
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 cur = run_cmd(brightness_args({ "get" }))
|
||||||
local max = run_cmd(brightness_args({ "max" }))
|
local max = run_cmd(brightness_args({ "max" }))
|
||||||
if not cur or not max then
|
if not cur or not max then
|
||||||
@@ -137,7 +161,7 @@ local function current_brightness_percent()
|
|||||||
end
|
end
|
||||||
|
|
||||||
local function set_brightness_percent(pct)
|
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) }))
|
local ok = run_cmd(brightness_args({ "set", string.format("%.2f%%", target) }))
|
||||||
if ok then
|
if ok then
|
||||||
state.cached_brightness_pct = target
|
state.cached_brightness_pct = target
|
||||||
@@ -155,7 +179,7 @@ local function current_volume_percent()
|
|||||||
if not out then
|
if not out then
|
||||||
return nil
|
return nil
|
||||||
end
|
end
|
||||||
local val = out:match("(%d?%.?%d+)")
|
local val = out:match("Volume:%s*([%d%.]+)") or out:match("([%d%.]+)")
|
||||||
val = val and tonumber(val)
|
val = val and tonumber(val)
|
||||||
if not val then
|
if not val then
|
||||||
return nil
|
return nil
|
||||||
@@ -219,10 +243,18 @@ local function pick_zone(x, y, width, height)
|
|||||||
if width <= 0 then
|
if width <= 0 then
|
||||||
return nil
|
return nil
|
||||||
end
|
end
|
||||||
if height and height > 0 and config.bottom_exclude_ratio > 0 then
|
if height and height > 0 then
|
||||||
local cutoff = height * (1 - config.bottom_exclude_ratio)
|
if config.top_exclude_ratio > 0 then
|
||||||
if y >= cutoff then
|
local top_cutoff = height * config.top_exclude_ratio
|
||||||
return nil
|
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
|
||||||
end
|
end
|
||||||
local left_limit = width * config.left_zone_ratio
|
local left_limit = width * config.left_zone_ratio
|
||||||
@@ -236,12 +268,6 @@ local function pick_zone(x, y, width, height)
|
|||||||
return nil
|
return nil
|
||||||
end
|
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)
|
local function apply_steps(kind, steps)
|
||||||
if steps == 0 then
|
if steps == 0 then
|
||||||
return
|
return
|
||||||
@@ -260,6 +286,10 @@ local function apply_steps(kind, steps)
|
|||||||
end
|
end
|
||||||
local next_value = current + (steps * config.brightness_step)
|
local next_value = current + (steps * config.brightness_step)
|
||||||
set_brightness_percent(next_value)
|
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
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -269,18 +299,33 @@ local function reset_state()
|
|||||||
end
|
end
|
||||||
state.active = false
|
state.active = false
|
||||||
state.zone = nil
|
state.zone = nil
|
||||||
|
state.axis = nil
|
||||||
state.accum = 0
|
state.accum = 0
|
||||||
state.last_x = 0
|
state.last_x = 0
|
||||||
state.last_y = 0
|
state.last_y = 0
|
||||||
|
state.start_x = 0
|
||||||
|
state.start_y = 0
|
||||||
end
|
end
|
||||||
|
|
||||||
local function start_gesture(pos, zone)
|
local function begin_gesture_tracking(pos)
|
||||||
state.active = true
|
state.active = true
|
||||||
state.zone = zone
|
state.zone = nil
|
||||||
|
state.axis = nil
|
||||||
state.accum = 0
|
state.accum = 0
|
||||||
state.last_x = pos.x
|
state.last_x = pos.x
|
||||||
state.last_y = pos.y
|
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
|
end
|
||||||
|
|
||||||
local function update_gesture(_, pos)
|
local function update_gesture(_, pos)
|
||||||
@@ -288,48 +333,62 @@ local function update_gesture(_, pos)
|
|||||||
return
|
return
|
||||||
end
|
end
|
||||||
|
|
||||||
if not state.active and config.start_on_move then
|
if not state.active and config.start_on_move and state.button_down then
|
||||||
local width, height = surface_size()
|
begin_gesture_tracking(pos)
|
||||||
local zone = pick_zone(pos.x, pos.y, width, height)
|
|
||||||
if zone then
|
|
||||||
start_gesture(pos, zone)
|
|
||||||
else
|
|
||||||
return
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
if not state.active then
|
if not state.active then
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
|
|
||||||
|
local dx = pos.x - state.last_x
|
||||||
local dy = pos.y - state.last_y
|
local dy = pos.y - state.last_y
|
||||||
if config.invert_vertical then
|
|
||||||
dy = -dy
|
|
||||||
end
|
|
||||||
|
|
||||||
state.last_x = pos.x
|
state.last_x = pos.x
|
||||||
state.last_y = pos.y
|
state.last_y = pos.y
|
||||||
|
|
||||||
if math.abs(state.accum) < config.deadzone_px then
|
if not state.axis then
|
||||||
state.accum = state.accum + dy
|
local total_dx = math.abs(pos.x - state.start_x)
|
||||||
if math.abs(state.accum) < config.deadzone_px then
|
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
|
return
|
||||||
end
|
end
|
||||||
-- once deadzone crossed, drop the deadzone offset so first step is clean
|
end
|
||||||
if state.accum > 0 then
|
|
||||||
state.accum = state.accum - config.deadzone_px
|
local delta
|
||||||
else
|
if state.axis == "horizontal" then
|
||||||
state.accum = state.accum + config.deadzone_px
|
delta = dx
|
||||||
|
if config.invert_horizontal then
|
||||||
|
delta = -delta
|
||||||
end
|
end
|
||||||
else
|
else
|
||||||
state.accum = state.accum + dy
|
delta = dy
|
||||||
|
if config.invert_vertical then
|
||||||
|
delta = -delta
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
state.accum = state.accum + delta
|
||||||
|
|
||||||
local step_pixels = config.pixels_per_step
|
local step_pixels = config.pixels_per_step
|
||||||
if config.steps_per_screen and config.steps_per_screen > 0 then
|
if config.steps_per_screen and config.steps_per_screen > 0 then
|
||||||
local _, surf_h = surface_size()
|
local surf_w, surf_h = surface_size()
|
||||||
if surf_h and surf_h > 0 then
|
local ref_size = state.axis == "horizontal" and surf_w or surf_h
|
||||||
step_pixels = surf_h / config.steps_per_screen
|
if ref_size and ref_size > 0 then
|
||||||
|
step_pixels = ref_size / config.steps_per_screen
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
if step_pixels <= 0 then
|
if step_pixels <= 0 then
|
||||||
@@ -341,40 +400,86 @@ local function update_gesture(_, pos)
|
|||||||
|
|
||||||
if whole_steps ~= 0 then
|
if whole_steps ~= 0 then
|
||||||
dbg(
|
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)
|
state.accum = state.accum - (whole_steps * step_pixels)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
local forwarding_click = false
|
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)
|
local function handle_touch(event)
|
||||||
if forwarding_click then
|
if forwarding_click then
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
|
|
||||||
if event.event == "down" then
|
if event.event == "down" then
|
||||||
|
state.button_down = true
|
||||||
|
state.forwarded_down = false
|
||||||
local pos = mp.get_property_native("mouse-pos")
|
local pos = mp.get_property_native("mouse-pos")
|
||||||
if not pos or not pos.x or not pos.y then
|
if not pos or not pos.x or not pos.y then
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
|
|
||||||
local width, height = surface_size()
|
if in_excluded_zone(pos) then
|
||||||
local zone = pick_zone(pos.x, pos.y, width, height)
|
state.forwarded_down = true
|
||||||
if not zone then
|
|
||||||
forwarding_click = true
|
forwarding_click = true
|
||||||
mp.commandv("keypress", "mouse_btn0")
|
mp.commandv("keydown", "mouse_btn0")
|
||||||
forwarding_click = false
|
forwarding_click = false
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
|
|
||||||
start_gesture(pos, zone)
|
begin_gesture_tracking(pos)
|
||||||
elseif event.event == "up" then
|
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()
|
reset_state()
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
mp.add_forced_key_binding("mouse_btn0", "touch-gestures", handle_touch, { complex = true })
|
mp.add_forced_key_binding("mouse_btn0", "touch-gestures", handle_touch, { complex = true })
|
||||||
mp.observe_property("mouse-pos", "native", update_gesture)
|
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