1
0
mirror of https://github.com/mpv-player/mpv synced 2025-04-24 20:29:53 +00:00

scripting: add mp.input

This lets scripts get textual input from the user using console.lua.
This commit is contained in:
Guido Cella 2024-01-02 18:58:32 +01:00 committed by Dudemanguy
parent 2dd3951a9c
commit 871f7a152a
8 changed files with 378 additions and 42 deletions

View File

@ -36,6 +36,7 @@ Interface changes
- `--screenshot-avif-opts` defaults to lossless screenshot - `--screenshot-avif-opts` defaults to lossless screenshot
- rename key `MP_KEY_BACK` to `MP_KEY_GO_BACK` - rename key `MP_KEY_BACK` to `MP_KEY_GO_BACK`
- add `--sub-filter-sdh-enclosures` option - add `--sub-filter-sdh-enclosures` option
- added the `mp.input` scripting API to query the user for textual input
--- mpv 0.37.0 --- --- mpv 0.37.0 ---
- `--save-position-on-quit` and its associated commands now store state files - `--save-position-on-quit` and its associated commands now store state files
in %LOCALAPPDATA% instead of %APPDATA% directory by default on Windows. in %LOCALAPPDATA% instead of %APPDATA% directory by default on Windows.

View File

@ -27,16 +27,17 @@ otherwise, the documented Lua options, script directories, loading, etc apply to
JavaScript files too. JavaScript files too.
Script initialization and lifecycle is the same as with Lua, and most of the Lua 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 functions in the modules ``mp``, ``mp.utils``, ``mp.msg``, ``mp.options`` and
available to JavaScript with identical APIs - including running commands, ``mp.input`` are available to JavaScript with identical APIs - including running
getting/setting properties, registering events/key-bindings/hooks, etc. commands, getting/setting properties, registering events/key-bindings/hooks,
etc.
Differences from Lua Differences from Lua
-------------------- --------------------
No need to load modules. ``mp``, ``mp.utils``, ``mp.msg`` and ``mp.options`` No need to load modules. ``mp``, ``mp.utils``, ``mp.msg``, ``mp.options`` and
are preloaded, and you can use e.g. ``var cwd = mp.utils.getcwd();`` without ``mp.input`` are preloaded, and you can use e.g. ``var cwd =
prior setup. mp.utils.getcwd();`` without prior setup.
Errors are slightly different. Where the Lua APIs return ``nil`` for error, Errors are slightly different. Where the Lua APIs return ``nil`` for error,
the JavaScript ones return ``undefined``. Where Lua returns ``something, 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: ``mp.options.read_options(obj [, identifier [, on_update]])`` (types:
string/boolean/number) 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 Additional utilities
-------------------- --------------------

View File

@ -862,6 +862,88 @@ strictly part of the guaranteed API.
Turn the given value into a string. Formats tables and their contents. This 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. 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 Events
------ ------

View File

@ -642,6 +642,53 @@ function read_options(opts, id, on_update, conf_override) {
mp.options = { read_options: read_options }; 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 * various
*********************************************************************/ *********************************************************************/

View File

@ -60,6 +60,9 @@ static const char * const builtin_lua_scripts[][2] = {
}, },
{"mp.assdraw", {"mp.assdraw",
# include "player/lua/assdraw.lua.inc" # include "player/lua/assdraw.lua.inc"
},
{"mp.input",
# include "player/lua/input.lua.inc"
}, },
{"mp.options", {"mp.options",
# include "player/lua/options.lua.inc" # include "player/lua/options.lua.inc"

View File

@ -82,11 +82,17 @@ local insert_mode = false
local pending_update = false local pending_update = false
local line = '' local line = ''
local cursor = 1 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 history_pos = 1
local log_buffer = {} local log_buffers = {[id] = {}}
local key_bindings = {} local key_bindings = {}
local global_margins = { t = 0, b = 0 } local global_margins = { t = 0, b = 0 }
local input_caller
local suggestion_buffer = {} local suggestion_buffer = {}
local selected_suggestion_index local selected_suggestion_index
@ -94,6 +100,8 @@ local completion_start_position
local completion_append local completion_append
local file_commands = {} local file_commands = {}
local path_separator = platform == 'windows' and '\\' or '/' local path_separator = platform == 'windows' and '\\' or '/'
local completion_old_line
local completion_old_cursor
local update_timer = nil local update_timer = nil
update_timer = mp.add_periodic_timer(0.05, function() 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) -- Add a line to the log buffer (which is limited to 100 lines)
function log_add(style, text) function log_add(style, text)
local log_buffer = log_buffers[id]
log_buffer[#log_buffer + 1] = { style = style, text = text } log_buffer[#log_buffer + 1] = { style = style, text = text }
if #log_buffer > 100 then if #log_buffer > 100 then
table.remove(log_buffer, 1) table.remove(log_buffer, 1)
@ -321,7 +330,7 @@ local function print_to_terminal()
end end
local log = '' local log = ''
for _, log_line in ipairs(log_buffer) do for _, log_line in ipairs(log_buffers[id]) do
log = log .. log_line.text log = log .. log_line.text
end end
@ -337,8 +346,9 @@ local function print_to_terminal()
after_cur = ' ' after_cur = ' '
end end
mp.osd_message(log .. suggestions .. '> ' .. before_cur .. '\027[7m' .. mp.osd_message(log .. suggestions .. prompt .. ' ' .. before_cur ..
after_cur:sub(1, 1) .. '\027[0m' .. after_cur:sub(2), 999) '\027[7m' .. after_cur:sub(1, 1) .. '\027[0m' ..
after_cur:sub(2), 999)
end end
-- Render the REPL and console as an ASS OSD -- Render the REPL and console as an ASS OSD
@ -407,6 +417,7 @@ function update()
local suggestion_ass = style .. styles.suggestion .. suggestions local suggestion_ass = style .. styles.suggestion .. suggestions
local log_ass = '' local log_ass = ''
local log_buffer = log_buffers[id]
local log_messages = #log_buffer local log_messages = #log_buffer
local log_max_lines = math.max(0, lines_max - rows) local log_max_lines = math.max(0, lines_max - rows)
if log_max_lines < log_messages then if log_max_lines < log_messages then
@ -423,7 +434,7 @@ function update()
if #suggestions > 0 then if #suggestions > 0 then
ass:append(suggestion_ass .. '\\N') ass:append(suggestion_ass .. '\\N')
end end
ass:append(style .. '> ' .. before_cur) ass:append(style .. ass_escape(prompt) .. ' ' .. before_cur)
ass:append(cglyph) ass:append(cglyph)
ass:append(style .. after_cur) ass:append(style .. after_cur)
@ -432,7 +443,7 @@ function update()
ass:new_event() ass:new_event()
ass:an(1) ass:an(1)
ass:pos(2, screeny - 2 - global_margins.b * screeny) 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(cglyph)
ass:append(style .. '{\\alpha&HFF&}' .. after_cur) ass:append(style .. '{\\alpha&HFF&}' .. after_cur)
@ -446,12 +457,28 @@ function set_active(active)
repl_active = true repl_active = true
insert_mode = false insert_mode = false
mp.enable_key_bindings('console-input', 'allow-hide-cursor+allow-vo-dragging') mp.enable_key_bindings('console-input', 'allow-hide-cursor+allow-vo-dragging')
mp.enable_messages('terminal-default')
define_key_bindings() 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 else
repl_active = false repl_active = false
suggestion_buffer = {}
undefine_key_bindings() undefine_key_bindings()
mp.enable_messages('silent:terminal-default') 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() collectgarbage()
end end
update() update()
@ -513,6 +540,16 @@ function len_utf8(str)
return len return len
end 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) -- Insert a character at the current cursor position (any_unicode)
function handle_char_input(c) function handle_char_input(c)
if insert_mode then if insert_mode then
@ -521,8 +558,7 @@ function handle_char_input(c)
line = line:sub(1, cursor - 1) .. c .. line:sub(cursor) line = line:sub(1, cursor - 1) .. c .. line:sub(cursor)
end end
cursor = cursor + #c cursor = cursor + #c
suggestion_buffer = {} handle_edit()
update()
end end
-- Remove the character behind the cursor (Backspace) -- Remove the character behind the cursor (Backspace)
@ -531,16 +567,14 @@ function handle_backspace()
local prev = prev_utf8(line, cursor) local prev = prev_utf8(line, cursor)
line = line:sub(1, prev - 1) .. line:sub(cursor) line = line:sub(1, prev - 1) .. line:sub(cursor)
cursor = prev cursor = prev
suggestion_buffer = {} handle_edit()
update()
end end
-- Remove the character in front of the cursor (Del) -- Remove the character in front of the cursor (Del)
function handle_del() function handle_del()
if cursor > line:len() then return end if cursor > line:len() then return end
line = line:sub(1, cursor - 1) .. line:sub(next_utf8(line, cursor)) line = line:sub(1, cursor - 1) .. line:sub(next_utf8(line, cursor))
suggestion_buffer = {} handle_edit()
update()
end end
-- Toggle insert mode (Ins) -- Toggle insert mode (Ins)
@ -568,8 +602,7 @@ function clear()
cursor = 1 cursor = 1
insert_mode = false insert_mode = false
history_pos = #history + 1 history_pos = #history + 1
suggestion_buffer = {} handle_edit()
update()
end end
-- Close the REPL if the current line is empty, otherwise delete the next -- Close the REPL if the current line is empty, otherwise delete the next
@ -642,13 +675,17 @@ end
-- Run the current command and clear the line (Enter) -- Run the current command and clear the line (Enter)
function handle_enter() function handle_enter()
if line == '' then if line == '' and input_caller == nil then
return return
end end
if history[#history] ~= line then if history[#history] ~= line and line ~= '' then
history_add(line) history_add(line)
end end
if input_caller then
mp.commandv('script-message-to', input_caller, 'input-event', 'submit',
line)
else
-- match "help [<text>]", return <text> or "", strip all whitespace -- match "help [<text>]", return <text> or "", strip all whitespace
local help = line:match('^%s*help%s+(.-)%s*$') or local help = line:match('^%s*help%s+(.-)%s*$') or
(line:match('^%s*help$') and '') (line:match('^%s*help$') and '')
@ -657,6 +694,7 @@ function handle_enter()
else else
mp.command(line) mp.command(line)
end end
end
clear() clear()
end end
@ -1025,6 +1063,14 @@ function complete(backwards)
return return
end 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 before_cur = line:sub(1, cursor - 1)
local after_cur = line:sub(cursor) local after_cur = line:sub(cursor)
@ -1111,8 +1157,7 @@ function del_word()
before_cur = before_cur:gsub('[^%s]+%s*$', '', 1) before_cur = before_cur:gsub('[^%s]+%s*$', '', 1)
line = before_cur .. after_cur line = before_cur .. after_cur
cursor = before_cur:len() + 1 cursor = before_cur:len() + 1
suggestion_buffer = {} handle_edit()
update()
end end
-- Delete from the cursor to the end of the word (Ctrl+Del) -- 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) after_cur = after_cur:gsub('^%s*[^%s]+', '', 1)
line = before_cur .. after_cur line = before_cur .. after_cur
suggestion_buffer = {} handle_edit()
update()
end end
-- Delete from the cursor to the end of the line (Ctrl+K) -- Delete from the cursor to the end of the line (Ctrl+K)
function del_to_eol() function del_to_eol()
line = line:sub(1, cursor - 1) line = line:sub(1, cursor - 1)
suggestion_buffer = {} handle_edit()
update()
end end
-- Delete from the cursor back to the start of the line (Ctrl+U) -- Delete from the cursor back to the start of the line (Ctrl+U)
function del_to_start() function del_to_start()
line = line:sub(cursor) line = line:sub(cursor)
cursor = 1 cursor = 1
suggestion_buffer = {} handle_edit()
update()
end end
-- Empty the log buffer of all messages (Ctrl+L) -- Empty the log buffer of all messages (Ctrl+L)
function clear_log_buffer() function clear_log_buffer()
log_buffer = {} log_buffers[id] = {}
update() update()
end end
@ -1212,8 +1254,7 @@ function paste(clip)
local after_cur = line:sub(cursor) local after_cur = line:sub(cursor)
line = before_cur .. text .. after_cur line = before_cur .. text .. after_cur
cursor = cursor + text:len() cursor = cursor + text:len()
suggestion_buffer = {} handle_edit()
update()
end end
-- List of input bindings. This is a weird mashup between common GUI text-input -- 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) set_active(true)
end) 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 -- Add a script-message to show the REPL and fill it with the provided text
mp.register_script_message('type', function(text, cursor_pos) mp.register_script_message('type', function(text, cursor_pos)
show_and_type(text, cursor_pos) show_and_type(text, cursor_pos)
end) 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 -- Redraw the REPL when the OSD size changes. This is needed because the
-- PlayRes of the OSD will need to be adjusted. -- PlayRes of the OSD will need to be adjusted.
mp.observe_property('osd-width', 'native', update) mp.observe_property('osd-width', 'native', update)

66
player/lua/input.lua Normal file
View File

@ -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

View File

@ -1,5 +1,6 @@
lua_files = ['defaults.lua', 'assdraw.lua', 'options.lua', 'osc.lua', 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 foreach file: lua_files
lua_file = custom_target(file, lua_file = custom_target(file,
input: join_paths(source_root, 'player', 'lua', file), input: join_paths(source_root, 'player', 'lua', file),