From 871f7a152a3f0b3561a627d21f7417d10ac9c25c Mon Sep 17 00:00:00 2001 From: Guido Cella Date: Tue, 2 Jan 2024 18:58:32 +0100 Subject: scripting: add mp.input This lets scripts get textual input from the user using console.lua. --- player/lua/console.lua | 195 ++++++++++++++++++++++++++++++++++++++++--------- player/lua/input.lua | 66 +++++++++++++++++ player/lua/meson.build | 3 +- 3 files changed, 228 insertions(+), 36 deletions(-) create mode 100644 player/lua/input.lua (limited to 'player/lua') 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 []", return 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 []", return 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 . +]] + +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), -- cgit v1.2.3