From bfa355859d4a007648b5b62971cd07bdfa044bbd Mon Sep 17 00:00:00 2001 From: Pavel Date: Tue, 26 May 2026 22:31:45 +0200 Subject: [PATCH] select.lua: add keep-open argument to select bindings Adds an optional keep-open argument to all select.lua script bindings that prevents the menu from closing after a selection. Menus that display selection state (tracks, audio device) or a current position indicator (playlist, chapter, edition, subtitle line) are wrapped in a recursive function so the list is rebuilt after each selection and the indicators remain accurate. default_item is passed to preserve the cursor position on the last selected item. The history confirmation dialog reopens the history menu when No is selected, and closes when Yes is selected as there is nothing to show anymore. --- DOCS/interface-changes/select-keep-open.txt | 1 + DOCS/man/select.rst | 7 + player/lua/select.lua | 580 +++++++++++--------- 3 files changed, 337 insertions(+), 251 deletions(-) create mode 100644 DOCS/interface-changes/select-keep-open.txt diff --git a/DOCS/interface-changes/select-keep-open.txt b/DOCS/interface-changes/select-keep-open.txt new file mode 100644 index 0000000000000..f7dc490d32dbe --- /dev/null +++ b/DOCS/interface-changes/select-keep-open.txt @@ -0,0 +1 @@ +add `keep-open` argument support to select.lua script bindings diff --git a/DOCS/man/select.rst b/DOCS/man/select.rst index a4e624b71edf0..425e512ae3829 100644 --- a/DOCS/man/select.rst +++ b/DOCS/man/select.rst @@ -68,10 +68,17 @@ By default select.lua's script bindings are bound to key sequences starting with ``g`` listed in `Keyboard Control`_. The names of the script bindings listed below can be used to bind them to different keys. +All script bindings accept an optional ``keep-open`` argument that +prevents the menu from closing after a selection. + .. admonition:: Example to rebind playlist selection in input.conf Ctrl+p script-binding select/select-playlist +.. admonition:: Example to keep the track menu open in input.conf + + g-t script-binding select/select-track keep-open + Available script bindings are: ``select-playlist`` diff --git a/player/lua/select.lua b/player/lua/select.lua index 090eab875acc2..bdb061c522f56 100644 --- a/player/lua/select.lua +++ b/player/lua/select.lua @@ -72,33 +72,41 @@ local function format_playlist_entry(entry, show) return item end -mp.add_key_binding(nil, "select-playlist", function () - local playlist = {} - local default_item - local show = mp.get_property_native("osd-playlist-entry") - - for i, entry in ipairs(mp.get_property_native("playlist")) do - playlist[i] = format_playlist_entry(entry, show) +mp.add_key_binding(nil, "select-playlist", function (t) + if t.event == "up" or t.event == "repeat" then return end + local keep_open = t.arg == "keep-open" + local function open(default_item) + local playlist = {} + local show = mp.get_property_native("osd-playlist-entry") + + for i, entry in ipairs(mp.get_property_native("playlist")) do + playlist[i] = format_playlist_entry(entry, show) + + if not default_item and entry.playing then + default_item = i + end + end - if entry.playing then - default_item = i + if #playlist == 0 then + show_warning("The playlist is empty.") + return end - end - if #playlist == 0 then - show_warning("The playlist is empty.") - return + input.select({ + prompt = "Select a playlist entry:", + items = playlist, + default_item = default_item, + keep_open = keep_open, + submit = function (index) + mp.commandv("playlist-play-index", index - 1) + if keep_open then + open(index) + end + 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) - end, - }) -end) + open() +end, { complex = true }) local function format_track_type(track) return (track.image @@ -150,229 +158,284 @@ local function format_track(track) ):sub(1, -2) .. ")" .. format_flags(track) end -mp.add_key_binding(nil, "select-track", function () - local tracks = {} +mp.add_key_binding(nil, "select-track", function (t) + if t.event == "up" or t.event == "repeat" then return end + local keep_open = t.arg == "keep-open" + local function open(default_item) + local tracks = {} - for i, track in ipairs(mp.get_property_native("track-list")) do - tracks[i] = format_track_type(track) .. format_track(track) - end + for i, track in ipairs(mp.get_property_native("track-list")) do + tracks[i] = format_track_type(track) .. format_track(track) + end - if #tracks == 0 then - show_warning("No available tracks.") - return - end + if #tracks == 0 then + show_warning("No available tracks.") + return + 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 - end, - }) -end) + input.select({ + prompt = "Select a track:", + items = tracks, + default_item = default_item, + keep_open = keep_open, + 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 + if keep_open then + open(id) + end + end, + }) + end + open() +end, { complex = true }) -local function select_track(property, type, prompt, warning) - local tracks = {} - local items = {} - local default_item - local track_id = mp.get_property_native(property) +local function select_track(property, type, prompt, warning, keep_open) + local function open(default_item) + local tracks = {} + local items = {} + local track_id = mp.get_property_native(property) - for _, track in ipairs(mp.get_property_native("track-list")) do - if track.type == type then - tracks[#tracks + 1] = track - items[#items + 1] = format_track(track) + for _, track in ipairs(mp.get_property_native("track-list")) do + if track.type == type then + tracks[#tracks + 1] = track + items[#items + 1] = format_track(track) - if track.id == track_id then - default_item = #items + if not default_item and track.id == track_id then + default_item = #items + end end end - end - if #items == 0 then - show_warning(warning) - return - end + if #items == 0 then + show_warning(warning) + return + end - input.select({ - prompt = prompt, - items = items, - default_item = default_item, - submit = function (id) - mp.set_property(property, tracks[id].selected and "no" or tracks[id].id) - end, - }) + input.select({ + prompt = prompt, + items = items, + default_item = default_item, + keep_open = keep_open, + submit = function (id) + mp.set_property(property, tracks[id].selected and "no" or tracks[id].id) + if keep_open then + open(id) + end + end, + }) + end + open() end -mp.add_key_binding(nil, "select-sid", function () - select_track("sid", "sub", "Select a subtitle:", "No available subtitles.") -end) +mp.add_key_binding(nil, "select-sid", function (t) + if t.event == "up" or t.event == "repeat" then return end + select_track("sid", "sub", "Select a subtitle:", "No available subtitles.", + t.arg == "keep-open") +end, { complex = true }) -mp.add_key_binding(nil, "select-secondary-sid", function () +mp.add_key_binding(nil, "select-secondary-sid", function (t) + if t.event == "up" or t.event == "repeat" then return end select_track("secondary-sid", "sub", "Select a secondary subtitle:", - "No available subtitles.") -end) + "No available subtitles.", t.arg == "keep-open") +end, { complex = true }) -mp.add_key_binding(nil, "select-aid", function () +mp.add_key_binding(nil, "select-aid", function (t) + if t.event == "up" or t.event == "repeat" then return end select_track("aid", "audio", "Select an audio track:", - "No available audio tracks.") -end) + "No available audio tracks.", t.arg == "keep-open") +end, { complex = true }) -mp.add_key_binding(nil, "select-vid", function () +mp.add_key_binding(nil, "select-vid", function (t) + if t.event == "up" or t.event == "repeat" then return end select_track("vid", "video", "Select a video track:", - "No available video tracks.") -end) + "No available video tracks.", t.arg == "keep-open") +end, { complex = true }) local function format_time(t, duration) local fmt = math.max(t, duration) >= 60 * 60 and "%H:%M:%S" or "%M:%S" return mp.format_time(t, fmt) end -mp.add_key_binding(nil, "select-chapter", function () - local chapters = {} - local default_item = mp.get_property_native("chapter") +mp.add_key_binding(nil, "select-chapter", function (t) + if t.event == "up" or t.event == "repeat" then return end + local keep_open = t.arg == "keep-open" + local function open(default_item) + local chapters = {} + local current_chapter = mp.get_property_native("chapter") - if default_item == nil then - show_warning("No available chapters.") - return - end + if current_chapter == nil then + show_warning("No available chapters.") + return + end - local duration = mp.get_property_native("duration", math.huge) + local duration = mp.get_property_native("duration", math.huge) - for i, chapter in ipairs(mp.get_property_native("chapter-list")) do - chapters[i] = format_time(chapter.time, duration) .. " " .. chapter.title - end + for i, chapter in ipairs(mp.get_property_native("chapter-list")) do + chapters[i] = format_time(chapter.time, duration) .. " " .. chapter.title + end - input.select({ - prompt = "Select a chapter:", - items = chapters, - default_item = default_item > -1 and default_item + 1, - submit = function (chapter) - mp.set_property("chapter", chapter - 1) - end, - }) -end) + input.select({ + prompt = "Select a chapter:", + items = chapters, + default_item = default_item or (current_chapter > -1 and current_chapter + 1), + keep_open = keep_open, + submit = function (chapter) + mp.set_property("chapter", chapter - 1) + if keep_open then + open(chapter) + end + end, + }) + end + open() +end, { complex = true }) local function format_edition(edition) return edition.title or ("Edition " .. edition.id + 1) end -mp.add_key_binding(nil, "select-edition", function () - local edition_list = mp.get_property_native("edition-list") - - if edition_list == nil or #edition_list < 2 then - show_warning("No available editions.") - return - end - - local editions = {} - local default_item = mp.get_property_native("current-edition") +mp.add_key_binding(nil, "select-edition", function (t) + if t.event == "up" or t.event == "repeat" then return end + local keep_open = t.arg == "keep-open" + local function open(default_item) + local edition_list = mp.get_property_native("edition-list") - for i, edition in ipairs(edition_list) do - editions[i] = format_edition(edition) - end + if edition_list == nil or #edition_list < 2 then + show_warning("No available editions.") + return + end - input.select({ - prompt = "Select an edition:", - items = editions, - default_item = default_item > -1 and default_item + 1, - submit = function (edition) - mp.set_property("edition", edition - 1) - end, - }) -end) + local editions = {} + local current_edition = mp.get_property_native("current-edition") -local function select_subtitle_line(secondary) - local lines = mp.get_property_native(secondary .. "sub-lines") + for i, edition in ipairs(edition_list) do + editions[i] = format_edition(edition) + end - if not lines then - show_warning("Subtitle lines could not be retrieved.") - return + input.select({ + prompt = "Select an edition:", + items = editions, + default_item = default_item or (current_edition > -1 and current_edition + 1), + keep_open = keep_open, + submit = function (edition) + mp.set_property("edition", edition - 1) + if keep_open then + open(edition) + end + end, + }) end + open() +end, { complex = true }) - local items = {} - local times = {} - local default_item - local delay = mp.get_property_native(secondary .. "sub-delay") - local time_pos = mp.get_property_native("time-pos") - delay - local duration = mp.get_property_native("duration", math.huge) +local function select_subtitle_line(secondary, keep_open) + local function open(default_item) + local lines = mp.get_property_native(secondary .. "sub-lines") - for _, line in ipairs(lines) do - if line.start <= time_pos then - default_item = #items + 1 + if not lines then + show_warning("Subtitle lines could not be retrieved.") + return end - for text in line.text:gmatch("[^\n]+") do - items[#items + 1] = format_time(line.start, duration) .. " " .. text - times[#times + 1] = line.start - end + local items = {} + local times = {} + local delay = mp.get_property_native(secondary .. "sub-delay") + local time_pos = mp.get_property_native("time-pos") - delay + local duration = mp.get_property_native("duration", math.huge) - if line.text == "" then - items[#items + 1] = format_time(line.start, duration) - times[#times + 1] = line.start - end - end + for _, line in ipairs(lines) do + if not default_item and line.start <= time_pos then + default_item = #items + 1 + end - input.select({ - prompt = "Select a line to seek to:", - items = items, - default_item = default_item, - submit = function (i) - -- Add an offset to seek to the correct line while paused without a - -- video track. - if mp.get_property_native("current-tracks/video/image") ~= false then - delay = delay + 0.1 + for text in line.text:gmatch("[^\n]+") do + items[#items + 1] = format_time(line.start, duration) .. " " .. text + times[#times + 1] = line.start end - mp.commandv("seek", times[i] + delay, "absolute") - end, - }) + if line.text == "" then + items[#items + 1] = format_time(line.start, duration) + times[#times + 1] = line.start + end + end + + input.select({ + prompt = "Select a line to seek to:", + items = items, + default_item = default_item, + keep_open = keep_open, + submit = function (i) + -- Add an offset to seek to the correct line while paused without a + -- video track. + if mp.get_property_native("current-tracks/video/image") ~= false then + delay = delay + 0.1 + end + + mp.commandv("seek", times[i] + delay, "absolute") + if keep_open then + open(i) + end + end, + }) + end + open() end -mp.add_key_binding(nil, "select-subtitle-line", function () - select_subtitle_line("") -end) -mp.add_key_binding(nil, "select-secondary-subtitle-line", function () - select_subtitle_line("secondary-") -end) +mp.add_key_binding(nil, "select-subtitle-line", function (t) + if t.event == "up" or t.event == "repeat" then return end + select_subtitle_line("", t.arg == "keep-open") +end, { complex = true }) +mp.add_key_binding(nil, "select-secondary-subtitle-line", function (t) + if t.event == "up" or t.event == "repeat" then return end + select_subtitle_line("secondary-", t.arg == "keep-open") +end, { complex = true }) local function format_audio_device(device) return device.name .. " (" .. device.description .. ")" end -mp.add_key_binding(nil, "select-audio-device", function () - local devices = mp.get_property_native("audio-device-list") - local items = {} - -- This is only useful if an --audio-device has been explicitly set, - -- otherwise its value is just auto and there is no current-audio-device - -- property. - local selected_device = mp.get_property("audio-device") - local default_item - - if #devices == 0 then - show_warning("No available audio devices.") - return - end +mp.add_key_binding(nil, "select-audio-device", function (t) + if t.event == "up" or t.event == "repeat" then return end + local keep_open = t.arg == "keep-open" + local function open(default_item) + local devices = mp.get_property_native("audio-device-list") + local items = {} + -- This is only useful if an --audio-device has been explicitly set, + -- otherwise its value is just auto and there is no current-audio-device + -- property. + local selected_device = mp.get_property("audio-device") + + if #devices == 0 then + show_warning("No available audio devices.") + return + end - for i, device in ipairs(devices) do - items[i] = format_audio_device(device) + for i, device in ipairs(devices) do + items[i] = format_audio_device(device) - if device.name == selected_device then - default_item = i + if not default_item and device.name == selected_device then + default_item = i + end end - end - input.select({ - prompt = "Select an audio device:", - items = items, - default_item = default_item, - submit = function (id) - mp.set_property("audio-device", devices[id].name) - end, - }) -end) + input.select({ + prompt = "Select an audio device:", + items = items, + default_item = default_item, + keep_open = keep_open, + submit = function (id) + mp.set_property("audio-device", devices[id].name) + if keep_open then + open(id) + end + end, + }) + end + open() +end, { complex = true }) local function format_history_entry(entry) local status @@ -397,74 +460,84 @@ local function format_history_entry(entry) return item .. filename .. " (" .. directory .. ")" end -mp.add_key_binding(nil, "select-watch-history", function () - local history_file_path = mp.command_native( - {"expand-path", mp.get_property("watch-history-path")}) - local history_file, error_message = io.open(history_file_path) - if not history_file then - show_warning(mp.get_property_native("save-watch-history") - and error_message - or "Enable --save-watch-history to jump to recently played files.") - return - end - - local all_entries = {} - local line_num = 1 - for line in history_file:lines() do - local entry = utils.parse_json(line) - if entry and entry.path then - all_entries[#all_entries + 1] = entry - else - mp.msg.warn(history_file_path .. ": Parse error at line " .. line_num) +mp.add_key_binding(nil, "select-watch-history", function (t) + if t.event == "up" or t.event == "repeat" then return end + local keep_open = t.arg == "keep-open" + local function open_history() + local history_file_path = mp.command_native( + {"expand-path", mp.get_property("watch-history-path")}) + local history_file, error_message = io.open(history_file_path) + if not history_file then + show_warning(mp.get_property_native("save-watch-history") + and error_message + or "Enable --save-watch-history to jump to recently played files.") + return end - line_num = line_num + 1 - end - history_file:close() - local entries = {} - local items = {} - local seen = {} - - for i = #all_entries, 1, -1 do - local entry = all_entries[i] - if not seen[entry.path] or not options.hide_history_duplicates then - seen[entry.path] = true - entries[#entries + 1] = entry - items[#items + 1] = format_history_entry(entry) + local all_entries = {} + local line_num = 1 + for line in history_file:lines() do + local entry = utils.parse_json(line) + if entry and entry.path then + all_entries[#all_entries + 1] = entry + else + mp.msg.warn(history_file_path .. ": Parse error at line " .. line_num) + end + line_num = line_num + 1 + end + history_file:close() + + local entries = {} + local items = {} + local seen = {} + + for i = #all_entries, 1, -1 do + local entry = all_entries[i] + if not seen[entry.path] or not options.hide_history_duplicates then + seen[entry.path] = true + entries[#entries + 1] = entry + items[#items + 1] = format_history_entry(entry) + end end - end - items[#items+1] = "Clear history" + items[#items+1] = "Clear history" - input.select({ - prompt = "Select a file:", - items = items, - submit = function (i) - if entries[i] then - mp.commandv("loadfile", entries[i].path) - return - end + input.select({ + prompt = "Select a file:", + items = items, + keep_open = keep_open, + submit = function (i) + if entries[i] then + mp.commandv("loadfile", entries[i].path) + return + end - input.select({ - prompt = "Are you sure you want to clear the history?", - items = {"No", "Yes"}, - submit = function (j) - if j == 2 then - error_message = select(2, os.remove(history_file_path)) - if error_message then - show_error(error_message) + input.select({ + prompt = "Are you sure you want to clear the history?", + items = {"No", "Yes"}, + submit = function (j) + if j == 2 then + error_message = select(2, os.remove(history_file_path)) + if error_message then + show_error(error_message) + else + mp.osd_message("History cleared.") + end else - mp.osd_message("History cleared.") + open_history() end end - end - }) + }) - end, - }) -end) + end, + }) + end + open_history() +end, { complex = true }) -mp.add_key_binding(nil, "select-watch-later", function () +mp.add_key_binding(nil, "select-watch-later", function (t) + if t.event == "up" or t.event == "repeat" then return end + local keep_open = t.arg == "keep-open" local watch_later_dir = mp.get_property("current-watch-later-dir") if not watch_later_dir then @@ -514,11 +587,12 @@ mp.add_key_binding(nil, "select-watch-later", function () input.select({ prompt = "Select a file:", items = items, + keep_open = keep_open, submit = function (i) mp.commandv("loadfile", files[i][1]) end, }) -end) +end, { complex = true }) local function get_active_bindings() local bindings = {} @@ -540,7 +614,8 @@ local function get_active_bindings() return bindings end -mp.add_key_binding(nil, "select-binding", function () +mp.add_key_binding(nil, "select-binding", function (t) + if t.event == "up" or t.event == "repeat" then return end local items = {} for _, binding in pairs(get_active_bindings()) do if binding.cmd ~= "ignore" then @@ -553,11 +628,12 @@ mp.add_key_binding(nil, "select-binding", function () input.select({ prompt = "Select a binding:", items = items, + keep_open = t.arg == "keep-open", submit = function (i) mp.command(items[i]:gsub("^.- ", "")) end, }) -end) +end, { complex = true }) local properties = {} @@ -573,7 +649,8 @@ local function add_property(property, value) end end -mp.add_key_binding(nil, "show-properties", function () +mp.add_key_binding(nil, "show-properties", function (t) + if t.event == "up" or t.event == "repeat" then return end properties = {} -- Don't log errors for renamed and removed properties. @@ -597,6 +674,7 @@ mp.add_key_binding(nil, "show-properties", function () input.select({ prompt = "Inspect a property:", items = properties, + keep_open = t.arg == "keep-open", submit = function (i) if mp.get_property_native("vo-configured") then mp.commandv("expand-properties", "show-text", @@ -608,7 +686,7 @@ mp.add_key_binding(nil, "show-properties", function () end end, }) -end) +end, { complex = true }) local function system_open(path) local args