updated mpv code to try and fix invisible cursor

This commit is contained in:
2026-04-19 18:13:44 -07:00
parent bafd37fdfb
commit f8230bab9c
2 changed files with 188 additions and 76 deletions
+179 -71
View File
@@ -388,31 +388,52 @@ M._disable_input_section = function(name, reason)
end
end
function M._sync_uosc_cursor(reason)
local why = tostring(reason or 'unknown')
local function do_sync(suffix)
pcall(mp.set_property, 'cursor-autohide', '1000')
if ensure_uosc_loaded() then
pcall(mp.commandv, 'script-message-to', 'uosc', 'sync-cursor')
end
M._disable_input_section('input_console', why .. suffix)
M._disable_input_section('input_forced_console', why .. suffix)
M._disable_input_section('image', why .. suffix)
end
do_sync('@immediate')
mp.add_timeout(0.05, function()
do_sync('@sync')
end)
mp.add_timeout(0.20, function()
do_sync('@sync2')
end)
end
function M._close_uosc_menu_and_sync(menu_type, reason)
local why = tostring(reason or 'unknown')
if ensure_uosc_loaded() then
if menu_type and menu_type ~= '' then
pcall(mp.commandv, 'script-message-to', 'uosc', 'close-menu', menu_type)
else
pcall(mp.commandv, 'script-message-to', 'uosc', 'close-menu')
end
end
M._sync_uosc_cursor(why)
return true
end
M._reset_uosc_input_state = function(reason)
local why = tostring(reason or 'unknown')
M._disable_input_section('input_console', why)
M._disable_input_section('input_forced_console', why)
M._disable_input_section('image', why)
pcall(mp.set_property, 'cursor-autohide', '1000')
if not ensure_uosc_loaded() then
return false
end
pcall(mp.commandv, 'script-message-to', 'uosc', 'close-menu')
pcall(mp.commandv, 'script-message-to', 'uosc', 'sync-cursor')
M._disable_input_section('input_console', why .. '@immediate')
M._disable_input_section('input_forced_console', why .. '@immediate')
mp.add_timeout(0.05, function()
if ensure_uosc_loaded() then
pcall(mp.commandv, 'script-message-to', 'uosc', 'sync-cursor')
end
M._disable_input_section('input_console', why .. '@sync')
M._disable_input_section('input_forced_console', why .. '@sync')
end)
mp.add_timeout(0.20, function()
if ensure_uosc_loaded() then
pcall(mp.commandv, 'script-message-to', 'uosc', 'sync-cursor')
end
M._disable_input_section('input_console', why .. '@sync2')
M._disable_input_section('input_forced_console', why .. '@sync2')
end)
M._sync_uosc_cursor(why)
return true
end
@@ -429,6 +450,12 @@ M._open_uosc_menu = function(menu_data, reason)
_lua_log('menu: open-menu failed reason=' .. why .. ' err=' .. tostring(err))
return false
end
mp.add_timeout(0.03, function()
pcall(mp.set_property, 'cursor-autohide', '1000')
if ensure_uosc_loaded() then
pcall(mp.commandv, 'script-message-to', 'uosc', 'sync-cursor')
end
end)
return true
end
@@ -927,6 +954,25 @@ local function _normalize_store_name(store)
return trim(store)
end
function M._store_name_is_visible_in_mpv(store)
store = _normalize_store_name(store)
return store ~= '' and store:lower() ~= 'local'
end
function M._visible_store_names()
local out = {}
if type(_cached_store_names) ~= 'table' then
return out
end
for _, name in ipairs(_cached_store_names) do
name = _normalize_store_name(name)
if M._store_name_is_visible_in_mpv(name) then
out[#out + 1] = name
end
end
return out
end
local function _is_cached_store_name(store)
local needle = _normalize_store_name(store)
if needle == '' then
@@ -1174,17 +1220,25 @@ local HELPER_START_DEBOUNCE = 2.0
-- Track ready-heartbeat freshness so stale or non-timestamp values don't mask a stopped helper
local _helper_ready_last_value = ''
local _helper_ready_last_seen_ts = 0
local HELPER_READY_STALE_SECONDS = 10.0
local HELPER_READY_STALE_SECONDS = 120.0
M._lyric_helper_state = M._lyric_helper_state or { last_start_ts = -1000, debounce = 3.0 }
M._subtitle_autoselect_state = M._subtitle_autoselect_state or { serial = 0, deadline = 0 }
function M._normalize_mpv_user_data_text(value)
local text = trim(tostring(value or ''))
if text:sub(1, 1) == '"' and text:sub(-1) == '"' and #text >= 2 then
text = text:sub(2, -2)
end
return trim(text)
end
local function _is_pipeline_helper_ready()
local helper_version = mp.get_property('user-data/medeia-pipeline-helper-version')
if helper_version == nil or helper_version == '' then
helper_version = mp.get_property_native('user-data/medeia-pipeline-helper-version')
end
helper_version = tostring(helper_version or '')
if helper_version ~= '2026-03-23.1' then
helper_version = M._normalize_mpv_user_data_text(helper_version)
if helper_version ~= '' and helper_version ~= '2026-03-23.1' then
return false
end
@@ -1197,7 +1251,7 @@ local function _is_pipeline_helper_ready()
_helper_ready_last_seen_ts = 0
return false
end
local s = tostring(ready)
local s = M._normalize_mpv_user_data_text(ready)
if s == '' or s == '0' then
_helper_ready_last_value = s
_helper_ready_last_seen_ts = 0
@@ -1228,6 +1282,9 @@ local function _is_pipeline_helper_ready()
-- Fall back only for non-timestamp values so stale helper timestamps from a
-- previous session do not look fresh right after Lua reload.
if helper_version ~= '2026-03-23.1' then
return false
end
if _helper_ready_last_seen_ts > 0 and (now - _helper_ready_last_seen_ts) <= HELPER_READY_STALE_SECONDS then
return true
end
@@ -1236,22 +1293,33 @@ local function _is_pipeline_helper_ready()
end
local function _helper_ready_diagnostics()
local ready = mp.get_property(PIPELINE_READY_PROP)
if ready == nil or ready == '' then
ready = mp.get_property_native(PIPELINE_READY_PROP)
local raw_ready = mp.get_property(PIPELINE_READY_PROP)
if raw_ready == nil or raw_ready == '' then
raw_ready = mp.get_property_native(PIPELINE_READY_PROP)
end
local helper_version = mp.get_property('user-data/medeia-pipeline-helper-version')
if helper_version == nil or helper_version == '' then
helper_version = mp.get_property_native('user-data/medeia-pipeline-helper-version')
local raw_helper_version = mp.get_property('user-data/medeia-pipeline-helper-version')
if raw_helper_version == nil or raw_helper_version == '' then
raw_helper_version = mp.get_property_native('user-data/medeia-pipeline-helper-version')
end
local ready = M._normalize_mpv_user_data_text(raw_ready)
local helper_version = M._normalize_mpv_user_data_text(raw_helper_version)
local now = mp.get_time() or 0
local age = 'n/a'
if _helper_ready_last_seen_ts > 0 then
age = string.format('%.2fs', math.max(0, now - _helper_ready_last_seen_ts))
end
local heartbeat_age = 'n/a'
local ready_epoch = tonumber(ready)
local os_now = (os and os.time) and os.time() or nil
if ready_epoch and ready_epoch > 1000000000 and os_now then
heartbeat_age = string.format('%.2fs', math.max(0, os_now - ready_epoch))
end
return 'ready=' .. tostring(ready or '')
.. ' raw_ready=' .. tostring(raw_ready or '')
.. ' helper_version=' .. tostring(helper_version or '')
.. ' raw_helper_version=' .. tostring(raw_helper_version or '')
.. ' required_version=2026-03-23.1'
.. ' heartbeat_age=' .. tostring(heartbeat_age)
.. ' last_value=' .. tostring(_helper_ready_last_value or '')
.. ' last_seen_age=' .. tostring(age)
end
@@ -2327,6 +2395,29 @@ local function _get_current_item_is_image()
return false
end
function M._has_visual_screenshot_source()
if _get_current_item_is_image() then
return true, 'image'
end
local width = tonumber(mp.get_property_number('width') or 0) or 0
local height = tonumber(mp.get_property_number('height') or 0) or 0
if width > 0 and height > 0 then
return true, 'video'
end
local video_params = mp.get_property_native('video-params')
if type(video_params) == 'table' then
local params_width = tonumber(video_params.w or video_params.dw or video_params.width or 0) or 0
local params_height = tonumber(video_params.h or video_params.dh or video_params.height or 0) or 0
if params_width > 0 and params_height > 0 then
return true, 'video'
end
end
return false, ''
end
local function _set_image_property(value)
pcall(mp.set_property_native, 'user-data/mpv/image', value and true or false)
@@ -2776,6 +2867,7 @@ local function _open_store_picker_for_pending_screenshot()
local function build_items()
local selected = _get_selected_store()
local visible_names = M._visible_store_names()
local items = {}
items[#items + 1] = {
@@ -2784,8 +2876,8 @@ local function _open_store_picker_for_pending_screenshot()
value = { 'script-message-to', mp.get_script_name(), 'medeia-image-screenshot-pick-path', '{}' },
}
if type(_cached_store_names) == 'table' and #_cached_store_names > 0 then
for _, name in ipairs(_cached_store_names) do
if #visible_names > 0 then
for _, name in ipairs(visible_names) do
name = trim(tostring(name or ''))
if name ~= '' then
items[#items + 1] = {
@@ -2822,6 +2914,16 @@ local function _open_store_picker_for_pending_screenshot()
end
local function _capture_screenshot()
local screenshot_available = false
local screenshot_kind = ''
screenshot_available, screenshot_kind = M._has_visual_screenshot_source()
if not screenshot_available then
_lua_log('screenshot: blocked reason=no visual source')
M._sync_uosc_cursor('screenshot-unavailable')
mp.osd_message('Screenshot unavailable (no video/image frame)', 2)
return
end
local function _format_time_label(seconds)
local total = math.max(0, math.floor(tonumber(seconds or 0) or 0))
local hours = math.floor(total / 3600)
@@ -2856,6 +2958,11 @@ local function _capture_screenshot()
end
local out_path = utils.join_path(temp_dir, filename)
out_path = _normalize_fs_path(out_path)
local was_paused = mp.get_property_native('pause') and true or false
if not was_paused then
pcall(mp.set_property_native, 'pause', true)
end
local function do_screenshot(mode)
mode = mode or 'video'
@@ -2869,22 +2976,31 @@ local function _capture_screenshot()
-- try 'window' as fallback.
local ok = do_screenshot('video')
if not ok then
_lua_log('screenshot: video-mode failed; trying window-mode')
_lua_log('screenshot: video-mode failed; trying window-mode kind=' .. tostring(screenshot_kind))
ok = do_screenshot('window')
end
if not ok then
_lua_log('screenshot: BOTH video and window modes FAILED')
if not was_paused then
pcall(mp.set_property_native, 'pause', false)
end
M._sync_uosc_cursor('screenshot-failed')
mp.osd_message('Screenshot failed (no frames)', 2)
return
end
if not was_paused then
mp.osd_message('Screenshot captured. Playback paused for review.', 2)
end
_ensure_selected_store_loaded()
local function dispatch_screenshot_save()
local store_count = (type(_cached_store_names) == 'table') and #_cached_store_names or 0
local visible_store_names = M._visible_store_names()
local store_count = #visible_store_names
local selected_store = _normalize_store_name(_get_selected_store())
if not _is_cached_store_name(selected_store) then
if not M._store_name_is_visible_in_mpv(selected_store) then
selected_store = ''
end
@@ -2895,7 +3011,7 @@ local function _capture_screenshot()
end
if selected_store == '' and store_count == 1 then
selected_store = _normalize_store_name(_cached_store_names[1])
selected_store = _normalize_store_name(visible_store_names[1])
end
if selected_store == '' then
@@ -3616,16 +3732,11 @@ _refresh_store_cache = function(timeout_seconds, on_complete)
end
local cached_json = mp.get_property('user-data/medeia-store-choices-cached')
_lua_log('stores: cache_read cached_json=' .. tostring(cached_json) .. ' len=' .. tostring(cached_json and #cached_json or 0))
if cached_json and cached_json ~= '' then
local ok, cached_resp = pcall(utils.parse_json, cached_json)
_lua_log('stores: cache_parse ok=' .. tostring(ok) .. ' resp_type=' .. tostring(type(cached_resp)))
if ok then
if type(cached_resp) == 'string' then
_lua_log('stores: cache_parse returned string, trying again...')
ok, cached_resp = pcall(utils.parse_json, cached_resp)
_lua_log('stores: cache_parse retry ok=' .. tostring(ok) .. ' resp_type=' .. tostring(type(cached_resp)))
end
if ok then
if apply_store_choices(cached_resp, 'cache') then
@@ -3633,10 +3744,8 @@ _refresh_store_cache = function(timeout_seconds, on_complete)
end
end
else
_lua_log('stores: cache_parse failed ok=' .. tostring(ok) .. ' resp=' .. tostring(cached_resp))
_lua_log('stores: cached property parse failed; falling back to direct config')
end
else
_lua_log('stores: cache_empty cached_json=' .. tostring(cached_json))
end
if not _store_cache_retry_pending then
@@ -3678,10 +3787,11 @@ local function _open_store_picker()
_ensure_selected_store_loaded()
local selected = _get_selected_store()
local cached_count = (type(_cached_store_names) == 'table') and #_cached_store_names or 0
local visible_store_names = M._visible_store_names()
local cached_count = #visible_store_names
local cached_preview = ''
if type(_cached_store_names) == 'table' and #_cached_store_names > 0 then
cached_preview = table.concat(_cached_store_names, ', ')
if cached_count > 0 then
cached_preview = table.concat(visible_store_names, ', ')
end
_lua_log(
'stores: open picker selected='
@@ -3695,10 +3805,11 @@ local function _open_store_picker()
local function build_items()
local selected = _get_selected_store()
local current_url = trim(tostring(_current_url_for_web_actions() or _current_target() or ''))
local visible_names = M._visible_store_names()
local items = {}
if type(_cached_store_names) == 'table' and #_cached_store_names > 0 then
for _, name in ipairs(_cached_store_names) do
if #visible_names > 0 then
for _, name in ipairs(visible_names) do
name = trim(tostring(name or ''))
if name ~= '' then
local payload = { store = name }
@@ -3728,25 +3839,13 @@ local function _open_store_picker()
-- Open immediately with whatever cache we have.
_uosc_open_list_picker(STORE_PICKER_MENU_TYPE, 'Store', build_items())
-- Best-effort refresh; retry briefly to avoid races where the helper isn't
-- ready/observing yet at the exact moment the menu opens.
local function attempt_refresh(tries_left)
mp.add_timeout(0.05, function()
_refresh_store_cache(1.2, function(success, changed)
if success and changed then
_lua_log('stores: reopening menu (store list changed)')
_uosc_open_list_picker(STORE_PICKER_MENU_TYPE, 'Store', build_items())
end
end)
if tries_left > 0 then
mp.add_timeout(0.25, function()
attempt_refresh(tries_left - 1)
end)
end
end
mp.add_timeout(0.05, function()
attempt_refresh(6)
end)
end
@@ -4225,6 +4324,9 @@ end
_store_status_hint_for_url = function(store, url, fallback)
store = trim(tostring(store or ''))
url = trim(tostring(url or ''))
if not M._store_name_is_visible_in_mpv(store) then
return fallback
end
if store == '' or url == '' or not _current_store_url_status_matches(store, url) then
return fallback
end
@@ -4257,6 +4359,11 @@ _refresh_current_store_url_status = function(reason)
local url = trim(tostring(target or ''))
local normalized_url = _normalize_url_for_store_lookup(url)
if not M._store_name_is_visible_in_mpv(store) then
_begin_current_store_url_status(store, url, 'idle')
return
end
if url == '' or not _is_http_url(url) or _extract_store_hash(url) then
_begin_current_store_url_status(store, url, 'idle')
return
@@ -5167,6 +5274,7 @@ local function _open_save_location_picker_for_pending_download()
local function build_items()
local selected = _get_selected_store()
local visible_names = M._visible_store_names()
local items = {
{
title = 'Pick folder…',
@@ -5175,8 +5283,8 @@ local function _open_save_location_picker_for_pending_download()
},
}
if type(_cached_store_names) == 'table' and #_cached_store_names > 0 then
for _, name in ipairs(_cached_store_names) do
if #visible_names > 0 then
for _, name in ipairs(visible_names) do
name = trim(tostring(name or ''))
if name ~= '' then
local payload = { store = name }
@@ -5898,10 +6006,13 @@ end
-- Open the command submenu with tailored cmdlets (screenshot, clip, trim prompt)
function M.open_cmd_menu()
local screenshot_available, screenshot_kind = M._has_visual_screenshot_source()
local screenshot_hint = screenshot_available and ((screenshot_kind == 'image') and 'Capture current image' or 'Capture current frame') or 'Disabled: no video or image frame'
local items = {
{
title = 'Screenshot',
hint = 'Capture a screenshot',
hint = screenshot_hint,
selectable = screenshot_available,
value = { 'script-message-to', mp.get_script_name(), 'medios-cmd-exec', utils.format_json({ cmd = 'screenshot' }) },
},
{
@@ -6195,19 +6306,15 @@ mp.register_script_message('medios-load-url-event', function(json)
if not ok or type(event) ~= 'table' then
_lua_log('[LOAD-URL] Failed to parse JSON: ' .. tostring(json))
mp.osd_message('Failed to parse URL', 2)
if ensure_uosc_loaded() then
_lua_log('[LOAD-URL] Closing menu due to parse error')
mp.commandv('script-message-to', 'uosc', 'close-menu', LOAD_URL_MENU_TYPE)
end
M._close_uosc_menu_and_sync(LOAD_URL_MENU_TYPE, 'load-url-parse-error')
return
end
_lua_log('[LOAD-URL] Parsed event: type=' .. tostring(event.type) .. ', query=' .. tostring(event.query))
if event.type ~= 'search' then
_lua_log('[LOAD-URL] Event type is not search: ' .. tostring(event.type))
if ensure_uosc_loaded() then
_lua_log('[LOAD-URL] Closing menu due to type mismatch')
mp.commandv('script-message-to', 'uosc', 'close-menu', LOAD_URL_MENU_TYPE)
end
M._close_uosc_menu_and_sync(LOAD_URL_MENU_TYPE, 'load-url-close')
return
end
@@ -6217,10 +6324,8 @@ mp.register_script_message('medios-load-url-event', function(json)
_lua_log('[LOAD-URL] URL is empty')
_log_all('ERROR', 'Load URL failed: URL is empty')
mp.osd_message('URL is empty', 2)
if ensure_uosc_loaded() then
_lua_log('[LOAD-URL] Closing menu due to empty URL')
mp.commandv('script-message-to', 'uosc', 'close-menu', LOAD_URL_MENU_TYPE)
end
M._close_uosc_menu_and_sync(LOAD_URL_MENU_TYPE, 'load-url-empty')
return
end
@@ -6341,6 +6446,9 @@ function M.show_menu()
local target = _current_target()
local selected_store = trim(tostring(_get_selected_store() or ''))
if not M._store_name_is_visible_in_mpv(selected_store) then
selected_store = ''
end
local download_hint = nil
if selected_store ~= '' and target and not _extract_store_hash(tostring(target)) then
download_hint = _store_status_hint_for_url(selected_store, tostring(target), 'save to ' .. selected_store)
@@ -1123,8 +1123,12 @@ mp.register_script_message('sync-cursor', function()
local mouse = mp.get_property_native('mouse-pos')
if type(mouse) == 'table' and mouse.hover and mouse.x and mouse.y then
cursor:move(mouse.x, mouse.y)
else
cursor:leave()
elseif cursor.x < math.huge and cursor.y < math.huge then
if cursor.hidden then
cursor.hidden = false
Elements:trigger('global_mouse_enter')
end
Elements:update_proximities()
end
cursor:queue_autohide()
request_render()