From b890a4efcd1564441c74159c3164b12b32068d08 Mon Sep 17 00:00:00 2001 From: Giovanni Harting <539@idlegandalf.com> Date: Thu, 18 Dec 2025 17:14:36 +0100 Subject: [PATCH] feat: add power gauge/delta/minmax to ac-power and extract shared utils MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ac-power enhancements: - Add visual block gauge [████░░░░] colored by power level - Show delta indicator (+50W) colored by trend (green=dropping, red=rising) - Track min/max values with persistence to ~/.cache/mpv/ - Multi-process safe file writes (merges values on save) - New script-message: ac-power-reset-minmax Shared library (scripts/lib/utils.lua): - Extract common helpers: trim, clamp, rgb_to_bgr, colorize - Update all scripts to use shared module via package.path --- scripts/ac-power.lua | 233 +++++++++++++++++++++++++++++++++++- scripts/kb-blackout.lua | 11 +- scripts/lib/utils.lua | 44 +++++++ scripts/playback-health.lua | 30 ++--- scripts/touch-gestures.lua | 25 ++-- 5 files changed, 298 insertions(+), 45 deletions(-) create mode 100644 scripts/lib/utils.lua diff --git a/scripts/ac-power.lua b/scripts/ac-power.lua index 9aa396c..e480148 100644 --- a/scripts/ac-power.lua +++ b/scripts/ac-power.lua @@ -35,6 +35,34 @@ local config = { show_errors = true, -- display API errors via HUD initial_message = "Fetching AC power…", -- shown until first reading initial_message_duration = 0, -- 0 keeps OSD up until refreshed + + -- Gauge display + show_gauge = true, + gauge_width = 8, -- number of bar segments + gauge_max_watts = 1000, -- fallback max until tracked max exceeds this + gauge_use_tracked_max = true, -- scale to tracked max when available + + -- Delta indicator + show_delta = true, + delta_stable_range = 5, -- watts within which delta shows as stable + + -- Min/Max tracking + track_minmax = true, + show_minmax = false, -- toggle: show [min-max] inline + persist_minmax = true, + data_file = "ac-power-data.json", + + -- Colors (RGB hex) + use_colors = true, + color_delta_rising = "FF4444", -- red when power increasing + color_delta_falling = "44FF44", -- green when power decreasing + color_delta_stable = "FFFFFF", -- white when stable + color_low_threshold = 200, + color_mid_threshold = 500, + color_high_threshold = 800, + color_low = "44FF44", -- green + color_mid = "FFFF44", -- yellow + color_high = "FF4444", -- red } -- No edits are required below unless you want to change script behavior. @@ -43,12 +71,20 @@ local mp = require("mp") local utils = require("mp.utils") local msg = require("mp.msg") +package.path = mp.command_native({ "expand-path", "~~/scripts/lib/?.lua;" }) .. package.path +local lib = require("utils") + local last_watts local last_display_ts = 0 local last_unit local request_inflight = false local backoff_failures = 0 local poll_timer +local prev_watts = nil +local min_watts = nil +local max_watts = nil +local data_loaded = false +local osd_clear_timer = nil local function log(level, text) msg[level]("ac-power: " .. text) @@ -92,6 +128,138 @@ local function format_value(value) return string.format(fmt, value) end +local function get_level_color(watts) + if watts <= config.color_low_threshold then + return config.color_low + elseif watts <= config.color_mid_threshold then + return config.color_mid + else + return config.color_high + end +end + +local function get_delta_color(delta) + if math.abs(delta) <= config.delta_stable_range then + return config.color_delta_stable + elseif delta > 0 then + return config.color_delta_rising + else + return config.color_delta_falling + end +end + +local function get_gauge_max() + if config.gauge_use_tracked_max and max_watts and max_watts > config.gauge_max_watts then + return max_watts + end + return config.gauge_max_watts +end + +local function build_gauge(watts, max_scale) + if not config.show_gauge then + return "" + end + local ratio = math.min(1, math.max(0, watts / max_scale)) + local filled = math.floor(ratio * config.gauge_width + 0.5) + local empty = config.gauge_width - filled + local bar = string.rep("█", filled) .. string.rep("░", empty) + return "[" .. lib.colorize(bar, get_level_color(watts), config.use_colors) .. "] " +end + +local function get_cache_dir() + local xdg = os.getenv("XDG_CACHE_HOME") + if xdg and xdg ~= "" then + return xdg .. "/mpv" + end + return os.getenv("HOME") .. "/.cache/mpv" +end + +local function get_data_file_path() + return get_cache_dir() .. "/" .. config.data_file +end + +local function ensure_cache_dir() + local dir = get_cache_dir() + os.execute('mkdir -p "' .. dir .. '"') +end + +local function load_minmax() + if data_loaded or not config.persist_minmax then + return + end + data_loaded = true + local f = io.open(get_data_file_path(), "r") + if not f then + return + end + local content = f:read("*a") + f:close() + if not content or content == "" then + return + end + local data = utils.parse_json(content) + if data then + min_watts = data.min_watts + max_watts = data.max_watts + log("info", string.format("loaded min/max: %s/%s", tostring(min_watts), tostring(max_watts))) + end +end + +local function save_minmax() + if not config.persist_minmax then + return + end + ensure_cache_dir() + + local disk_min, disk_max = nil, nil + local f = io.open(get_data_file_path(), "r") + if f then + local content = f:read("*a") + f:close() + if content and content ~= "" then + local data = utils.parse_json(content) + if data then + disk_min, disk_max = data.min_watts, data.max_watts + end + end + end + + local final_min = min_watts + local final_max = max_watts + if disk_min and (not final_min or disk_min < final_min) then + final_min = disk_min + end + if disk_max and (not final_max or disk_max > final_max) then + final_max = disk_max + end + + min_watts, max_watts = final_min, final_max + + f = io.open(get_data_file_path(), "w") + if f then + f:write(utils.format_json({ min_watts = final_min, max_watts = final_max })) + f:close() + end +end + +local function update_minmax(watts) + if not config.track_minmax or not watts then + return + end + local changed = false + if not min_watts or watts < min_watts then + min_watts = watts + changed = true + end + if not max_watts or watts > max_watts then + max_watts = watts + changed = true + end + if changed then + save_minmax() + end +end + local function sensor_units(payload) if config.use_sensor_unit and payload and payload.attributes then local unit = payload.attributes.unit_of_measurement @@ -111,8 +279,42 @@ local function show_message(text, duration) last_display_ts = mp.get_time() end -local function show_value(value_text, unit) - show_message(string.format("%s: %s %s", config.display_label, value_text, unit or "")) +local function show_value(value_text, unit, delta) + local watts_num = tonumber(value_text) or 0 + local parts = {} + + table.insert(parts, build_gauge(watts_num, get_gauge_max())) + table.insert( + parts, + lib.colorize(value_text .. (unit or ""), get_level_color(watts_num), config.use_colors) + ) + + if config.show_delta and delta then + local sign = delta >= 0 and "+" or "" + table.insert( + parts, + " " .. lib.colorize(sign .. format_value(delta), get_delta_color(delta), config.use_colors) + ) + end + + if config.show_minmax and min_watts and max_watts then + table.insert(parts, string.format(" [%s-%s]", format_value(min_watts), format_value(max_watts))) + end + + local text = table.concat(parts, "") + if config.use_colors then + if osd_clear_timer then + osd_clear_timer:kill() + end + mp.set_osd_ass(0, 0, text) + osd_clear_timer = mp.add_timeout(config.display_timeout, function() + mp.set_osd_ass(0, 0, "") + osd_clear_timer = nil + end) + else + mp.osd_message(text, config.display_timeout) + end + last_display_ts = mp.get_time() end local function show_error(err) @@ -199,11 +401,20 @@ local function handle_success(value, payload) end end + local delta = nil + if value and prev_watts then + delta = value - prev_watts + end + + update_minmax(value) + if should_render then - show_value(printable, unit) + show_value(printable, unit, delta) last_unit = unit last_watts = value end + + prev_watts = value end local function redraw_last_value() @@ -287,6 +498,7 @@ local function start() if not validate_config() then return end + load_minmax() if config.initial_message and config.initial_message ~= "" then show_message( string.format("%s: %s", config.display_label, config.initial_message), @@ -301,11 +513,26 @@ mp.register_script_message("ac-power-refresh", function() ensure_poll_timer(0.01, poll_sensor) end) +mp.register_script_message("ac-power-reset-minmax", function() + log("info", "resetting min/max values") + min_watts = nil + max_watts = nil + save_minmax() + mp.osd_message("AC Power: min/max reset", 2) +end) + mp.register_event("shutdown", function() if poll_timer then poll_timer:kill() poll_timer = nil end + if osd_clear_timer then + osd_clear_timer:kill() + osd_clear_timer = nil + end + if config.persist_minmax then + save_minmax() + end end) mp.register_event("file-loaded", redraw_last_value) diff --git a/scripts/kb-blackout.lua b/scripts/kb-blackout.lua index ccce8d5..1cfea57 100644 --- a/scripts/kb-blackout.lua +++ b/scripts/kb-blackout.lua @@ -32,6 +32,9 @@ local msg = require("mp.msg") local utils = require("mp.utils") local jit = rawget(_G, "jit") or {} +package.path = mp.command_native({ "expand-path", "~~/scripts/lib/?.lua;" }) .. package.path +local lib = require("utils") + local function coerce_boolean(value, default) if type(value) == "boolean" then return value @@ -136,10 +139,6 @@ if unsupported_platform() then return end -local function trim(str) - return str and str:match("^%s*(.-)%s*$") or "" -end - local function build_brightnessctl_args(extra) local args = { config.brightnessctl_path } if config.led_path ~= nil and config.led_path ~= "" then @@ -193,7 +192,7 @@ local function run_brightnessctl(extra, action) end if result.status ~= 0 then - local stderr_text = trim(result.stderr or "") + local stderr_text = lib.trim(result.stderr or "") msg.error(("brightnessctl %s failed (exit %d): %s"):format(action, result.status, stderr_text)) log_permission_hint(stderr_text) state.control_blocked = true @@ -233,7 +232,7 @@ local function read_current_brightness() return nil, err end - local cleaned = trim(stdout) + local cleaned = lib.trim(stdout) if cleaned == "" then msg.error("brightnessctl returned empty output") return nil, "empty output" diff --git a/scripts/lib/utils.lua b/scripts/lib/utils.lua new file mode 100644 index 0000000..e2e9ed3 --- /dev/null +++ b/scripts/lib/utils.lua @@ -0,0 +1,44 @@ +--[[ + Shared utility functions for mpv scripts. +]] + +local M = {} + +-- String utilities +function M.trim(str) + return str and str:match("^%s*(.-)%s*$") or "" +end + +-- Math utilities +function M.clamp(value, min_v, max_v) + if value < min_v then + return min_v + end + if value > max_v then + return max_v + end + return value +end + +-- ASS color utilities +function M.rgb_to_bgr(rgb) + return rgb:sub(5, 6) .. rgb:sub(3, 4) .. rgb:sub(1, 2) +end + +function M.colorize(text, color_rgb, use_colors) + if use_colors == false then + return text + end + return "{\\1c&H" .. M.rgb_to_bgr(color_rgb) .. "&}" .. text .. "{\\1c&HFFFFFF&}" +end + +-- Debug logging factory +function M.make_dbg(prefix, log_fn) + return function(text, config_enabled) + if config_enabled ~= false then + log_fn(prefix .. ": " .. text) + end + end +end + +return M diff --git a/scripts/playback-health.lua b/scripts/playback-health.lua index 5b3d9d0..951d247 100644 --- a/scripts/playback-health.lua +++ b/scripts/playback-health.lua @@ -48,6 +48,9 @@ local config = { local mp = require("mp") local msg = require("mp.msg") +package.path = mp.command_native({ "expand-path", "~~/scripts/lib/?.lua;" }) .. package.path +local lib = require("utils") + local state = { is_stream = false, buffering = false, @@ -69,21 +72,6 @@ local function dbg(text) end end -local function rgb_to_bgr(rgb) - local r = rgb:sub(1, 2) - local g = rgb:sub(3, 4) - local b = rgb:sub(5, 6) - return b .. g .. r -end - -local function colorize(text, color_rgb) - if not config.use_colors then - return text - end - local bgr = rgb_to_bgr(color_rgb) - return "{\\1c&H" .. bgr .. "&}" .. text .. "{\\1c&HFFFFFF&}" -end - local function clear_osd() mp.set_osd_ass(0, 0, "") end @@ -189,14 +177,14 @@ local function show_warning(warning_type, message, color) state.last_warning[warning_type] = mp.get_time() state.active_warning = warning_type local col = color or config.color_warning - local styled = colorize("⚠ " .. message, col) + local styled = lib.colorize("⚠ " .. message, col, config.use_colors) show_osd(styled, config.osd_duration) dbg(("warning: %s"):format(message)) end local function show_recovery() if state.active_warning and config.show_recovery then - local styled = colorize("✓ Recovered", config.color_recovery) + local styled = lib.colorize("✓ Recovered", config.color_recovery, config.use_colors) show_osd(styled, config.osd_duration) dbg("recovery shown") end @@ -256,12 +244,13 @@ local function on_cache_buffering_state(_, pct) local eta = predict_buffer_time(cache, target) local eta_str = format_prediction(eta) if eta_str then - local pred = colorize("(" .. eta_str .. " to resume)", config.color_info) + local pred = + lib.colorize("(" .. eta_str .. " to resume)", config.color_info, config.use_colors) table.insert(parts, pred) end end local text = table.concat(parts, " ") - local styled = colorize("⚠ " .. text, config.color_warning) + local styled = lib.colorize("⚠ " .. text, config.color_warning, config.use_colors) show_osd(styled, config.osd_duration) end end @@ -294,7 +283,8 @@ local function on_cache_duration(_, duration) local eta = predict_buffer_time(duration, 0) local eta_str = format_prediction(eta) if eta_str then - local pred = colorize("(stall in " .. eta_str .. ")", config.color_info) + local pred = + lib.colorize("(stall in " .. eta_str .. ")", config.color_info, config.use_colors) table.insert(parts, pred) end end diff --git a/scripts/touch-gestures.lua b/scripts/touch-gestures.lua index fa1aa08..53c4c3f 100644 --- a/scripts/touch-gestures.lua +++ b/scripts/touch-gestures.lua @@ -50,6 +50,9 @@ local mp = require("mp") local msg = require("mp.msg") local utils = require("mp.utils") +package.path = mp.command_native({ "expand-path", "~~/scripts/lib/?.lua;" }) .. package.path +local lib = require("utils") + local state = { active = false, zone = nil, @@ -72,16 +75,6 @@ local function dbg(text) end end -local function clamp(value, min_v, max_v) - if value < min_v then - return min_v - end - if value > max_v then - return max_v - end - return value -end - local function run_cmd(args) local res = utils.subprocess({ args = args, cancellable = false }) if not res or res.error or res.status ~= 0 then @@ -149,7 +142,7 @@ local function current_brightness_percent() 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) + state.cached_brightness_pct = lib.clamp((c / m) * 100, 0, 100) return state.cached_brightness_pct end end @@ -163,12 +156,12 @@ local function current_brightness_percent() if not c or not m or m <= 0 then return nil end - state.cached_brightness_pct = clamp((c / m) * 100, 0, 100) + state.cached_brightness_pct = lib.clamp((c / m) * 100, 0, 100) return state.cached_brightness_pct end local function set_brightness_percent(pct) - local target = clamp(pct, config.min_brightness, 100) + local target = lib.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 @@ -191,7 +184,7 @@ local function current_volume_percent() if not val then return nil end - state.cached_volume_pct = clamp(val * 100, 0, config.max_volume) + state.cached_volume_pct = lib.clamp(val * 100, 0, config.max_volume) return state.cached_volume_pct elseif backend == "pactl" then local out = run_cmd({ config.pactl_path, "get-sink-volume", "@DEFAULT_SINK@" }) @@ -203,7 +196,7 @@ local function current_volume_percent() if not pct then return nil end - state.cached_volume_pct = clamp(pct, 0, config.max_volume) + state.cached_volume_pct = lib.clamp(pct, 0, config.max_volume) return state.cached_volume_pct end return nil @@ -214,7 +207,7 @@ local function set_volume_percent(pct) if not backend then return end - local target = clamp(pct, 0, config.max_volume) + local target = lib.clamp(pct, 0, config.max_volume) if backend == "wpctl" then run_cmd({ config.wpctl_path,