diff options
author | Guido Cella <guido@guidocella.xyz> | 2024-01-02 18:58:32 +0100 |
---|---|---|
committer | Dudemanguy <random342@airmail.cc> | 2024-01-13 02:53:08 +0000 |
commit | 871f7a152a3f0b3561a627d21f7417d10ac9c25c (patch) | |
tree | c793a9c700b2e52b2a896234b679af5ec9c3f006 | |
parent | 2dd3951a9c05e7a4fc0a614f13de1bfe0e7f132f (diff) | |
download | mpv-871f7a152a3f0b3561a627d21f7417d10ac9c25c.tar.bz2 mpv-871f7a152a3f0b3561a627d21f7417d10ac9c25c.tar.xz |
scripting: add mp.input
This lets scripts get textual input from the user using console.lua.
-rw-r--r-- | DOCS/interface-changes.rst | 1 | ||||
-rw-r--r-- | DOCS/man/javascript.rst | 23 | ||||
-rw-r--r-- | DOCS/man/lua.rst | 82 | ||||
-rw-r--r-- | player/javascript/defaults.js | 47 | ||||
-rw-r--r-- | player/lua.c | 3 | ||||
-rw-r--r-- | player/lua/console.lua | 195 | ||||
-rw-r--r-- | player/lua/input.lua | 66 | ||||
-rw-r--r-- | player/lua/meson.build | 3 |
8 files changed, 378 insertions, 42 deletions
diff --git a/DOCS/interface-changes.rst b/DOCS/interface-changes.rst index 558392b00f..d3a48804fe 100644 --- a/DOCS/interface-changes.rst +++ b/DOCS/interface-changes.rst @@ -36,6 +36,7 @@ Interface changes - `--screenshot-avif-opts` defaults to lossless screenshot - rename key `MP_KEY_BACK` to `MP_KEY_GO_BACK` - add `--sub-filter-sdh-enclosures` option + - added the `mp.input` scripting API to query the user for textual input --- mpv 0.37.0 --- - `--save-position-on-quit` and its associated commands now store state files in %LOCALAPPDATA% instead of %APPDATA% directory by default on Windows. diff --git a/DOCS/man/javascript.rst b/DOCS/man/javascript.rst index bdbb04b198..0edb01f674 100644 --- a/DOCS/man/javascript.rst +++ b/DOCS/man/javascript.rst @@ -27,16 +27,17 @@ otherwise, the documented Lua options, script directories, loading, etc apply to JavaScript files too. Script initialization and lifecycle is the same as with Lua, and most of the Lua -functions at the modules ``mp``, ``mp.utils``, ``mp.msg`` and ``mp.options`` are -available to JavaScript with identical APIs - including running commands, -getting/setting properties, registering events/key-bindings/hooks, etc. +functions in the modules ``mp``, ``mp.utils``, ``mp.msg``, ``mp.options`` and +``mp.input`` are available to JavaScript with identical APIs - including running +commands, getting/setting properties, registering events/key-bindings/hooks, +etc. Differences from Lua -------------------- -No need to load modules. ``mp``, ``mp.utils``, ``mp.msg`` and ``mp.options`` -are preloaded, and you can use e.g. ``var cwd = mp.utils.getcwd();`` without -prior setup. +No need to load modules. ``mp``, ``mp.utils``, ``mp.msg``, ``mp.options`` and +``mp.input`` are preloaded, and you can use e.g. ``var cwd = +mp.utils.getcwd();`` without prior setup. Errors are slightly different. Where the Lua APIs return ``nil`` for error, the JavaScript ones return ``undefined``. Where Lua returns ``something, error`` @@ -195,6 +196,16 @@ meta-paths like ``~~/foo`` (other JS file functions do expand meta paths). ``mp.options.read_options(obj [, identifier [, on_update]])`` (types: string/boolean/number) +``mp.input.get(obj)`` (LE) + +``mp.input.terminate()`` + +``mp.input.log(message, style)`` + +``mp.input.log_error(message)`` + +``mp.input.set_log(log)`` + Additional utilities -------------------- diff --git a/DOCS/man/lua.rst b/DOCS/man/lua.rst index 5708e19372..ac062aec6c 100644 --- a/DOCS/man/lua.rst +++ b/DOCS/man/lua.rst @@ -862,6 +862,88 @@ strictly part of the guaranteed API. Turn the given value into a string. Formats tables and their contents. This doesn't do anything special; it is only needed because Lua is terrible. +mp.input functions +-------------------- + +This module lets scripts get textual input from the user using the console +REPL. + +``input.get(table)`` + Show the console to let the user enter text. + + The following entries of ``table`` are read: + + ``prompt`` + The string to be displayed before the input field. + + ``submit`` + A callback invoked when the user presses Enter. The first argument is + the text in the console. You can close the console from within the + callback by calling ``input.terminate()``. If you don't, the console + stays open and the user can input more text. + + ``opened`` + A callback invoked when the console is shown. This can be used to + present a list of options with ``input.set_log()``. + + ``edited`` + A callback invoked when the text changes. This can be used to filter a + list of options based on what the user typed with ``input.set_log()``, + like dmenu does. The first argument is the text in the console. + + ``complete`` + A callback invoked when the user presses TAB. The first argument is the + text before the cursor. The callback should return a table of the string + candidate completion values and the 1-based cursor position from which + the completion starts. console.lua will filter the suggestions beginning + with the the text between this position and the cursor, sort them + alphabetically, insert their longest common prefix, and show them when + there are multiple ones. + + ``closed`` + A callback invoked when the console is hidden, either because + ``input.terminate()`` was invoked from the other callbacks, or because + the user closed it with a key binding. The first argument is the text in + the console, and the second argument is the cursor position. + + ``default_text`` + A string to pre-fill the input field with. + + ``cursor_position`` + The initial cursor position, starting from 1. + + ``id`` + An identifier that determines which input history and log buffer to use + among the ones stored for ``input.get()`` calls. The input histories + and logs are stored in memory and do not persist across different mpv + invocations. Defaults to the calling script name with ``prompt`` + appended. + +``input.terminate()`` + Close the console. + +``input.log(message, style)`` + Add a line to the log buffer. ``style`` can contain additional ASS tags to + apply to ``message``. + +``input.log_error(message)`` + Helper to add a line to the log buffer with the same color as the one the + console uses for errors. Useful when the user submits invalid input. + +``input.set_log(log)`` + Replace the entire log buffer. + + ``log`` is a table of strings, or tables with ``text`` and ``style`` keys. + + Example: + + :: + + input.set_log({ + "regular text", + { style = "{\\c&H7a77f2&}", text = "error text" } + }) + Events ------ diff --git a/player/javascript/defaults.js b/player/javascript/defaults.js index be4fca61c9..cc2ca3f232 100644 --- a/player/javascript/defaults.js +++ b/player/javascript/defaults.js @@ -643,6 +643,53 @@ function read_options(opts, id, on_update, conf_override) { mp.options = { read_options: read_options }; /********************************************************************** +* input +*********************************************************************/ +mp.input = { + get: function(t) { + mp.commandv("script-message-to", "console", "get-input", mp.script_name, + JSON.stringify({ + prompt: t.prompt, + default_text: t.default_text, + cursor_position: t.cursor_position, + id: t.id, + })); + + mp.register_script_message("input-event", function (type, text, cursor_position) { + if (t[type]) { + var result = t[type](text, cursor_position); + + if (type == "complete" && result) { + mp.commandv("script-message-to", "console", "complete", + JSON.stringify(result[0]), result[1]); + } + } + + if (type == "closed") { + mp.unregister_script_message("input-event"); + } + }) + + return true; + }, + terminate: function () { + mp.commandv("script-message-to", "console", "disable"); + }, + log: function (message, style) { + mp.commandv("script-message-to", "console", "log", + JSON.stringify({ text: message, style: style })); + }, + log_error: function (message) { + mp.commandv("script-message-to", "console", "log", + JSON.stringify({ text: message, error: true })); + }, + set_log: function (log) { + mp.commandv("script-message-to", "console", "set-log", + JSON.stringify(log)); + } +} + +/********************************************************************** * various *********************************************************************/ g.print = mp.msg.info; // convenient alias diff --git a/player/lua.c b/player/lua.c index 549d8d9e55..b2548387f6 100644 --- a/player/lua.c +++ b/player/lua.c @@ -61,6 +61,9 @@ static const char * const builtin_lua_scripts[][2] = { {"mp.assdraw", # include "player/lua/assdraw.lua.inc" }, + {"mp.input", +# include "player/lua/input.lua.inc" + }, {"mp.options", # include "player/lua/options.lua.inc" }, diff --git a/player/lua/console.lua b/player/lua/console.lua index 1ddfca151a..92c2195623 100644 --- a/player/lua/console.lua +++ b/player/lua/console.lua @@ -82,11 +82,17 @@ local insert_mode = false local pending_update = false local line = '' local cursor = 1 -local history = {} +local default_prompt = '>' +local prompt = default_prompt +local default_id = 'default' +local id = default_id +local histories = {[id] = {}} +local history = histories[id] local history_pos = 1 -local log_buffer = {} +local log_buffers = {[id] = {}} local key_bindings = {} local global_margins = { t = 0, b = 0 } +local input_caller local suggestion_buffer = {} local selected_suggestion_index @@ -94,6 +100,8 @@ local completion_start_position local completion_append local file_commands = {} local path_separator = platform == 'windows' and '\\' or '/' +local completion_old_line +local completion_old_cursor local update_timer = nil update_timer = mp.add_periodic_timer(0.05, function() @@ -190,6 +198,7 @@ end -- Add a line to the log buffer (which is limited to 100 lines) function log_add(style, text) + local log_buffer = log_buffers[id] log_buffer[#log_buffer + 1] = { style = style, text = text } if #log_buffer > 100 then table.remove(log_buffer, 1) @@ -321,7 +330,7 @@ local function print_to_terminal() end local log = '' - for _, log_line in ipairs(log_buffer) do + for _, log_line in ipairs(log_buffers[id]) do log = log .. log_line.text end @@ -337,8 +346,9 @@ local function print_to_terminal() after_cur = ' ' end - mp.osd_message(log .. suggestions .. '> ' .. before_cur .. '\027[7m' .. - after_cur:sub(1, 1) .. '\027[0m' .. after_cur:sub(2), 999) + mp.osd_message(log .. suggestions .. prompt .. ' ' .. before_cur .. + '\027[7m' .. after_cur:sub(1, 1) .. '\027[0m' .. + after_cur:sub(2), 999) end -- Render the REPL and console as an ASS OSD @@ -407,6 +417,7 @@ function update() local suggestion_ass = style .. styles.suggestion .. suggestions local log_ass = '' + local log_buffer = log_buffers[id] local log_messages = #log_buffer local log_max_lines = math.max(0, lines_max - rows) if log_max_lines < log_messages then @@ -423,7 +434,7 @@ function update() if #suggestions > 0 then ass:append(suggestion_ass .. '\\N') end - ass:append(style .. '> ' .. before_cur) + ass:append(style .. ass_escape(prompt) .. ' ' .. before_cur) ass:append(cglyph) ass:append(style .. after_cur) @@ -432,7 +443,7 @@ function update() ass:new_event() ass:an(1) ass:pos(2, screeny - 2 - global_margins.b * screeny) - ass:append(style .. '{\\alpha&HFF&}> ' .. before_cur) + ass:append(style .. '{\\alpha&HFF&}' .. ass_escape(prompt) .. ' ' .. before_cur) ass:append(cglyph) ass:append(style .. '{\\alpha&HFF&}' .. after_cur) @@ -446,12 +457,28 @@ function set_active(active) repl_active = true insert_mode = false mp.enable_key_bindings('console-input', 'allow-hide-cursor+allow-vo-dragging') - mp.enable_messages('terminal-default') define_key_bindings() + + if not input_caller then + prompt = default_prompt + id = default_id + history = histories[id] + history_pos = #history + 1 + mp.enable_messages('terminal-default') + end else repl_active = false + suggestion_buffer = {} undefine_key_bindings() mp.enable_messages('silent:terminal-default') + + if input_caller then + mp.commandv('script-message-to', input_caller, 'input-event', + 'closed', line, cursor) + input_caller = nil + line = '' + cursor = 1 + end collectgarbage() end update() @@ -513,6 +540,16 @@ function len_utf8(str) return len end +local function handle_edit() + suggestion_buffer = {} + update() + + if input_caller then + mp.commandv('script-message-to', input_caller, 'input-event', 'edited', + line) + end +end + -- Insert a character at the current cursor position (any_unicode) function handle_char_input(c) if insert_mode then @@ -521,8 +558,7 @@ function handle_char_input(c) line = line:sub(1, cursor - 1) .. c .. line:sub(cursor) end cursor = cursor + #c - suggestion_buffer = {} - update() + handle_edit() end -- Remove the character behind the cursor (Backspace) @@ -531,16 +567,14 @@ function handle_backspace() local prev = prev_utf8(line, cursor) line = line:sub(1, prev - 1) .. line:sub(cursor) cursor = prev - suggestion_buffer = {} - update() + handle_edit() end -- Remove the character in front of the cursor (Del) function handle_del() if cursor > line:len() then return end line = line:sub(1, cursor - 1) .. line:sub(next_utf8(line, cursor)) - suggestion_buffer = {} - update() + handle_edit() end -- Toggle insert mode (Ins) @@ -568,8 +602,7 @@ function clear() cursor = 1 insert_mode = false history_pos = #history + 1 - suggestion_buffer = {} - update() + handle_edit() end -- Close the REPL if the current line is empty, otherwise delete the next @@ -642,20 +675,25 @@ end -- Run the current command and clear the line (Enter) function handle_enter() - if line == '' then + if line == '' and input_caller == nil then return end - if history[#history] ~= line then + if history[#history] ~= line and line ~= '' then history_add(line) end - -- match "help [<text>]", return <text> or "", strip all whitespace - local help = line:match('^%s*help%s+(.-)%s*$') or - (line:match('^%s*help$') and '') - if help then - help_command(help) + if input_caller then + mp.commandv('script-message-to', input_caller, 'input-event', 'submit', + line) else - mp.command(line) + -- match "help [<text>]", return <text> or "", strip all whitespace + local help = line:match('^%s*help%s+(.-)%s*$') or + (line:match('^%s*help$') and '') + if help then + help_command(help) + else + mp.command(line) + end end clear() @@ -1025,6 +1063,14 @@ function complete(backwards) return end + if input_caller then + completion_old_line = line + completion_old_cursor = cursor + mp.commandv('script-message-to', input_caller, 'input-event', + 'complete', line:sub(1, cursor - 1)) + return + end + local before_cur = line:sub(1, cursor - 1) local after_cur = line:sub(cursor) @@ -1111,8 +1157,7 @@ function del_word() before_cur = before_cur:gsub('[^%s]+%s*$', '', 1) line = before_cur .. after_cur cursor = before_cur:len() + 1 - suggestion_buffer = {} - update() + handle_edit() end -- Delete from the cursor to the end of the word (Ctrl+Del) @@ -1124,28 +1169,25 @@ function del_next_word() after_cur = after_cur:gsub('^%s*[^%s]+', '', 1) line = before_cur .. after_cur - suggestion_buffer = {} - update() + handle_edit() end -- Delete from the cursor to the end of the line (Ctrl+K) function del_to_eol() line = line:sub(1, cursor - 1) - suggestion_buffer = {} - update() + handle_edit() end -- Delete from the cursor back to the start of the line (Ctrl+U) function del_to_start() line = line:sub(cursor) cursor = 1 - suggestion_buffer = {} - update() + handle_edit() end -- Empty the log buffer of all messages (Ctrl+L) function clear_log_buffer() - log_buffer = {} + log_buffers[id] = {} update() end @@ -1212,8 +1254,7 @@ function paste(clip) local after_cur = line:sub(cursor) line = before_cur .. text .. after_cur cursor = cursor + text:len() - suggestion_buffer = {} - update() + handle_edit() end -- List of input bindings. This is a weird mashup between common GUI text-input @@ -1318,11 +1359,95 @@ mp.add_key_binding(nil, 'enable', function() set_active(true) end) +mp.register_script_message('disable', function() + set_active(false) +end) + -- Add a script-message to show the REPL and fill it with the provided text mp.register_script_message('type', function(text, cursor_pos) show_and_type(text, cursor_pos) end) +mp.register_script_message('get-input', function (script_name, args) + if repl_active then + return + end + + input_caller = script_name + args = utils.parse_json(args) + prompt = args.prompt or default_prompt + line = args.default_text or '' + cursor = tonumber(args.cursor_position) or line:len() + 1 + id = args.id or script_name .. prompt + if histories[id] == nil then + histories[id] = {} + log_buffers[id] = {} + end + history = histories[id] + history_pos = #history + 1 + + set_active(true) + mp.commandv('script-message-to', input_caller, 'input-event', 'opened') +end) + +mp.register_script_message('log', function (message) + -- input.get's edited handler is invoked after submit, so avoid modifying + -- the default log. + if input_caller == nil then + return + end + + message = utils.parse_json(message) + + log_add(message.error and styles.error or message.style or '', + message.text .. '\n') +end) + +mp.register_script_message('set-log', function (log) + if input_caller == nil then + return + end + + log = utils.parse_json(log) + log_buffers[id] = {} + + for i = 1, #log do + if type(log[i]) == 'table' then + log[i].text = log[i].text .. '\n' + log_buffers[id][i] = log[i] + else + log_buffers[id][i] = { + style = '', + text = log[i] .. '\n', + } + end + end + + update() +end) + +mp.register_script_message('complete', function(list, start_pos) + if line ~= completion_old_line or cursor ~= completion_old_cursor then + return + end + + local completions, prefix = complete_match(line:sub(start_pos, cursor), + utils.parse_json(list)) + local before_cur = line:sub(1, start_pos - 1) .. prefix + local after_cur = line:sub(cursor) + cursor = before_cur:len() + 1 + line = before_cur .. after_cur + + if #completions > 1 then + suggestion_buffer = completions + selected_suggestion_index = 0 + completion_start_position = start_pos + completion_append = '' + end + + update() +end) + -- Redraw the REPL when the OSD size changes. This is needed because the -- PlayRes of the OSD will need to be adjusted. mp.observe_property('osd-width', 'native', update) diff --git a/player/lua/input.lua b/player/lua/input.lua new file mode 100644 index 0000000000..a73128ec7d --- /dev/null +++ b/player/lua/input.lua @@ -0,0 +1,66 @@ +--[[ +This file is part of mpv. + +mpv is free software; you can redistribute it and/or +modify it under the terms of the GNU Lesser General Public +License as published by the Free Software Foundation; either +version 2.1 of the License, or (at your option) any later version. + +mpv is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public +License along with mpv. If not, see <http://www.gnu.org/licenses/>. +]] + +local utils = require "mp.utils" +local input = {} + +function input.get(t) + mp.commandv("script-message-to", "console", "get-input", + mp.get_script_name(), utils.format_json({ + prompt = t.prompt, + default_text = t.default_text, + cursor_position = t.cursor_position, + id = t.id, + })) + + mp.register_script_message("input-event", function (type, text, cursor_position) + if t[type] then + local suggestions, completion_start_position = t[type](text, cursor_position) + + if type == "complete" and suggestions then + mp.commandv("script-message-to", "console", "complete", + utils.format_json(suggestions), completion_start_position) + end + end + + if type == "closed" then + mp.unregister_script_message("input-event") + end + end) + + return true +end + +function input.terminate() + mp.commandv("script-message-to", "console", "disable") +end + +function input.log(message, style) + mp.commandv("script-message-to", "console", "log", + utils.format_json({ text = message, style = style })) +end + +function input.log_error(message) + mp.commandv("script-message-to", "console", "log", + utils.format_json({ text = message, error = true })) +end + +function input.set_log(log) + mp.commandv("script-message-to", "console", "set-log", utils.format_json(log)) +end + +return input diff --git a/player/lua/meson.build b/player/lua/meson.build index 362c87cbb7..1d87938f1a 100644 --- a/player/lua/meson.build +++ b/player/lua/meson.build @@ -1,5 +1,6 @@ lua_files = ['defaults.lua', 'assdraw.lua', 'options.lua', 'osc.lua', - 'ytdl_hook.lua', 'stats.lua', 'console.lua', 'auto_profiles.lua'] + 'ytdl_hook.lua', 'stats.lua', 'console.lua', 'auto_profiles.lua', + 'input.lua'] foreach file: lua_files lua_file = custom_target(file, input: join_paths(source_root, 'player', 'lua', file), |