From 5e6de1aeae862d1a742fa1473e63f2a59d24c3f7 Mon Sep 17 00:00:00 2001 From: Guido Cella Date: Thu, 9 May 2024 23:28:55 +0200 Subject: [PATCH] select.lua: add this script This adds script messages to select playlist entries, tracks, chapters, subtitle lines, bindings and properties using the newly introduced mp.input.select(). This fully closes #13964. --- DOCS/man/console.rst | 11 ++ DOCS/man/mpv.rst | 28 ++++ DOCS/man/options.rst | 4 + etc/input.conf | 8 ++ options/options.c | 2 + options/options.h | 1 + player/core.h | 2 +- player/lua.c | 3 + player/lua/console.lua | 15 ++- player/lua/meson.build | 2 +- player/lua/select.lua | 294 +++++++++++++++++++++++++++++++++++++++++ player/scripting.c | 1 + 12 files changed, 364 insertions(+), 7 deletions(-) create mode 100644 player/lua/select.lua diff --git a/DOCS/man/console.rst b/DOCS/man/console.rst index 69cc103c15512..981a0f0372617 100644 --- a/DOCS/man/console.rst +++ b/DOCS/man/console.rst @@ -175,3 +175,14 @@ Configurable Options The ratio of font height to font width. Adjusts table width of completion suggestions. Values in the range 1.8..2.5 make sense for common monospace fonts. + +``select_font_size`` + Default: 26 + + The font size when using the console through ``mp.input.select()``, which by + default is used by keybindings starting with ``g``. + +``select_border_size`` + Default: 3 + + The border size when using the console through ``mp.input.select()``. diff --git a/DOCS/man/mpv.rst b/DOCS/man/mpv.rst index 579f1b313f69b..3cb39289955a5 100644 --- a/DOCS/man/mpv.rst +++ b/DOCS/man/mpv.rst @@ -270,6 +270,34 @@ Alt+2 (and Command+2 on macOS) Command + f (macOS only) Toggle fullscreen (see also ``--fs``). +(The following keybindings open a selector in the console that lets you choose +from a list of items by typing part of the desired item and/or by navigating +them with keybindings: ``Down`` and ``Ctrl+n`` go down, ``Up`` and ``Ctrl+p`` go +up, ``Page down`` and ``Ctrl+f`` scroll down one page, and ``Page up`` and +``Ctrl+b`` scroll up one page.) + +g-p + Select a playlist entry. + +g-t + Select a track. + +g-j + Select a secondary subtitle. + +g-c + Select a chapter. + +g-s + Select a subtitle line to seek to. This requires ``ffmpeg`` in PATH, or in + the same folder as mpv on Windows. + +g-b + Select a defined input binding. + +g-r + Show the values of all properties. + (The following keys are valid if you have a keyboard with multimedia keys.) PAUSE diff --git a/DOCS/man/options.rst b/DOCS/man/options.rst index e40e7d2c6d117..1dd628d36aa75 100644 --- a/DOCS/man/options.rst +++ b/DOCS/man/options.rst @@ -1023,6 +1023,10 @@ Program Behavior `Conditional auto profiles`_ for details. ``auto`` will load the script, but immediately unload it if there are no conditional profiles. +``--load-select=`` + Enable the builtin script that lets you select from lists of items (default: + yes). By default, its keybindings start with the ``g`` key. + ``--player-operation-mode=`` For enabling "pseudo GUI mode", which means that the defaults for some options are changed. This option should not normally be used directly, but diff --git a/etc/input.conf b/etc/input.conf index 80f1ceb2f3e01..0f466802f6644 100644 --- a/etc/input.conf +++ b/etc/input.conf @@ -168,6 +168,14 @@ #ctrl+h cycle-values hwdec "auto-safe" "no" # toggle hardware decoding #F8 show-text ${playlist} # show the playlist #F9 show-text ${track-list} # show the list of video, audio and sub tracks +#g ignore +#g-p script-binding select/select-playlist +#g-t script-binding select/select-track +#g-j script-binding select/select-secondary-sub +#g-c script-binding select/select-chapter +#g-s script-binding select/sub-seek +#g-b script-binding select/select-binding +#g-r script-binding select/show-properties # # Legacy bindings (may or may not be removed in the future) diff --git a/options/options.c b/options/options.c index 4415ed8145bfa..039f8fc481408 100644 --- a/options/options.c +++ b/options/options.c @@ -547,6 +547,7 @@ static const m_option_t mp_opts[] = { {"load-auto-profiles", OPT_CHOICE(lua_load_auto_profiles, {"no", 0}, {"yes", 1}, {"auto", -1}), .flags = UPDATE_BUILTIN_SCRIPTS}, + {"load-select", OPT_BOOL(lua_load_select), .flags = UPDATE_BUILTIN_SCRIPTS}, #endif // ------------------------- stream options -------------------- @@ -969,6 +970,7 @@ static const struct MPOpts mp_default_opts = { .lua_load_stats = true, .lua_load_console = true, .lua_load_auto_profiles = -1, + .lua_load_select = true, #endif .auto_load_scripts = true, .loop_times = 1, diff --git a/options/options.h b/options/options.h index 6733fa3161bc4..49aacbd21ac61 100644 --- a/options/options.h +++ b/options/options.h @@ -175,6 +175,7 @@ typedef struct MPOpts { bool lua_load_stats; bool lua_load_console; int lua_load_auto_profiles; + bool lua_load_select; bool auto_load_scripts; diff --git a/player/core.h b/player/core.h index c44868cecd58f..a3a212ed9145f 100644 --- a/player/core.h +++ b/player/core.h @@ -436,7 +436,7 @@ typedef struct MPContext { struct mp_ipc_ctx *ipc_ctx; - int64_t builtin_script_ids[5]; + int64_t builtin_script_ids[6]; mp_mutex abort_lock; diff --git a/player/lua.c b/player/lua.c index d51ea5c8a716d..1be3428b99cae 100644 --- a/player/lua.c +++ b/player/lua.c @@ -81,6 +81,9 @@ static const char * const builtin_lua_scripts[][2] = { }, {"@auto_profiles.lua", # include "player/lua/auto_profiles.lua.inc" + }, + {"@select.lua", +# include "player/lua/select.lua.inc" }, {0} }; diff --git a/player/lua/console.lua b/player/lua/console.lua index 1757c62f5a057..dbbda4e1bfee0 100644 --- a/player/lua/console.lua +++ b/player/lua/console.lua @@ -34,6 +34,8 @@ local opts = { -- Adjusts table width of completion suggestions. -- Values in the range 1.8..2.5 make sense for common monospace fonts. font_hw_ratio = 'auto', + select_font_size = 26, + select_border_size = 3, } function detect_platform() @@ -252,7 +254,7 @@ local function calculate_max_log_lines() / mp.get_property_native('display-hidpi-scale', 1) / opts.scale * (1 - global_margins.t - global_margins.b) - / opts.font_size + / (opts[selectable_items and 'select_font_size' or 'font_size']) - 1.5) end @@ -465,6 +467,9 @@ function update() return end + local font_size = opts[selectable_items and 'select_font_size' or 'font_size'] + local border_size = opts[selectable_items and 'select_border_size' or 'border_size'] + local coordinate_top = math.floor(global_margins.t * screeny + 0.5) local clipping_coordinates = '0,' .. coordinate_top .. ',' .. screenx .. ',' .. screeny @@ -473,15 +478,15 @@ function update() local style = '{\\r' .. '\\1a&H00&\\3a&H00&\\1c&Heeeeee&\\3c&H111111&' .. (has_shadow and '\\4a&H99&\\4c&H000000&' or '') .. - '\\fn' .. opts.font .. '\\fs' .. opts.font_size .. - '\\bord' .. opts.border_size .. '\\xshad0\\yshad1\\fsp0\\q1' .. + '\\fn' .. opts.font .. '\\fs' .. font_size .. + '\\bord' .. border_size .. '\\xshad0\\yshad1\\fsp0\\q1' .. '\\clip(' .. clipping_coordinates .. ')}' -- Create the cursor glyph as an ASS drawing. ASS will draw the cursor -- inline with the surrounding text, but it sets the advance to the width -- of the drawing. So the cursor doesn't affect layout too much, make it as -- thin as possible and make it appear to be 1px wide by giving it 0.5px -- horizontal borders. - local cheight = opts.font_size * 8 + local cheight = font_size * 8 local cglyph = '{\\rDefault' .. (mp.get_property_native('focused') == false and '\\alpha&HFF&' or '\\1a&H44&\\3a&H44&\\4a&H99&') .. @@ -497,7 +502,7 @@ function update() local lines_max = calculate_max_log_lines() -- Estimate how many characters fit in one line - local width_max = math.ceil(screenx / opts.font_size * get_font_hw_ratio()) + local width_max = math.ceil(screenx / font_size * get_font_hw_ratio()) local suggestions, rows = format_table(suggestion_buffer, width_max, lines_max) local suggestion_ass = style .. styles.suggestion .. suggestions diff --git a/player/lua/meson.build b/player/lua/meson.build index 7df62d219b6c1..0812817cb66b0 100644 --- a/player/lua/meson.build +++ b/player/lua/meson.build @@ -1,6 +1,6 @@ lua_files = ['defaults.lua', 'assdraw.lua', 'options.lua', 'osc.lua', 'ytdl_hook.lua', 'stats.lua', 'console.lua', 'auto_profiles.lua', - 'input.lua', 'fzy.lua'] + 'input.lua', 'fzy.lua', 'select.lua'] foreach file: lua_files lua_file = custom_target(file, input: join_paths(source_root, 'player', 'lua', file), diff --git a/player/lua/select.lua b/player/lua/select.lua new file mode 100644 index 0000000000000..ff696a41afa31 --- /dev/null +++ b/player/lua/select.lua @@ -0,0 +1,294 @@ +--[[ +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 input = require "mp.input" + +mp.add_forced_key_binding(nil, "select-playlist", function () + local playlist = {} + local default_item = 1 + + for i, entry in ipairs(mp.get_property_native("playlist")) do + playlist[i] = select(2, require "mp.utils".split_path(entry.filename)) + + if entry.playing then + default_item = i + end + end + + input.select({ + prompt = "Select a playlist entry:", + items = playlist, + default_item = default_item, + submit = function (index) + mp.commandv("playlist-play-index", index - 1) + input.terminate() + end, + }) +end) + +mp.add_forced_key_binding(nil, "select-track", function () + local tracks = {} + + for i, track in ipairs(mp.get_property_native("track-list")) do + tracks[i] = track.type:sub(1, 1):upper() .. track.type:sub(2) .. ": " .. + (track.selected and "●" or "○") .. + (track.title and " " .. track.title or "") .. + " (" .. ( + (track.lang and track.lang .. " " or "") .. + (track.codec and track.codec .. " " or "") .. + (track["demux-w"] and track["demux-w"] .. "x" .. track["demux-h"] .. " " or "") .. + (track["demux-fps"] and not track.image and string.format("%.3f", track["demux-fps"]) .. "FPS " or "") .. + (track["demux-channel-count"] and track["demux-channel-count"] .. "ch " or "") .. + (track["demux-samplerate"] and track["demux-samplerate"] / 1000 .. "kHz " or "") .. + (track.external and "external " or "") + ):sub(1, -2) .. ")" + end + + input.select({ + prompt = "Select a track:", + items = tracks, + submit = function (id) + local track = mp.get_property_native("track-list/" .. id - 1) + if track then + mp.set_property(track.type, track.selected and "no" or track.id) + end + input.terminate() + end, + }) +end) + +local function show_error(message) + mp.msg.error(message) + if mp.get_property_native("vo-configured") then + mp.osd_message(message) + end +end + +mp.add_forced_key_binding(nil, "select-secondary-sub", function () + local subs = {} + local sub_indexes = {} + local default_item = 1 + local secondary_sid = mp.get_property_native("secondary-sid") + + for i, track in ipairs(mp.get_property_native("track-list")) do + if track.type == "sub" then + subs[#subs + 1] = (track.selected and "●" or "○") .. + (track.title and " " .. track.title or "") .. + " (" .. ( + (track.lang and track.lang .. " " or "") .. + (track.codec and track.codec .. " " or "") .. + (track.external and "external " or "") + ):sub(1, -2) .. ")" + + sub_indexes[#sub_indexes + 1] = i - 1 + + if track.id == secondary_sid then + default_item = #subs + end + end + end + + if #subs == 0 then + show_error("No subtitle is loaded.") + return + end + + input.select({ + prompt = "Select a secondary subtitle:", + items = subs, + default_item = default_item, + submit = function (id) + local sub = mp.get_property_native("track-list/" .. sub_indexes[id]) + if sub then + mp.set_property("secondary-sid", sub.selected and "no" or sub.id) + end + input.terminate() + end, + }) +end) + +local function format_time(t) + local h = math.floor(t / (60 * 60)) + t = t - (h * 60 * 60) + local m = math.floor(t / 60) + local s = t - (m * 60) + + return string.format("%.2d:%.2d:%.2d", h, m, s) +end + +mp.add_forced_key_binding(nil, "select-chapter", function () + local chapters = {} + local default_item = mp.get_property_native("chapter") + + if default_item == nil then + show_error("No chapters are present.") + return + end + + for i, chapter in ipairs(mp.get_property_native("chapter-list")) do + chapters[i] = format_time(chapter.time) .. " " .. chapter.title + end + + input.select({ + prompt = "Select a chapter:", + items = chapters, + default_item = default_item + 1, + submit = function (chapter) + mp.set_property("chapter", chapter - 1) + input.terminate() + end, + }) +end) + +mp.add_forced_key_binding(nil, "sub-seek", function () + local sub = mp.get_property_native("current-tracks/sub") + + if sub == nil then + show_error("No subtitle is loaded.") + return + end + + local r = mp.command_native({ + name = "subprocess", + capture_stdout = true, + args = sub.external + and {"ffmpeg", "-loglevel", "quiet", "-i", sub["external-filename"], "-f", "lrc", "-map_metadata", "-1", "-fflags", "+bitexact", "-"} + or {"ffmpeg", "-loglevel", "quiet", "-i", mp.get_property("path"), "-map", "s:" .. sub["id"] - 1, "-f", "lrc", "-map_metadata", "-1", "-fflags", "+bitexact", "-"} + }) + + if r.status < 0 then + show_error("subprocess error: " .. r.error_string) + return + end + + if r.status > 0 then + show_error("ffmpeg failed with code " .. r.status) + return + end + + local sub_lines = {} + local default_item = 1 + + local sub_start = mp.get_property_native("sub-start", 0) + local m = math.floor(sub_start / 60) + local s = sub_start - m * 60 + sub_start = string.format("%.2d:%05.2f", m, s) + + -- Strip HTML and ASS tags. + for line in r.stdout:gsub("<.->", ""):gsub("{\\.-}", ""):gmatch("[^\n]+") do + sub_lines[#sub_lines + 1] = line + + if line:find("^%[" .. sub_start) then + default_item = #sub_lines + end + end + + input.select({ + prompt = "Select a line to seek to:", + items = sub_lines, + default_item = default_item, + submit = function (index) + mp.commandv("seek", sub_lines[index]:match("[%d:%.]+"), "absolute") + input.terminate() + end, + }) +end) + +local function truncate(str) + return #str < 100 and str or str:sub(1, 99) .. "…" +end + +mp.add_forced_key_binding(nil, "select-binding", function () + local binding_map = {} + + for _, binding in pairs(mp.get_property_native("input-bindings")) do + if binding.priority >= 0 and ( + binding_map[binding.key] == nil or + (binding_map[binding.key].is_weak and not binding.is_weak) or + (binding.is_weak == binding_map[binding.key].is_weak and + binding.priority > binding_map[binding.key].priority) + ) then + binding_map[binding.key] = binding + end + end + + local bindings = {} + for _, binding in pairs(binding_map) do + if binding.cmd ~= "ignore" then + bindings[#bindings + 1] = binding + end + end + + table.sort(bindings, function (a, b) + return a.key < b.key + end) + + local items = {} + for i, binding in ipairs(bindings) do + items[i] = truncate(binding.key .. " " .. binding.cmd) + end + + input.select({ + prompt = "Select a binding:", + items = items, + submit = function (i) + mp.command(bindings[i].cmd) + input.terminate() + end, + }) +end) + +mp.add_forced_key_binding(nil, "show-properties", function () + local properties = {} + + -- Don't log errors for renamed and removed properties. + local msg_level_backup = mp.get_property("msg-level") + mp.set_property("msg-level", msg_level_backup == "" and "cplayer=no" + or msg_level_backup .. ",cplayer=no") + + for _, property in pairs(mp.get_property_native("property-list")) do + if property ~= "current-tracks" then + properties[#properties + 1] = truncate(property .. ": " .. + mp.get_property(property, "")) + end + end + + for _, sub_property in pairs({"video", "audio", "sub", "sub2"}) do + properties[#properties + 1] = + truncate("current-tracks/" .. sub_property .. ": " .. + mp.get_property("current-tracks/" .. sub_property, "")) + end + + mp.set_property("msg-level", msg_level_backup) + + table.sort(properties, function (a, b) + return a < b + end) + + input.select({ + prompt = "Inspect a property:", + items = properties, + submit = function (i) + mp.command("show-text ${osd-ass-cc/0}{\\an9}" .. + (#properties[i] > 100 and "{\\fs9}" or "") .. + "${" .. properties[i]:gsub(":.*", "") .. "} 999999") + end, + closed = function () + mp.osd_message("") + end + }) +end) diff --git a/player/scripting.c b/player/scripting.c index 5c99f4c70e9d3..2dec9d84468b9 100644 --- a/player/scripting.c +++ b/player/scripting.c @@ -262,6 +262,7 @@ void mp_load_builtin_scripts(struct MPContext *mpctx) load_builtin_script(mpctx, 3, mpctx->opts->lua_load_console, "@console.lua"); load_builtin_script(mpctx, 4, mpctx->opts->lua_load_auto_profiles, "@auto_profiles.lua"); + load_builtin_script(mpctx, 5, mpctx->opts->lua_load_select, "@select.lua"); } bool mp_load_scripts(struct MPContext *mpctx)