summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorGuido Cella <guido@guidocella.xyz>2024-01-02 18:58:32 +0100
committerDudemanguy <random342@airmail.cc>2024-01-13 02:53:08 +0000
commit871f7a152a3f0b3561a627d21f7417d10ac9c25c (patch)
treec793a9c700b2e52b2a896234b679af5ec9c3f006
parent2dd3951a9c05e7a4fc0a614f13de1bfe0e7f132f (diff)
downloadmpv-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.rst1
-rw-r--r--DOCS/man/javascript.rst23
-rw-r--r--DOCS/man/lua.rst82
-rw-r--r--player/javascript/defaults.js47
-rw-r--r--player/lua.c3
-rw-r--r--player/lua/console.lua195
-rw-r--r--player/lua/input.lua66
-rw-r--r--player/lua/meson.build3
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),