diff --git a/CLI.py b/CLI.py index bf8c07f..0533b4b 100644 --- a/CLI.py +++ b/CLI.py @@ -1917,7 +1917,7 @@ class CmdletExecutor: selection = SelectionSyntax.parse(arg) if selection is not None: - zero_based = sorted(idx - 1 for idx in selection) + zero_based = [idx - 1 for idx in selection] for idx in zero_based: if idx not in selected_indices: selected_indices.append(idx) @@ -2326,7 +2326,7 @@ class CLI: self._pipeline_executor = PipelineExecutor(config_loader=self._config_loader) @staticmethod - def parse_selection_syntax(token: str) -> Optional[Set[int]]: + def parse_selection_syntax(token: str) -> Optional[List[int]]: return SelectionSyntax.parse(token) @classmethod diff --git a/SYS/cli_parsing.py b/SYS/cli_parsing.py index 2109b30..93cb67b 100644 --- a/SYS/cli_parsing.py +++ b/SYS/cli_parsing.py @@ -7,7 +7,7 @@ these pure helpers are easier to test. from __future__ import annotations import re -from typing import Any, Callable, Dict, List, Optional, Set, Tuple +from typing import Any, Callable, Dict, List, Optional, Tuple from SYS.logger import debug # Prompt-toolkit lexer types are optional and expensive (~300ms). Use find_spec @@ -66,13 +66,13 @@ DRIVE_RE = re.compile(r"^[A-Za-z]:[\\/]") class SelectionSyntax: - """Parses @ selection syntax into 1-based indices.""" + """Parses @ selection syntax into ordered 1-based indices.""" _RANGE_RE = re.compile(r"^[0-9\-]+$") @staticmethod - def parse(token: str) -> Optional[Set[int]]: - """Return 1-based indices or None when not a concrete selection. + def parse(token: str) -> Optional[List[int]]: + """Return ordered 1-based indices or None when not a concrete selection. Concrete selections: - @2 @@ -96,7 +96,8 @@ class SelectionSyntax: if selector.startswith("{") and selector.endswith("}"): selector = selector[1:-1].strip() - indices: Set[int] = set() + indices: List[int] = [] + seen: set[int] = set() for part in selector.split(","): part = part.strip() if not part: @@ -117,7 +118,10 @@ class SelectionSyntax: return None if start <= 0 or end <= 0 or start > end: return None - indices.update(range(start, end + 1)) + for value in range(start, end + 1): + if value not in seen: + seen.add(value) + indices.append(value) continue try: @@ -126,7 +130,9 @@ class SelectionSyntax: return None if value <= 0: return None - indices.add(value) + if value not in seen: + seen.add(value) + indices.append(value) return indices if indices else None @@ -498,4 +504,3 @@ class MedeiaLexer(Lexer): return get_line - diff --git a/SYS/pipeline.py b/SYS/pipeline.py index 82539ec..53d1473 100644 --- a/SYS/pipeline.py +++ b/SYS/pipeline.py @@ -1452,9 +1452,7 @@ class PipelineExecutor: if token.startswith("@"): # selection selection = _cli_parsing().SelectionSyntax.parse(token) if selection is not None: - first_stage_selection_indices = sorted( - [i - 1 for i in selection] - ) + first_stage_selection_indices = [i - 1 for i in selection] continue if token == "@*": first_stage_select_all = True @@ -2893,9 +2891,7 @@ class PipelineExecutor: if _cli_parsing().SelectionFilterSyntax.matches(item, filter_spec) ] else: - selected_indices = sorted( - [i - 1 for i in selection] - ) # type: ignore[arg-type] + selected_indices = [i - 1 for i in selection] # type: ignore[arg-type] resolved_items = items_list if items_list else [] filtered = [ diff --git a/plugins/mpv/LUA/main.lua b/plugins/mpv/LUA/main.lua index 03ad71a..eeb9749 100644 --- a/plugins/mpv/LUA/main.lua +++ b/plugins/mpv/LUA/main.lua @@ -1573,7 +1573,7 @@ end function M._attempt_start_lyric_helper_async(reason) reason = trim(tostring(reason or 'startup')) - local state = M._lyric_helper_state or { last_start_ts = -1000, debounce = 3.0 } + local state = M._lyric_helper_state or { last_start_ts = -1000, debounce = 3.0, launch_inflight = false } M._lyric_helper_state = state if not ensure_mpv_ipc_server() then @@ -1581,6 +1581,10 @@ function M._attempt_start_lyric_helper_async(reason) return false end + if state.launch_inflight then + return false + end + local now = mp.get_time() or 0 if (state.last_start_ts or -1000) > -1 and (now - (state.last_start_ts or -1000)) < (state.debounce or 3.0) then return false @@ -1626,16 +1630,32 @@ function M._attempt_start_lyric_helper_async(reason) args[#args + 1] = lyric_log end - local ok, result, detail = _run_subprocess_command({ name = 'subprocess', args = args, detach = true }) - if not ok then - ok, result, detail = _run_subprocess_command({ name = 'subprocess', args = args }) - end - if not ok then - _lua_log('lyric-helper: spawn failed reason=' .. tostring(reason) .. ' detail=' .. tostring(detail or _describe_subprocess_result(result))) + state.launch_inflight = true + local ok_async, async_err = pcall(mp.command_native_async, { + name = 'subprocess', + args = args, + detach = true, + }, function(success, result, callback_err) + state.launch_inflight = false + if not success then + _lua_log('lyric-helper: async spawn failed reason=' .. tostring(reason) .. ' detail=' .. tostring(callback_err or 'subprocess failed')) + return + end + if type(result) == 'table' then + local result_err = trim(tostring(result.error or '')) + local status = tonumber(result.status) + if (result_err ~= '' and result_err ~= 'success') or (status ~= nil and status ~= 0) then + _lua_log('lyric-helper: async spawn failed reason=' .. tostring(reason) .. ' detail=' .. tostring(_describe_subprocess_result(result))) + return + end + end + _lua_log('lyric-helper: start requested reason=' .. tostring(reason) .. ' script=' .. tostring(lyric_script)) + end) + if not ok_async then + state.launch_inflight = false + _lua_log('lyric-helper: async spawn failed reason=' .. tostring(reason) .. ' detail=' .. tostring(async_err)) return false end - - _lua_log('lyric-helper: start requested reason=' .. tostring(reason) .. ' script=' .. tostring(lyric_script)) return true end @@ -6840,6 +6860,21 @@ mp.add_timeout(0, function() else _lua_log('[KEY] forced mbtn_right failed: ' .. tostring(rclick_err) .. ' (falling back to input.conf)') end + + -- Some mpv/uosc input paths emit double-right-click as `mbtn_right_dbl`. + -- Register it explicitly so menu invocation still works and mpv doesn't + -- warn about missing binding for this key. + local ok_rclick_dbl, rclick_dbl_err = pcall(function() + mp.add_forced_key_binding('mbtn_right_dbl', 'medios-mbtn-right-dbl-forced', function() + _lua_log('[KEY] mbtn_right_dbl -> show_menu') + M.show_menu() + end, {repeatable = false}) + end) + if ok_rclick_dbl then + _lua_log('[KEY] registered forced mbtn_right_dbl binding') + else + _lua_log('[KEY] forced mbtn_right_dbl failed: ' .. tostring(rclick_dbl_err) .. ' (falling back to input.conf)') + end end) return M diff --git a/plugins/mpv/portable_config/input.conf b/plugins/mpv/portable_config/input.conf index ca5b8a9..e89b442 100644 --- a/plugins/mpv/portable_config/input.conf +++ b/plugins/mpv/portable_config/input.conf @@ -1,6 +1,7 @@ # Medios Macina keybindings # Route right-click to our menu handler (before UOSC can claim it) mbtn_right script-message medios-show-menu +mbtn_right_dbl script-message medios-show-menu # Route 'm' key (alternative to keybinding, in case keybinding doesn't work) m script-message medios-show-menu