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.
This commit is contained in:
Pavel authored and Kacper Michajłow committed 2026-06-17 00:30:39 +02:00
1 parent f88bcd25c7
commit 53796ca2ea
3 files changed
+346 -260

No files matched your search

@@ -0,0 +1 @@
add `keep-open` argument support to select.lua script bindings
+7
View File
@@ -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 ``g`` listed in `Keyboard Control`_. The names of the script bindings listed
below can be used to bind them to different keys. 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 .. admonition:: Example to rebind playlist selection in input.conf
Ctrl+p script-binding select/select-playlist 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: Available script bindings are:
``select-playlist`` ``select-playlist``
+338 -260
View File
@@ -72,33 +72,41 @@ local function format_playlist_entry(entry, show)
return item return item
end end
mp.add_key_binding(nil, "select-playlist", function () mp.add_key_binding(nil, "select-playlist", function (t)
local playlist = {} if t.event == "up" or t.event == "repeat" then return end
local default_item local keep_open = t.arg == "keep-open"
local show = mp.get_property_native("osd-playlist-entry") 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 for i, entry in ipairs(mp.get_property_native("playlist")) do
playlist[i] = format_playlist_entry(entry, show) playlist[i] = format_playlist_entry(entry, show)
if entry.playing then if not default_item and entry.playing then
default_item = i default_item = i
end
end end
end
if #playlist == 0 then if #playlist == 0 then
show_warning("The playlist is empty.") show_warning("The playlist is empty.")
return return
end end
input.select({ input.select({
prompt = "Select a playlist entry:", prompt = "Select a playlist entry:",
items = playlist, items = playlist,
default_item = default_item, default_item = default_item,
submit = function (index) keep_open = keep_open,
mp.commandv("playlist-play-index", index - 1) submit = function (index)
end, mp.commandv("playlist-play-index", index - 1)
}) if keep_open then
end) open(index)
end
end,
})
end
open()
end, { complex = true })
local function format_track_type(track) local function format_track_type(track)
return (track.image return (track.image
@@ -150,229 +158,284 @@ local function format_track(track)
):sub(1, -2) .. ")" .. format_flags(track) ):sub(1, -2) .. ")" .. format_flags(track)
end end
mp.add_key_binding(nil, "select-track", function () mp.add_key_binding(nil, "select-track", function (t)
local tracks = {} 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 for i, track in ipairs(mp.get_property_native("track-list")) do
tracks[i] = format_track_type(track) .. format_track(track) tracks[i] = format_track_type(track) .. format_track(track)
end
if #tracks == 0 then
show_warning("No available tracks.")
return
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 end
open()
end, { complex = true })
if #tracks == 0 then local function select_track(property, type, prompt, warning, keep_open)
show_warning("No available tracks.") local function open(default_item)
return local tracks = {}
end local items = {}
local track_id = mp.get_property_native(property)
input.select({ for _, track in ipairs(mp.get_property_native("track-list")) do
prompt = "Select a track:", if track.type == type then
items = tracks, tracks[#tracks + 1] = track
submit = function (id) items[#items + 1] = format_track(track)
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)
local function select_track(property, type, prompt, warning) if not default_item and track.id == track_id then
local tracks = {} default_item = #items
local items = {} end
local default_item
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)
if track.id == track_id then
default_item = #items
end end
end end
end
if #items == 0 then if #items == 0 then
show_warning(warning) show_warning(warning)
return return
end end
input.select({ input.select({
prompt = prompt, prompt = prompt,
items = items, items = items,
default_item = default_item, default_item = default_item,
submit = function (id) keep_open = keep_open,
mp.set_property(property, tracks[id].selected and "no" or tracks[id].id) submit = function (id)
end, mp.set_property(property, tracks[id].selected and "no" or tracks[id].id)
}) if keep_open then
open(id)
end
end,
})
end
open()
end end
mp.add_key_binding(nil, "select-sid", function () mp.add_key_binding(nil, "select-sid", function (t)
select_track("sid", "sub", "Select a subtitle:", "No available subtitles.") if t.event == "up" or t.event == "repeat" then return end
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:", select_track("secondary-sid", "sub", "Select a secondary subtitle:",
"No available subtitles.") "No available subtitles.", t.arg == "keep-open")
end) 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:", select_track("aid", "audio", "Select an audio track:",
"No available audio tracks.") "No available audio tracks.", t.arg == "keep-open")
end) 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:", select_track("vid", "video", "Select a video track:",
"No available video tracks.") "No available video tracks.", t.arg == "keep-open")
end) end, { complex = true })
local function format_time(t, duration) local function format_time(t, duration)
local fmt = math.max(t, duration) >= 60 * 60 and "%H:%M:%S" or "%M:%S" local fmt = math.max(t, duration) >= 60 * 60 and "%H:%M:%S" or "%M:%S"
return mp.format_time(t, fmt) return mp.format_time(t, fmt)
end end
mp.add_key_binding(nil, "select-chapter", function () mp.add_key_binding(nil, "select-chapter", function (t)
local chapters = {} if t.event == "up" or t.event == "repeat" then return end
local default_item = mp.get_property_native("chapter") 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 if current_chapter == nil then
show_warning("No available chapters.") show_warning("No available chapters.")
return return
end
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
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 end
open()
local duration = mp.get_property_native("duration", math.huge) end, { complex = true })
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)
local function format_edition(edition) local function format_edition(edition)
return edition.title or ("Edition " .. edition.id + 1) return edition.title or ("Edition " .. edition.id + 1)
end end
mp.add_key_binding(nil, "select-edition", function () mp.add_key_binding(nil, "select-edition", function (t)
local edition_list = mp.get_property_native("edition-list") 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")
if edition_list == nil or #edition_list < 2 then if edition_list == nil or #edition_list < 2 then
show_warning("No available editions.") show_warning("No available editions.")
return return
end
local editions = {}
local default_item = mp.get_property_native("current-edition")
for i, edition in ipairs(edition_list) do
editions[i] = format_edition(edition)
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 function select_subtitle_line(secondary)
local lines = mp.get_property_native(secondary .. "sub-lines")
if not lines then
show_warning("Subtitle lines could not be retrieved.")
return
end
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)
for _, line in ipairs(lines) do
if line.start <= time_pos then
default_item = #items + 1
end end
for text in line.text:gmatch("[^\n]+") do local editions = {}
items[#items + 1] = format_time(line.start, duration) .. " " .. text local current_edition = mp.get_property_native("current-edition")
times[#times + 1] = line.start
for i, edition in ipairs(edition_list) do
editions[i] = format_edition(edition)
end end
if line.text == "" then input.select({
items[#items + 1] = format_time(line.start, duration) prompt = "Select an edition:",
times[#times + 1] = line.start items = editions,
end 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 end
open()
end, { complex = true })
input.select({ local function select_subtitle_line(secondary, keep_open)
prompt = "Select a line to seek to:", local function open(default_item)
items = items, local lines = mp.get_property_native(secondary .. "sub-lines")
default_item = default_item,
submit = function (i) if not lines then
-- Add an offset to seek to the correct line while paused without a show_warning("Subtitle lines could not be retrieved.")
-- video track. return
if mp.get_property_native("current-tracks/video/image") ~= false then end
delay = delay + 0.1
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)
for _, line in ipairs(lines) do
if not default_item and line.start <= time_pos then
default_item = #items + 1
end end
mp.commandv("seek", times[i] + delay, "absolute") for text in line.text:gmatch("[^\n]+") do
end, items[#items + 1] = format_time(line.start, duration) .. " " .. text
}) times[#times + 1] = line.start
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 end
mp.add_key_binding(nil, "select-subtitle-line", function () mp.add_key_binding(nil, "select-subtitle-line", function (t)
select_subtitle_line("") if t.event == "up" or t.event == "repeat" then return end
end) select_subtitle_line("", t.arg == "keep-open")
mp.add_key_binding(nil, "select-secondary-subtitle-line", function () end, { complex = true })
select_subtitle_line("secondary-") mp.add_key_binding(nil, "select-secondary-subtitle-line", function (t)
end) 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) local function format_audio_device(device)
return device.name .. " (" .. device.description .. ")" return device.name .. " (" .. device.description .. ")"
end end
mp.add_key_binding(nil, "select-audio-device", function () mp.add_key_binding(nil, "select-audio-device", function (t)
local devices = mp.get_property_native("audio-device-list") if t.event == "up" or t.event == "repeat" then return end
local items = {} local keep_open = t.arg == "keep-open"
-- This is only useful if an --audio-device has been explicitly set, local function open(default_item)
-- otherwise its value is just auto and there is no current-audio-device local devices = mp.get_property_native("audio-device-list")
-- property. local items = {}
local selected_device = mp.get_property("audio-device") -- This is only useful if an --audio-device has been explicitly set,
local default_item -- 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 if #devices == 0 then
show_warning("No available audio devices.") show_warning("No available audio devices.")
return return
end
for i, device in ipairs(devices) do
items[i] = format_audio_device(device)
if device.name == selected_device then
default_item = i
end end
end
input.select({ for i, device in ipairs(devices) do
prompt = "Select an audio device:", items[i] = format_audio_device(device)
items = items,
default_item = default_item, if not default_item and device.name == selected_device then
submit = function (id) default_item = i
mp.set_property("audio-device", devices[id].name) end
end, 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 function format_history_entry(entry)
local status local status
@@ -397,74 +460,84 @@ local function format_history_entry(entry)
return item .. filename .. " (" .. directory .. ")" return item .. filename .. " (" .. directory .. ")"
end end
mp.add_key_binding(nil, "select-watch-history", function () mp.add_key_binding(nil, "select-watch-history", function (t)
local history_file_path = mp.command_native( if t.event == "up" or t.event == "repeat" then return end
{"expand-path", mp.get_property("watch-history-path")}) local keep_open = t.arg == "keep-open"
local history_file, error_message = io.open(history_file_path) local function open_history()
if not history_file then local history_file_path = mp.command_native(
show_warning(mp.get_property_native("save-watch-history") {"expand-path", mp.get_property("watch-history-path")})
and error_message local history_file, error_message = io.open(history_file_path)
or "Enable --save-watch-history to jump to recently played files.") if not history_file then
return show_warning(mp.get_property_native("save-watch-history")
end and error_message
or "Enable --save-watch-history to jump to recently played files.")
local all_entries = {} return
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 end
line_num = line_num + 1
end
history_file:close()
local entries = {} local all_entries = {}
local items = {} local line_num = 1
local seen = {} for line in history_file:lines() do
local entry = utils.parse_json(line)
for i = #all_entries, 1, -1 do if entry and entry.path then
local entry = all_entries[i] all_entries[#all_entries + 1] = entry
if not seen[entry.path] or not options.hide_history_duplicates then else
seen[entry.path] = true mp.msg.warn(history_file_path .. ": Parse error at line " .. line_num)
entries[#entries + 1] = entry
items[#items + 1] = format_history_entry(entry)
end
end
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 end
line_num = line_num + 1
end
history_file:close()
input.select({ local entries = {}
prompt = "Are you sure you want to clear the history?", local items = {}
items = {"No", "Yes"}, local seen = {}
submit = function (j)
if j == 2 then for i = #all_entries, 1, -1 do
error_message = select(2, os.remove(history_file_path)) local entry = all_entries[i]
if error_message then if not seen[entry.path] or not options.hide_history_duplicates then
show_error(error_message) seen[entry.path] = true
entries[#entries + 1] = entry
items[#items + 1] = format_history_entry(entry)
end
end
items[#items+1] = "Clear history"
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)
else
mp.osd_message("History cleared.")
end
else else
mp.osd_message("History cleared.") open_history()
end end
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") local watch_later_dir = mp.get_property("current-watch-later-dir")
if not watch_later_dir then if not watch_later_dir then
@@ -514,11 +587,12 @@ mp.add_key_binding(nil, "select-watch-later", function ()
input.select({ input.select({
prompt = "Select a file:", prompt = "Select a file:",
items = items, items = items,
keep_open = keep_open,
submit = function (i) submit = function (i)
mp.commandv("loadfile", files[i][1]) mp.commandv("loadfile", files[i][1])
end, end,
}) })
end) end, { complex = true })
local function get_active_bindings() local function get_active_bindings()
local bindings = {} local bindings = {}
@@ -540,7 +614,8 @@ local function get_active_bindings()
return bindings return bindings
end 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 = {} local items = {}
for _, binding in pairs(get_active_bindings()) do for _, binding in pairs(get_active_bindings()) do
if binding.cmd ~= "ignore" then if binding.cmd ~= "ignore" then
@@ -553,11 +628,12 @@ mp.add_key_binding(nil, "select-binding", function ()
input.select({ input.select({
prompt = "Select a binding:", prompt = "Select a binding:",
items = items, items = items,
keep_open = t.arg == "keep-open",
submit = function (i) submit = function (i)
mp.command(items[i]:gsub("^.- ", "")) mp.command(items[i]:gsub("^.- ", ""))
end, end,
}) })
end) end, { complex = true })
local properties = {} local properties = {}
@@ -573,7 +649,8 @@ local function add_property(property, value)
end end
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 = {} properties = {}
-- Don't log errors for renamed and removed properties. -- Don't log errors for renamed and removed properties.
@@ -597,6 +674,7 @@ mp.add_key_binding(nil, "show-properties", function ()
input.select({ input.select({
prompt = "Inspect a property:", prompt = "Inspect a property:",
items = properties, items = properties,
keep_open = t.arg == "keep-open",
submit = function (i) submit = function (i)
if mp.get_property_native("vo-configured") then if mp.get_property_native("vo-configured") then
mp.commandv("expand-properties", "show-text", mp.commandv("expand-properties", "show-text",
@@ -608,7 +686,7 @@ mp.add_key_binding(nil, "show-properties", function ()
end end
end, end,
}) })
end) end, { complex = true })
local function system_open(path) local function system_open(path)
local args local args