summaryrefslogtreecommitdiffstats
path: root/.config/mpv/scripts/uosc_shared/lib/menus.lua
diff options
context:
space:
mode:
Diffstat (limited to '.config/mpv/scripts/uosc_shared/lib/menus.lua')
-rw-r--r--.config/mpv/scripts/uosc_shared/lib/menus.lua282
1 files changed, 282 insertions, 0 deletions
diff --git a/.config/mpv/scripts/uosc_shared/lib/menus.lua b/.config/mpv/scripts/uosc_shared/lib/menus.lua
new file mode 100644
index 0000000..e2a4ccc
--- /dev/null
+++ b/.config/mpv/scripts/uosc_shared/lib/menus.lua
@@ -0,0 +1,282 @@
+---@param data MenuData
+---@param opts? {submenu?: string; mouse_nav?: boolean}
+function open_command_menu(data, opts)
+ local menu = Menu:open(data, function(value)
+ if type(value) == 'string' then
+ mp.command(value)
+ else
+ ---@diagnostic disable-next-line: deprecated
+ mp.commandv(unpack(value))
+ end
+ end, opts)
+ if opts and opts.submenu then menu:activate_submenu(opts.submenu) end
+ return menu
+end
+
+---@param opts? {submenu?: string; mouse_nav?: boolean}
+function toggle_menu_with_items(opts)
+ if Menu:is_open('menu') then Menu:close()
+ else open_command_menu({type = 'menu', items = config.menu_items}, opts) end
+end
+
+---@param options {type: string; title: string; list_prop: string; active_prop?: string; serializer: fun(list: any, active: any): MenuDataItem[]; on_select: fun(value: any)}
+function create_self_updating_menu_opener(options)
+ return function()
+ if Menu:is_open(options.type) then Menu:close() return end
+ local list = mp.get_property_native(options.list_prop)
+ local active = options.active_prop and mp.get_property_native(options.active_prop) or nil
+ local menu
+
+ local function update() menu:update_items(options.serializer(list, active)) end
+
+ local ignore_initial_list = true
+ local function handle_list_prop_change(name, value)
+ if ignore_initial_list then ignore_initial_list = false
+ else list = value update() end
+ end
+
+ local ignore_initial_active = true
+ local function handle_active_prop_change(name, value)
+ if ignore_initial_active then ignore_initial_active = false
+ else active = value update() end
+ end
+
+ local initial_items, selected_index = options.serializer(list, active)
+
+ -- Items and active_index are set in the handle_prop_change callback, since adding
+ -- a property observer triggers its handler immediately, we just let that initialize the items.
+ menu = Menu:open(
+ {type = options.type, title = options.title, items = initial_items, selected_index = selected_index},
+ options.on_select, {
+ on_open = function()
+ mp.observe_property(options.list_prop, 'native', handle_list_prop_change)
+ if options.active_prop then
+ mp.observe_property(options.active_prop, 'native', handle_active_prop_change)
+ end
+ end,
+ on_close = function()
+ mp.unobserve_property(handle_list_prop_change)
+ mp.unobserve_property(handle_active_prop_change)
+ end,
+ })
+ end
+end
+
+function create_select_tracklist_type_menu_opener(menu_title, track_type, track_prop, load_command)
+ local function serialize_tracklist(tracklist)
+ local items = {}
+
+ if load_command then
+ items[#items + 1] = {
+ title = 'Load', bold = true, italic = true, hint = 'open file', value = '{load}', separator = true,
+ }
+ end
+
+ local first_item_index = #items + 1
+ local active_index = nil
+ local disabled_item = nil
+
+ -- Add option to disable a subtitle track. This works for all tracks,
+ -- but why would anyone want to disable audio or video? Better to not
+ -- let people mistakenly select what is unwanted 99.999% of the time.
+ -- If I'm mistaken and there is an active need for this, feel free to
+ -- open an issue.
+ if track_type == 'sub' then
+ disabled_item = {title = 'Disabled', italic = true, muted = true, hint = '—', value = nil, active = true}
+ items[#items + 1] = disabled_item
+ end
+
+ for _, track in ipairs(tracklist) do
+ if track.type == track_type then
+ local hint_values = {}
+ local function h(value) hint_values[#hint_values + 1] = value end
+
+ if track.lang then h(track.lang:upper()) end
+ if track['demux-h'] then
+ h(track['demux-w'] and (track['demux-w'] .. 'x' .. track['demux-h']) or (track['demux-h'] .. 'p'))
+ end
+ if track['demux-fps'] then h(string.format('%.5gfps', track['demux-fps'])) end
+ h(track.codec)
+ if track['audio-channels'] then h(track['audio-channels'] .. ' channels') end
+ if track['demux-samplerate'] then h(string.format('%.3gkHz', track['demux-samplerate'] / 1000)) end
+ if track.forced then h('forced') end
+ if track.default then h('default') end
+ if track.external then h('external') end
+
+ items[#items + 1] = {
+ title = (track.title and track.title or 'Track ' .. track.id),
+ hint = table.concat(hint_values, ', '),
+ value = track.id,
+ active = track.selected,
+ }
+
+ if track.selected then
+ if disabled_item then disabled_item.active = false end
+ active_index = #items
+ end
+ end
+ end
+
+ return items, active_index or first_item_index
+ end
+
+ local function selection_handler(value)
+ if value == '{load}' then
+ mp.command(load_command)
+ else
+ mp.commandv('set', track_prop, value and value or 'no')
+
+ -- If subtitle track was selected, assume user also wants to see it
+ if value and track_type == 'sub' then
+ mp.commandv('set', 'sub-visibility', 'yes')
+ end
+ end
+ end
+
+ return create_self_updating_menu_opener({
+ title = menu_title,
+ type = track_type,
+ list_prop = 'track-list',
+ serializer = serialize_tracklist,
+ on_select = selection_handler,
+ })
+end
+
+---@alias NavigationMenuOptions {type: string, title?: string, allowed_types?: string[], active_path?: string, selected_path?: string; on_open?: fun(); on_close?: fun()}
+
+-- Opens a file navigation menu with items inside `directory_path`.
+---@param directory_path string
+---@param handle_select fun(path: string): nil
+---@param opts NavigationMenuOptions
+function open_file_navigation_menu(directory_path, handle_select, opts)
+ directory = serialize_path(normalize_path(directory_path))
+ opts = opts or {}
+
+ if not directory then
+ msg.error('Couldn\'t serialize path "' .. directory_path .. '.')
+ return
+ end
+
+ local files, directories = read_directory(directory.path, opts.allowed_types)
+ local is_root = not directory.dirname
+ local path_separator = path_separator(directory.path)
+
+ if not files or not directories then return end
+
+ sort_filenames(directories)
+ sort_filenames(files)
+
+ -- Pre-populate items with parent directory selector if not at root
+ -- Each item value is a serialized path table it points to.
+ local items = {}
+
+ if is_root then
+ if state.os == 'windows' then
+ items[#items + 1] = {title = '..', hint = 'Drives', value = '{drives}', separator = true}
+ end
+ else
+ items[#items + 1] = {title = '..', hint = 'parent dir', value = directory.dirname, separator = true}
+ end
+
+ local back_path = items[#items] and items[#items].value
+ local selected_index = #items + 1
+
+ for _, dir in ipairs(directories) do
+ items[#items + 1] = {title = dir, value = join_path(directory.path, dir), hint = path_separator}
+ end
+
+ for _, file in ipairs(files) do
+ items[#items + 1] = {title = file, value = join_path(directory.path, file)}
+ end
+
+ for index, item in ipairs(items) do
+ if not item.value.is_to_parent and opts.active_path == item.value then
+ item.active = true
+ if not opts.selected_path then selected_index = index end
+ end
+
+ if opts.selected_path == item.value then selected_index = index end
+ end
+
+ local function open_path(path)
+ local is_drives = path == '{drives}'
+ local is_to_parent = is_drives or #path < #directory_path
+ local inheritable_options = {
+ type = opts.type, title = opts.title, allowed_types = opts.allowed_types, active_path = opts.active_path,
+ }
+
+ if is_drives then
+ open_drives_menu(function(drive_path)
+ open_file_navigation_menu(drive_path, handle_select, inheritable_options)
+ end, {
+ type = inheritable_options.type, title = inheritable_options.title, selected_path = directory.path,
+ on_open = opts.on_open, on_close = opts.on_close,
+ })
+ return
+ end
+
+ local info, error = utils.file_info(path)
+
+ if not info then
+ msg.error('Can\'t retrieve path info for "' .. path .. '". Error: ' .. (error or ''))
+ return
+ end
+
+ if info.is_dir then
+ -- Preselect directory we are coming from
+ if is_to_parent then
+ inheritable_options.selected_path = directory.path
+ end
+
+ open_file_navigation_menu(path, handle_select, inheritable_options)
+ else
+ handle_select(path)
+ end
+ end
+
+ local function handle_back()
+ if back_path then open_path(back_path) end
+ end
+
+ local menu_data = {
+ type = opts.type, title = opts.title or directory.basename .. path_separator, items = items,
+ selected_index = selected_index,
+ }
+ local menu_options = {on_open = opts.on_open, on_close = opts.on_close, on_back = handle_back}
+
+ return Menu:open(menu_data, open_path, menu_options)
+end
+
+-- Opens a file navigation menu with Windows drives as items.
+---@param handle_select fun(path: string): nil
+---@param opts? NavigationMenuOptions
+function open_drives_menu(handle_select, opts)
+ opts = opts or {}
+ local process = mp.command_native({
+ name = 'subprocess',
+ capture_stdout = true,
+ playback_only = false,
+ args = {'wmic', 'logicaldisk', 'get', 'name', '/value'},
+ })
+ local items, selected_index = {}, 1
+
+ if process.status == 0 then
+ for _, value in ipairs(split(process.stdout, '\n')) do
+ local drive = string.match(value, 'Name=([A-Z]:)')
+ if drive then
+ local drive_path = normalize_path(drive)
+ items[#items + 1] = {
+ title = drive, hint = 'drive', value = drive_path, active = opts.active_path == drive_path,
+ }
+ if opts.selected_path == drive_path then selected_index = #items end
+ end
+ end
+ else
+ msg.error(process.stderr)
+ end
+
+ return Menu:open(
+ {type = opts.type, title = opts.title or 'Drives', items = items, selected_index = selected_index},
+ handle_select
+ )
+end