Skip to content

Commit

Permalink
split dynamic menu logic to lua
Browse files Browse the repository at this point in the history
  • Loading branch information
tsl0922 committed Jan 4, 2024
1 parent 3ed8c34 commit 6737c21
Show file tree
Hide file tree
Showing 12 changed files with 686 additions and 564 deletions.
8 changes: 3 additions & 5 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -32,20 +32,18 @@ jobs:
cmake -DCMAKE_BUILD_TYPE=RELEASE \
-DCMAKE_SHARED_LINKER_FLAGS="-static" \
-G Ninja ..
cmake --build .
cmake --build . --target package
- uses: actions/upload-artifact@v3
with:
name: menu
path: build/menu.dll
path: build/menu.zip
publish:
needs: build
runs-on: ubuntu-22.04
if: ${{ github.ref == 'refs/heads/main' || github.ref_type == 'tag' }}
steps:
- uses: actions/checkout@v3
- uses: actions/download-artifact@v3
- name: Zip files
run: zip -r menu.zip menu
- uses: rickstaa/action-create-tag@v1
if: ${{ github.ref == 'refs/heads/main' }}
with:
Expand All @@ -55,7 +53,7 @@ jobs:
with:
commit: ${{ github.sha }}
tag: ${{ github.ref == 'refs/heads/main' && 'dev' || github.ref_name }}
artifacts: "menu.zip"
artifacts: "menu/menu.zip"
allowUpdates: true
prerelease: ${{ github.ref == 'refs/heads/main' }}
name: ${{ github.ref == 'refs/heads/main' && 'dev' || github.ref_name }}
Expand Down
9 changes: 8 additions & 1 deletion CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,11 @@ set(CMAKE_SHARED_LIBRARY_PREFIX "")
add_library(menu SHARED
mpv/misc/bstr.c
mpv/misc/dispatch.c
mpv/misc/node.c
mpv/ta/ta.c
mpv/ta/ta_talloc.c
mpv/ta/ta_utils.c

src/types.c
src/menu.c
src/plugin.c
)
Expand All @@ -23,3 +23,10 @@ set_property(TARGET menu PROPERTY POSITION_INDEPENDENT_CODE ON)
target_include_directories(menu PRIVATE mpv ${MPV_INCLUDE_DIRS})
target_link_libraries(menu PRIVATE shlwapi)
target_compile_definitions(menu PRIVATE MPV_CPLUGIN_DYNAMIC_SYM)

install(TARGETS menu RUNTIME DESTINATION .)

set(CPACK_GENERATOR ZIP)
set(CPACK_PACKAGE_FILE_NAME menu)
set(CPACK_INSTALLED_DIRECTORIES ${CMAKE_SOURCE_DIR}/lua .)
include(CPack)
45 changes: 31 additions & 14 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ See also [mpv-debug-plugin](https://github.com/tsl0922/mpv-debug-plugin).

[mpv](https://mpv.io) >= `0.37.0` is required, and the `cplugins` feature should be enabled.

Download the plugin from Releases, and place `menu.dll` in your mpv `scripts` folder.
Download the plugin from Releases, place `menu.dll` and `dyn_menu.lua` in your mpv `scripts` folder.

## Configuration

Expand All @@ -24,26 +24,22 @@ The menu syntax is similar to [mpv.net](https://github.com/mpvnet-player/mpv.net
- define separator with `-`
- split title with `>` to define submenus
- use `#@keyword` to display selection menu for:
- `#@tracks/video`: video tracks
- `#@tracks/audio`: audio tracks
- `#@tracks/sub`: subtitles
- `#@tracks/sub-secondary`: secondary subtitle
- `#@chapters`: chapters
- `#@editions`: editions
- `#@audio-devices`: audio devices
- `#@tracks`: track list (video/audio/subtitle)
- `#@tracks/video`: video track list
- `#@tracks/audio`: audio track list
- `#@tracks/sub`: subtitle list
- `#@tracks/sub-secondary`: subtitle list (secondary)
- `#@chapters`: chapter list
- `#@editions`: edition list
- `#@audio-devices`: audio device list
- use `_` if no keybinding
- use `ignore` if no command

```
Ctrl+a show-text foobar #menu: Foo > Bar
_ ignore #menu: -
_ ignore #menu: Tracks > Video #@tracks/video
_ ignore #menu: Tracks > Audio #@tracks/audio
_ ignore #menu: -
_ ignore #menu: Subtitle #@tracks/sub
_ ignore #menu: Second Subtitle #@tracks/sub-secondary
_ ignore #menu: -
_ ignore #menu: Tracks #@tracks
_ ignore #menu: Chapters #@chapters
_ ignore #menu: Editions #@editions
_ ignore #menu: -
Expand All @@ -56,10 +52,31 @@ Add a keybinding to trigger the menu (required):
MBTN_RIGHT script-message-to menu show
```

> **NOTE:** If the menu doesn't always show on mouse click, Rename other scripts that used the `menu` name.
>
> If both `menu.dll` and `menu.lua` exists in scripts folder, one of it may be named with `menu2` by mpv,
> `script-message-to menu show` will break when it happens on `menu.dll`.
### ~~/script-opts/menu.conf

- `uosc=yes`: Enalbe [uosc](https://github.com/tomasklaen/uosc#syntax) menu syntax support.

## Updating menu from another plugin

The menu data is stored in `user-data/menu/items` property with the following structure:

```
MPV_FORMAT_NODE_ARRAY
MPV_FORMAT_NODE_MAP (menu item)
"type" MPV_FORMAT_STRING (supported type: separator, submenu)
"title" MPV_FORMAT_STRING (required if type is not separator)
"cmd" MPV_FORMAT_STRING (optional)
"state" MPV_FORMAT_NODE_ARRAY[MPV_FORMAT_STRING] (supported state: checked, disabled)
"submenu" MPV_FORMAT_NODE_ARRAY[menu item] (required if type is submenu)
```

Updating this property will trigger an update of the menu UI.

## Credits

This project contains code copied from [mpv](https://github.com/mpv-player/mpv).
Expand Down
259 changes: 259 additions & 0 deletions lua/dyn_menu.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,259 @@
-- Copyright (c) 2023 tsl0922. All rights reserved.
-- SPDX-License-Identifier: GPL-2.0-only
--
-- #@keyword support for dynamic menu
--
-- supported keywords:
-- #@tracks: video/audio/sub tracks
-- #@tracks/video: video track list
-- #@tracks/audio: audio track list
-- #@tracks/sub: subtitle list
-- #@tracks/sub-secondary: subtitle list (secondary)
-- #@chapters: chapter list
-- #@editions: edition list
-- #@audio-devices: audio device list

local menu_prop = 'user-data/menu/items'
local menu_items = mp.get_property_native(menu_prop, {})

function build_track_title(track, prefix, filename)
local title = track.title or ''
local codec = track.codec or ''
local type = track.type

if track.external and title ~= '' then
if filename ~= '' then title = title:gsub(filename .. '%.?', '') end
if title:lower() == codec:lower() then title = '' end
end
if title == '' then
local name = type:sub(1, 1):upper() .. type:sub(2, #type)
title = string.format('%s %d', name, track.id)
end

local hints = {}
local function h(value) hints[#hints + 1] = value end
if codec ~= '' then h(codec) 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
if track['audio-channels'] then h(track['audio-channels'] .. 'ch') 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
if #hints > 0 then title = string.format('%s [%s]', title, table.concat(hints, ', ')) end

if track.lang then title = string.format('%s\t%s', title, track.lang:upper()) end
if prefix then title = string.format('%s: %s', type:sub(1, 1):upper(), title) end
return title
end

function build_track_items(list, type, prop, prefix)
local items = {}

local filename = mp.get_property('filename/no-ext', ''):gsub("[%(%)%.%%%+%-%*%?%[%]%^%$]", "%%%0")
local pos = mp.get_property_number(prop, -1)
for _, track in ipairs(list) do
if track.type == type then
local state = {}
if track.selected then
table.insert(state, 'checked')
if track.id ~= pos then table.insert(state, 'disabled') end
end

items[#items + 1] = {
title = build_track_title(track, prefix, filename),
cmd = string.format('set %s %d', prop, track.id),
state = state,
}
end
end

if #items > 0 then
local title = pos > 0 and 'Off' or 'Auto'
local value = pos > 0 and 'no' or 'auto'
if prefix then title = string.format('%s: %s', type:sub(1, 1):upper(), title) end

items[#items + 1] = {
title = title,
cmd = string.format('set %s %s', prop, value),
}
end

return items
end

function update_tracks_menu(submenu)
mp.observe_property('track-list', 'native', function(_, track_list)
for i = #submenu, 1, -1 do table.remove(submenu, i) end
if not track_list then return end

local items_v = build_track_items(track_list, 'video', 'vid', true)
local items_a = build_track_items(track_list, 'audio', 'aid', true)
local items_s = build_track_items(track_list, 'sub', 'sid', true)

for _, item in ipairs(items_v) do table.insert(submenu, item) end
if #submenu > 0 and #items_a > 0 then table.insert(submenu, { type = 'separator' }) end
for _, item in ipairs(items_a) do table.insert(submenu, item) end
if #submenu > 0 and #items_s > 0 then table.insert(submenu, { type = 'separator' }) end
for _, item in ipairs(items_s) do table.insert(submenu, item) end

mp.set_property_native(menu_prop, menu_items)
end)
end

function update_track_menu(submenu, type, prop)
mp.observe_property('track-list', 'native', function(_, track_list)
for i = #submenu, 1, -1 do table.remove(submenu, i) end
if not track_list then return end

local items = build_track_items(track_list, type, prop, false)
for _, item in ipairs(items) do table.insert(submenu, item) end

mp.set_property_native(menu_prop, menu_items)
end)
end

function update_chapters_menu(submenu)
mp.observe_property('chapter-list', 'native', function(_, chapter_list)
for i = #submenu, 1, -1 do table.remove(submenu, i) end
if not chapter_list then return end

local pos = mp.get_property_number('chapter', -1)
for id, chapter in ipairs(chapter_list) do
local title = chapter.title or ''
if title == '' then title = 'Chapter ' .. id end
local time = string.format('%02d:%02d:%02d', chapter.time / 3600, chapter.time / 60 % 60, chapter.time % 60)

submenu[#submenu + 1] = {
title = string.format('%s\t[%s]', title, time),
cmd = string.format('seek %f absolute', chapter.time),
state = id == pos + 1 and { 'checked' } or {},
}
end

mp.set_property_native(menu_prop, menu_items)
end)

mp.observe_property('chapter', 'number', function(_, pos)
if not pos then return end
for id, item in ipairs(submenu) do
item.state = id == pos + 1 and { 'checked' } or {}
end
mp.set_property_native(menu_prop, menu_items)
end)
end

function update_editions_menu(submenu)
mp.observe_property('edition-list', 'native', function(_, editions)
for i = #submenu, 1, -1 do table.remove(submenu, i) end
if not editions then return end

local current = mp.get_property_number('current-edition', -1)
for id, edition in ipairs(editions) do
local title = edition.title or ''
if title == '' then title = 'Edition ' .. id end
if edition.default then title = title .. ' [default]' end
submenu[#submenu + 1] = {
title = title,
cmd = string.format('set edition %d', id - 1),
state = id == current + 1 and { 'checked' } or {},
}
end

mp.set_property_native(menu_prop, menu_items)
end)

mp.observe_property('current-edition', 'number', function(_, pos)
if not pos then return end
for id, item in ipairs(submenu) do
item.state = id == pos + 1 and { 'checked' } or {}
end
mp.set_property_native(menu_prop, menu_items)
end)
end

function update_audio_devices_menu(submenu)
mp.observe_property('audio-device-list', 'native', function(_, devices)
for i = #submenu, 1, -1 do table.remove(submenu, i) end
if not devices then return end

local current = mp.get_property('audio-device', '')
for _, device in ipairs(devices) do
submenu[#submenu + 1] = {
title = device.description or device.name,
cmd = string.format('set audio-device %s', device.name),
state = device.name == current and { 'checked' } or {},
}
end

mp.set_property_native(menu_prop, menu_items)
end)

mp.observe_property('audio-device', 'string', function(_, name)
if not name then return end
for _, item in ipairs(submenu) do
item.state = item.cmd:match('%s*set audio%-device%s+(%S+)%s*$') == name and { 'checked' } or {}
end
mp.set_property_native(menu_prop, menu_items)
end)
end

function update_dyn_menu(submenu, keyword)
if keyword == 'tracks' then
update_tracks_menu(submenu)
elseif keyword == 'tracks/video' then
update_track_menu(submenu, "video", "vid")
elseif keyword == 'tracks/audio' then
update_track_menu(submenu, "audio", "aid")
elseif keyword == 'tracks/sub' then
update_track_menu(submenu, "sub", "sid")
elseif keyword == 'tracks/sub-secondary' then
update_track_menu(submenu, "sub", "secondary-sid")
elseif keyword == 'chapters' then
update_chapters_menu(submenu)
elseif keyword == 'editions' then
update_editions_menu(submenu)
elseif keyword == 'audio-devices' then
update_audio_devices_menu(submenu)
end
end

function check_keyword(items)
if not items then return end
for _, item in ipairs(items) do
if item.type == 'submenu' then
check_keyword(item.submenu)
else
if item.type ~= 'separator' and item.cmd then
local keyword = item.cmd:match('%s*#@([%S]+).-%s*$') or ''
if keyword ~= '' then
local submenu = {}

item.type = 'submenu'
item.submenu = submenu
item.cmd = nil

mp.set_property_native(menu_prop, menu_items)
update_dyn_menu(submenu, keyword)
end
end
end
end
end

function update_menu(name, items)
if items and #items > 0 then
mp.unobserve_property(update_menu)

menu_items = items
check_keyword(items)
end
end

if #menu_items > 0 then
check_keyword(menu_items)
else
mp.observe_property(menu_prop, 'native', update_menu)
end
Loading

0 comments on commit 6737c21

Please sign in to comment.