From 617564dfa2af48df47a4ddb4f4f0bf40fb4ae297 Mon Sep 17 00:00:00 2001 From: CogentRedTester Date: Sun, 11 Jan 2026 11:12:27 +1030 Subject: [PATCH 1/4] mp.input: use unique event handlers for input.get requests This makes changes to mp.input and console.lua so that every input.get request uses a unique script-message to handle input events. Previously, making new input.get requests shortly after the termination of a previous request made by the same script could cause a race condition where the input handler was closed but the new request was still being drawn in the UI. This was caused by the `closed` event for the previous request being received only after the new request was registered, hence closing the event handler for the new request instead. In addition, this commit makes the behaviour of calling input.get while another request is active more consistent. When a new request is received it overwrites the in-progress request, sending a `closed` event. However, previously, the `closed` event could not be sent if both requests came from the same script, as the new request would have overwritten the event handler. Now, the `closed` event is called regardless of where the new request comes from. --- player/javascript/defaults.js | 19 ++++++++++++------- player/lua/console.lua | 28 +++++++++++++++++----------- player/lua/input.lua | 17 ++++++++++++----- 3 files changed, 41 insertions(+), 23 deletions(-) diff --git a/player/javascript/defaults.js b/player/javascript/defaults.js index ae687febe1014..13124d1c7d8de 100644 --- a/player/javascript/defaults.js +++ b/player/javascript/defaults.js @@ -653,8 +653,12 @@ mp.options = { read_options: read_options }; /********************************************************************** * input *********************************************************************/ +var input_handle_counter = 0; + function register_event_handler(t) { - mp.register_script_message("input-event", function (type, args) { + var handler_id = "input-event/" + input_handle_counter++; + + mp.register_script_message(handler_id, function (type, args) { if (t[type]) { args = args ? JSON.parse(args) : []; var result = t[type].apply(null, args); @@ -666,18 +670,19 @@ function register_event_handler(t) { } if (type == "closed") - mp.unregister_script_message("input-event"); + mp.unregister_script_message(handler_id); }) + + return handler_id; } mp.input = { get: function(t) { - t.has_completions = t.complete !== undefined - - mp.commandv("script-message-to", "console", "get-input", mp.script_name, - JSON.stringify(t)); + t.has_completions = t.complete !== undefined; + t.client_name = mp.script_name; + t.handler_id = register_event_handler(t); - register_event_handler(t) + mp.commandv("script-message-to", "console", "get-input", JSON.stringify(t)); }, terminate: function () { mp.commandv("script-message-to", "console", "disable"); diff --git a/player/lua/console.lua b/player/lua/console.lua index 27ab10c974dd0..629e985801298 100644 --- a/player/lua/console.lua +++ b/player/lua/console.lua @@ -96,6 +96,7 @@ local key_bindings = {} local dont_bind_up_down = false local global_margins = { t = 0, b = 0 } local input_caller +local input_caller_handler local keep_open = false local completion_buffer = {} @@ -949,7 +950,7 @@ end local function handle_edit() if not selectable_items then handle_cursor_move() - mp.commandv("script-message-to", input_caller, "input-event", "edited", + mp.commandv("script-message-to", input_caller, input_caller_handler, "edited", utils.format_json({line})) return end @@ -1070,7 +1071,7 @@ local function submit() if selectable_items then if #matches > 0 then - mp.commandv("script-message-to", input_caller, "input-event", "submit", + mp.commandv("script-message-to", input_caller, input_caller_handler, "submit", utils.format_json({matches[focused_match].index})) end else @@ -1079,7 +1080,7 @@ local function submit() cycle_through_completions() end - mp.commandv("script-message-to", input_caller, "input-event", "submit", + mp.commandv("script-message-to", input_caller, input_caller_handler, "submit", utils.format_json({line})) history_add(line) @@ -1481,7 +1482,7 @@ end complete = function () completion_old_line = line completion_old_cursor = cursor - mp.commandv("script-message-to", input_caller, "input-event", + mp.commandv("script-message-to", input_caller, input_caller_handler, "complete", utils.format_json({line:sub(1, cursor - 1)})) render() end @@ -1651,7 +1652,7 @@ set_active = function (active) unbind_mouse() mp.set_property_bool("user-data/mpv/console/open", false) mp.set_property_bool("input-ime", ime_active) - mp.commandv("script-message-to", input_caller, "input-event", + mp.commandv("script-message-to", input_caller, input_caller_handler, "closed", utils.format_json({line, cursor})) collectgarbage() end @@ -1662,14 +1663,15 @@ mp.register_script_message("disable", function() set_active(false) end) -mp.register_script_message("get-input", function (script_name, args) - if open and script_name ~= input_caller then - mp.commandv("script-message-to", input_caller, "input-event", +mp.register_script_message("get-input", function (args) + if open then + mp.commandv("script-message-to", input_caller, input_caller_handler, "closed", utils.format_json({line, cursor})) end - input_caller = script_name args = utils.parse_json(args) + input_caller = args.client_name + input_caller_handler = args.handler_id prompt = args.prompt or "" line = args.default_text or "" cursor = tonumber(args.cursor_position) or line:len() + 1 @@ -1704,7 +1706,7 @@ mp.register_script_message("get-input", function (script_name, args) else selectable_items = nil unbind_mouse() - id = args.id or script_name .. prompt + id = args.id or args.client_name .. prompt log_offset = 0 completion_buffer = {} autoselect_completion = args.autoselect_completion @@ -1721,11 +1723,15 @@ mp.register_script_message("get-input", function (script_name, args) if line ~= "" then complete() + elseif open then + -- This is needed to update the prompt if a new request is + -- received while another is still active. + render() end end set_active(true) - mp.commandv("script-message-to", input_caller, "input-event", "opened") + mp.commandv("script-message-to", input_caller, input_caller_handler, "opened") end) -- Add a line to the log buffer diff --git a/player/lua/input.lua b/player/lua/input.lua index 3dded33dd744c..558fef91b424f 100644 --- a/player/lua/input.lua +++ b/player/lua/input.lua @@ -18,6 +18,8 @@ License along with mpv. If not, see . local utils = require "mp.utils" local input = {} +local handle_counter = 0 + local function get_non_callbacks(t) local non_callbacks = {} @@ -31,7 +33,10 @@ local function get_non_callbacks(t) end local function register_event_handler(t) - mp.register_script_message("input-event", function (type, args) + local handler_id = "input-event/"..handle_counter + handle_counter = handle_counter + 1 + + mp.register_script_message(handler_id, function (type, args) if t[type] then local completions, completion_pos, completion_append = t[type](unpack(utils.parse_json(args or "") or {})) @@ -44,18 +49,20 @@ local function register_event_handler(t) end if type == "closed" then - mp.unregister_script_message("input-event") + mp.unregister_script_message(handler_id) end end) + + return handler_id end function input.get(t) t.has_completions = t.complete ~= nil + t.client_name = mp.get_script_name() + t.handler_id = register_event_handler(t) mp.commandv("script-message-to", "console", "get-input", - mp.get_script_name(), utils.format_json(get_non_callbacks(t))) - - register_event_handler(t) + utils.format_json(get_non_callbacks(t))) end input.select = input.get From a9e56bedf372c5cc70c38e252a98a3c55c324b94 Mon Sep 17 00:00:00 2001 From: CogentRedTester Date: Mon, 12 Jan 2026 18:56:44 +1030 Subject: [PATCH 2/4] mp.input: remove race conditions when calling `input.terminate()` This commit ensures that `input.terminate()` can only close input requests made by the same script, and prevents any in-transit events for old input requests from being processed. Previously, there was no way to guarantee that the input request being terminated was the one intended; the asynchronous nature of the API meant that it was always possible (though unlikely) that another client may have activated its own input request while the termination request was in transit. This commit removes the race condition between different scripts calling `input.terminate()` by sending the script name alongside the termination message. In addition, when a script overwrites one of its own input requests, there may be incoming events still in transit. Some of these events may have a decent chance of calling `input.terminate()` if they are processed (e.g., `submit`). This commit avoids this issue by only processing `closed` requests once a new `input.get()` request is made. --- DOCS/man/lua.rst | 3 ++- player/javascript/defaults.js | 9 ++++++++- player/lua/console.lua | 8 ++++++-- player/lua/input.lua | 11 ++++++++++- 4 files changed, 26 insertions(+), 5 deletions(-) diff --git a/DOCS/man/lua.rst b/DOCS/man/lua.rst index 23c2008050670..f2e50745f2bc5 100644 --- a/DOCS/man/lua.rst +++ b/DOCS/man/lua.rst @@ -976,7 +976,8 @@ REPL. script name with ``prompt`` appended. ``input.terminate()`` - Close the console. + Closes any currently active input request. This will not close + requests made by other scripts. ``input.log(message, style, terminal_style)`` Add a line to the log buffer. ``style`` can contain additional ASS tags to diff --git a/player/javascript/defaults.js b/player/javascript/defaults.js index 13124d1c7d8de..b73b0533874d4 100644 --- a/player/javascript/defaults.js +++ b/player/javascript/defaults.js @@ -654,11 +654,16 @@ mp.options = { read_options: read_options }; * input *********************************************************************/ var input_handle_counter = 0; +var latest_handler_id; function register_event_handler(t) { var handler_id = "input-event/" + input_handle_counter++; + latest_handler_id = handler_id; mp.register_script_message(handler_id, function (type, args) { + if (latest_handler_id !== handler_id && type !== "closed") + return; + if (t[type]) { args = args ? JSON.parse(args) : []; var result = t[type].apply(null, args); @@ -685,7 +690,9 @@ mp.input = { mp.commandv("script-message-to", "console", "get-input", JSON.stringify(t)); }, terminate: function () { - mp.commandv("script-message-to", "console", "disable"); + mp.commandv("script-message-to", "console", "disable", JSON.stringify({ + client_name: mp.script_name, + })); }, log: function (message, style, terminal_style) { mp.commandv("script-message-to", "console", "log", JSON.stringify({ diff --git a/player/lua/console.lua b/player/lua/console.lua index 629e985801298..b0cd6b70aec76 100644 --- a/player/lua/console.lua +++ b/player/lua/console.lua @@ -1659,8 +1659,12 @@ set_active = function (active) render() end -mp.register_script_message("disable", function() - set_active(false) +mp.register_script_message("disable", function(message) + message = utils.parse_json(message or "") + + if not message or message.client_name == input_caller then + set_active(false) + end end) mp.register_script_message("get-input", function (args) diff --git a/player/lua/input.lua b/player/lua/input.lua index 558fef91b424f..102ad7a9f9072 100644 --- a/player/lua/input.lua +++ b/player/lua/input.lua @@ -19,6 +19,7 @@ local utils = require "mp.utils" local input = {} local handle_counter = 0 +local latest_handler_id local function get_non_callbacks(t) local non_callbacks = {} @@ -35,8 +36,14 @@ end local function register_event_handler(t) local handler_id = "input-event/"..handle_counter handle_counter = handle_counter + 1 + latest_handler_id = handler_id mp.register_script_message(handler_id, function (type, args) + -- do not process events (other than closed) for an input that has been overwritten + if latest_handler_id ~= handler_id and type ~= "closed" then + return + end + if t[type] then local completions, completion_pos, completion_append = t[type](unpack(utils.parse_json(args or "") or {})) @@ -68,7 +75,9 @@ end input.select = input.get function input.terminate() - mp.commandv("script-message-to", "console", "disable") + mp.commandv("script-message-to", "console", "disable", utils.format_json({ + client_name = mp.get_script_name(), + })) end function input.log(message, style, terminal_style) From 926102df254adf6fc6cba5f8be40d37277621a86 Mon Sep 17 00:00:00 2001 From: CogentRedTester Date: Mon, 12 Jan 2026 19:08:10 +1030 Subject: [PATCH 3/4] mp.input: avoid rare race condition when supplying completions It was previously possible for completion messages to be received by a new input request, if one was created while the message was in transit. Since it is trivial to avoid this by passing the script name and existing handle_id value, we may as well do so and guarantee that there will not be any data races. --- player/javascript/defaults.js | 9 +++++++-- player/lua/console.lua | 13 ++++++++----- player/lua/input.lua | 10 +++++++--- 3 files changed, 22 insertions(+), 10 deletions(-) diff --git a/player/javascript/defaults.js b/player/javascript/defaults.js index b73b0533874d4..c7816280cc725 100644 --- a/player/javascript/defaults.js +++ b/player/javascript/defaults.js @@ -669,8 +669,13 @@ function register_event_handler(t) { var result = t[type].apply(null, args); if (type == "complete" && result) { - mp.commandv("script-message-to", "console", "complete", - JSON.stringify(result[0]), result[1], result[2] || ""); + mp.commandv("script-message-to", "console", "complete", JSON.stringify({ + client_name: mp.script_name, + handler_id: handler_id, + list: result[0], + start_pos: result[1], + append: result[2] || "", + })); } } diff --git a/player/lua/console.lua b/player/lua/console.lua index b0cd6b70aec76..06bfcfe0e3bf7 100644 --- a/player/lua/console.lua +++ b/player/lua/console.lua @@ -1792,17 +1792,20 @@ mp.register_script_message("set-log", function (log) render() end) -mp.register_script_message("complete", function (list, start_pos, append) - if line ~= completion_old_line or cursor ~= completion_old_cursor then +mp.register_script_message("complete", function (message) + message = utils.parse_json(message) + + if message.client_name ~= input_caller or message.handler_id ~= input_caller_handler + or line ~= completion_old_line or cursor ~= completion_old_cursor then return end completion_buffer = {} selected_completion_index = 0 - local completions = utils.parse_json(list) + local completions = message.list table.sort(completions) - completion_pos = start_pos - completion_append = append + completion_pos = message.start_pos + completion_append = message.append for i, match in ipairs(fuzzy_find(line:sub(completion_pos, cursor - 1), completions)) do completion_buffer[i] = completions[match[1]] diff --git a/player/lua/input.lua b/player/lua/input.lua index 102ad7a9f9072..1486887bb314b 100644 --- a/player/lua/input.lua +++ b/player/lua/input.lua @@ -49,9 +49,13 @@ local function register_event_handler(t) t[type](unpack(utils.parse_json(args or "") or {})) if type == "complete" and completions then - mp.commandv("script-message-to", "console", "complete", - utils.format_json(completions), completion_pos, - completion_append or "") + mp.commandv("script-message-to", "console", "complete", utils.format_json({ + client_name = mp.get_script_name(), + handler_id = handler_id, + list = completions, + start_pos = completion_pos, + append = completion_append or "", + })) end end From df0658ffd41a79ce73171272415e4dc5aa0704ad Mon Sep 17 00:00:00 2001 From: CogentRedTester Date: Sat, 17 Jan 2026 16:08:06 +1030 Subject: [PATCH 4/4] mp.input: send buffer ids with logs to avoid race conditions This commit modifies the log methods in mp.input to always send the id of the latest `input.get()` request with log entries. Previously, the log methods applied to whichever input request happened to be open when the log message was received. Even when scripts used these methods correctly, there was the risk of sending a log to the wrong log buffer if the active input request changed while the log message was in transit; a race condition. Now the id of the latest `input.get()` request is sent alongside the log messages, preventing data races between scripts while also preventing those logs from being discarded. --- DOCS/man/lua.rst | 12 +++++++----- player/javascript/defaults.js | 33 +++++++++++++++++++++++---------- player/lua/console.lua | 25 ++++++++++++++++++------- player/lua/input.lua | 33 ++++++++++++++++++++++++--------- 4 files changed, 72 insertions(+), 31 deletions(-) diff --git a/DOCS/man/lua.rst b/DOCS/man/lua.rst index f2e50745f2bc5..3f55da35baa63 100644 --- a/DOCS/man/lua.rst +++ b/DOCS/man/lua.rst @@ -980,16 +980,18 @@ REPL. requests made by other scripts. ``input.log(message, style, terminal_style)`` - Add a line to the log buffer. ``style`` can contain additional ASS tags to - apply to ``message``, and ``terminal_style`` can contain escape sequences - that are used when the console is displayed in the terminal. + Add a line to the log buffer of the latest ``input.get()`` request. + ``style`` can contain additional ASS tags to apply to ``message``, + and ``terminal_style`` can contain escape sequences that are used + when the console is displayed in the terminal. ``input.log_error(message)`` - Helper to add a line to the log buffer with the same color as the one used + Helper to add an error line to the log buffer of the latest ``input.get()`` + request. The line is styled with the same color as the one used for commands that error. Useful when the user submits invalid input. ``input.set_log(log)`` - Replace the entire log buffer. + Replace the entire log buffer of the latest ``input.get()`` request. ``log`` is a table of strings, or tables with ``text``, ``style`` and ``terminal_style`` keys. diff --git a/player/javascript/defaults.js b/player/javascript/defaults.js index c7816280cc725..21d91e7994412 100644 --- a/player/javascript/defaults.js +++ b/player/javascript/defaults.js @@ -655,6 +655,7 @@ mp.options = { read_options: read_options }; *********************************************************************/ var input_handle_counter = 0; var latest_handler_id; +var latest_log_id; function register_event_handler(t) { var handler_id = "input-event/" + input_handle_counter++; @@ -686,13 +687,19 @@ function register_event_handler(t) { return handler_id; } +function input_request(t) { + t.has_completions = t.complete !== undefined; + t.client_name = mp.script_name; + t.handler_id = register_event_handler(t); + + mp.commandv("script-message-to", "console", "get-input", JSON.stringify(t)); +} + mp.input = { get: function(t) { - t.has_completions = t.complete !== undefined; - t.client_name = mp.script_name; - t.handler_id = register_event_handler(t); - - mp.commandv("script-message-to", "console", "get-input", JSON.stringify(t)); + t.id = t.id || mp.script_name + (t.prompt || ""); + latest_log_id = t.id; + return input_request(t); }, terminate: function () { mp.commandv("script-message-to", "console", "disable", JSON.stringify({ @@ -701,21 +708,27 @@ mp.input = { }, log: function (message, style, terminal_style) { mp.commandv("script-message-to", "console", "log", JSON.stringify({ + log_id: latest_log_id, text: message, style: style, terminal_style: terminal_style, })); }, log_error: function (message) { - mp.commandv("script-message-to", "console", "log", - JSON.stringify({ text: message, error: true })); + mp.commandv("script-message-to", "console", "log", JSON.stringify({ + log_id: latest_log_id, + text: message, + error: true, + })); }, set_log: function (log) { - mp.commandv("script-message-to", "console", "set-log", - JSON.stringify(log)); + if (latest_log_id) { + mp.commandv("script-message-to", "console", "set-log", + latest_log_id, JSON.stringify(log)); + } } } -mp.input.select = mp.input.get +mp.input.select = input_request; /********************************************************************** * various diff --git a/player/lua/console.lua b/player/lua/console.lua index 06bfcfe0e3bf7..6603b685a3546 100644 --- a/player/lua/console.lua +++ b/player/lua/console.lua @@ -1710,7 +1710,7 @@ mp.register_script_message("get-input", function (args) else selectable_items = nil unbind_mouse() - id = args.id or args.client_name .. prompt + id = args.id log_offset = 0 completion_buffer = {} autoselect_completion = args.autoselect_completion @@ -1740,8 +1740,13 @@ end) -- Add a line to the log buffer mp.register_script_message("log", function (message) - local log_buffer = log_buffers[id] - message = utils.parse_json(message) + message = utils.parse_json(message or "") + if not message or not message.log_id then + return + end + + local log_buffer = log_buffers[message.log_id] + if not log_buffer then return end log_buffer[#log_buffer + 1] = { text = message.text, @@ -1754,7 +1759,7 @@ mp.register_script_message("log", function (message) table.remove(log_buffer, 1) end - if not open then + if not open or message.log_id ~= id then return end @@ -1770,9 +1775,13 @@ mp.register_script_message("log", function (message) end end) -mp.register_script_message("set-log", function (log) +mp.register_script_message("set-log", function (log_id, log) + if not log_id or not log then + return + end + log = utils.parse_json(log) - log_buffers[id] = {} + log_buffers[log_id] = {} for i = 1, #log do if type(log[i]) == "table" then @@ -1789,7 +1798,9 @@ mp.register_script_message("set-log", function (log) end end - render() + if log_id == id then + render() + end end) mp.register_script_message("complete", function (message) diff --git a/player/lua/input.lua b/player/lua/input.lua index 1486887bb314b..d0fa59c1f77b8 100644 --- a/player/lua/input.lua +++ b/player/lua/input.lua @@ -20,6 +20,7 @@ local input = {} local handle_counter = 0 local latest_handler_id +local latest_log_id local function get_non_callbacks(t) local non_callbacks = {} @@ -67,7 +68,7 @@ local function register_event_handler(t) return handler_id end -function input.get(t) +local function input_request(t) t.has_completions = t.complete ~= nil t.client_name = mp.get_script_name() t.handler_id = register_event_handler(t) @@ -76,7 +77,14 @@ function input.get(t) utils.format_json(get_non_callbacks(t))) end -input.select = input.get +function input.get(t) + -- input.select does not support log buffers, so cannot override the latest id. + t.id = t.id or mp.get_script_name()..(t.prompt or "") + latest_log_id = t.id + return input_request(t) +end + +input.select = input_request function input.terminate() mp.commandv("script-message-to", "console", "disable", utils.format_json({ @@ -86,19 +94,26 @@ end function input.log(message, style, terminal_style) mp.commandv("script-message-to", "console", "log", utils.format_json({ - text = message, - style = style, - terminal_style = terminal_style, - })) + log_id = latest_log_id, + text = message, + style = style, + terminal_style = terminal_style, + })) end function input.log_error(message) - mp.commandv("script-message-to", "console", "log", - utils.format_json({ text = message, error = true })) + mp.commandv("script-message-to", "console", "log", utils.format_json({ + log_id = latest_log_id, + text = message, + error = true, + })) end function input.set_log(log) - mp.commandv("script-message-to", "console", "set-log", utils.format_json(log)) + if latest_log_id then + mp.commandv("script-message-to", "console", "set-log", + latest_log_id, utils.format_json(log)) + end end return input