-
-
Notifications
You must be signed in to change notification settings - Fork 6
/
dyn_menu.lua
727 lines (635 loc) · 24.4 KB
/
dyn_menu.lua
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
-- Copyright (c) 2023-2024 tsl0922. All rights reserved.
-- SPDX-License-Identifier: GPL-2.0-only
local opts = require('mp.options')
local utils = require('mp.utils')
local msg = require('mp.msg')
-- user options
local o = {
use_mpv_impl = true, -- use mpv's menu implementation if available
uosc_syntax = false, -- toggle uosc menu syntax support
escape_title = true, -- escape & to && in menu title
max_title_length = 80, -- limit the title length, set to 0 to disable.
max_playlist_items = 20, -- limit the playlist items in submenu, set to 0 to disable.
}
opts.read_options(o)
local use_mpv_impl = o.use_mpv_impl and (mp.get_property_native('menu-data') ~= nil)
local menu_prop = use_mpv_impl and 'menu-data' or 'user-data/menu/items' -- menu data property
local menu_items = {} -- raw menu data
local menu_items_dirty = false -- menu data dirty flag
local dyn_menus = {} -- dynamic menu list
local keyword_to_menu = {} -- keyword -> menu
local has_uosc = false -- uosc installed flag
-- lua expression compiler (copied from mpv auto_profiles.lua)
------------------------------------------------------------------------
local watched_properties = {} -- indexed by property name (used as a set)
local cached_properties = {} -- property name -> last known raw value
local properties_to_menus = {} -- property name -> set of menus using it
local have_dirty_menus = false -- at least one menu is marked dirty
-- Used during evaluation of the menu update
local current_menu = nil
-- Cached set of all top-level mpv properities. Only used for extra validation.
local property_set = {}
for _, property in pairs(mp.get_property_native("property-list")) do
property_set[property] = true
end
local function on_property_change(name, val)
cached_properties[name] = val
-- Mark all menus reading this property as dirty, so they get re-evaluated
-- the next time the script goes back to sleep.
local dependent_menus = properties_to_menus[name]
if dependent_menus then
for menu, _ in pairs(dependent_menus) do
menu.dirty = true
have_dirty_menus = true
end
end
end
function get(name, default)
-- Normally, we use the cached value only
if not watched_properties[name] then
watched_properties[name] = true
local res, err = mp.get_property_native(name)
-- Property has to not exist and the toplevel of property in the name must also
-- not have an existing match in the property set for this to be considered an error.
-- This allows things like user-data/test to still work.
if err == "property not found" and property_set[name:match("^([^/]+)")] == nil then
msg.error("Property '" .. name .. "' was not found.")
return default
end
cached_properties[name] = res
mp.observe_property(name, "native", on_property_change)
end
-- The first time the property is read we need add it to the
-- properties_to_menus table, which will be used to mark the menu
-- dirty if a property referenced by it changes.
if current_menu then
local map = properties_to_menus[name]
if not map then
map = {}
properties_to_menus[name] = map
end
map[current_menu] = true
end
local val = cached_properties[name]
if val == nil then
val = default
end
return val
end
local function magic_get(name)
-- Lua identifiers can't contain "-", so in order to match with mpv
-- property conventions, replace "_" to "-"
name = string.gsub(name, "_", "-")
return get(name, nil)
end
local evil_magic = {}
setmetatable(evil_magic, {
__index = function(table, key)
-- interpret everything as property, unless it already exists as
-- a non-nil global value
local v = _G[key]
if type(v) ~= "nil" then
return v
end
return magic_get(key)
end,
})
p = {}
setmetatable(p, {
__index = function(table, key)
return magic_get(key)
end,
})
local function compile_expr(name, s)
local code, chunkname = "return " .. s, "expr " .. name
local chunk, err
if setfenv then -- lua 5.1
chunk, err = loadstring(code, chunkname)
if chunk then
setfenv(chunk, evil_magic)
end
else -- lua 5.2
chunk, err = load(code, chunkname, "t", evil_magic)
end
if not chunk then
msg.error("expr '" .. name .. "' : " .. err)
chunk = function() return false end
end
return chunk
end
------------------------------------------------------------------------
-- append menu item to menu
local function append_menu(menu, item)
if (item.title and o.escape_title) then
item.title = item.title:gsub('&', '&&')
end
menu[#menu + 1] = item
end
-- escape codec name to make it more readable
local function escape_codec(str)
if not str or str == '' then return '' end
if str:find("mpeg2") then return "mpeg2"
elseif str:find("dvvideo") then return "dv"
elseif str:find("pcm") then return "pcm"
elseif str:find("pgs") then return "pgs"
elseif str:find("subrip") then return "srt"
elseif str:find("vtt") then return "vtt"
elseif str:find("dvd_sub") then return "vob"
elseif str:find("dvb_sub") then return "dvb"
elseif str:find("dvb_tele") then return "teletext"
elseif str:find("arib") then return "arib"
else return str end
end
-- from http://lua-users.org/wiki/LuaUnicode
local UTF8_PATTERN = '[%z\1-\127\194-\244][\128-\191]*'
-- return a substring based on utf8 characters
-- like string.sub, but negative index is not supported
local function utf8_sub(s, i, j)
local t = {}
local idx = 1
for match in s:gmatch(UTF8_PATTERN) do
if j and idx > j then break end
if idx >= i then t[#t + 1] = match end
idx = idx + 1
end
return table.concat(t)
end
-- return the length of a utf8 string
local function utf8_len(s)
local _, count = s:gsub(UTF8_PATTERN, "")
return count
end
-- abbreviate title if it's too long
local function abbr_title(str)
if not str or str == '' then return '' end
if o.max_title_length > 0 and utf8_len(str) > o.max_title_length then
return utf8_sub(str, 1, o.max_title_length) .. '...'
end
return str
end
-- build track title from track metadata
--
-- example:
-- V: Video 1 [h264, 1920x1080, 23.976 fps] (*) JPN
-- | | | | |
-- type title hints default lang
local function build_track_title(track, prefix, filename)
local type = track.type
local title = track.title or ''
local codec = escape_codec(track.codec)
-- remove filename from title if it's external track
if track.external and title ~= '' then
if filename ~= '' then title = title:gsub(filename .. '%.?', '') end
if title:lower() == codec:lower() then title = '' end
end
-- set a default title if it's empty
if title == '' then
local name = type:sub(1, 1):upper() .. type:sub(2, #type)
title = string.format('%s %d', name, track.id)
else
title = abbr_title(title)
end
-- build hints from track metadata
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('%.5g fps', track['demux-fps'])) end
if track['audio-channels'] then h(track['audio-channels'] .. ' ch') end
if track['demux-samplerate'] then h(string.format('%.5g kHz', track['demux-samplerate'] / 1000)) end
if track['demux-bitrate'] then h(string.format('%.5g kbps', track['demux-bitrate'] / 1000)) end
if #hints > 0 then title = string.format('%s [%s]', title, table.concat(hints, ', ')) end
-- put some important info at the end
if track.forced then title = title .. ' (forced)' end
if track.external then title = title .. ' (external)' end
if track.default then title = title .. ' (*)' end
-- prepend a 1-letter type prefix, used when displaying multiple track types
if prefix then title = string.format('%s: %s', type:sub(1, 1):upper(), title) end
return title
end
-- build track menu items from track list for given type
local function build_track_items(list, type, prop, prefix)
local items = {}
-- filename without extension, escaped for pattern matching
local filename = get('filename/no-ext', ''):gsub("[%(%)%.%%%+%-%*%?%[%]%^%$]", "%%%0")
local pos = tonumber(get(prop)) or -1
for _, track in ipairs(list) do
if track.type == type then
local state = {}
if track.selected and track.id == pos then
state[#state + 1] = 'checked'
if type == 'sub' then
if (prop == 'sid' and not get('sub-visibility')) or
(prop == 'secondary-sid' and not get('secondary-sub-visibility'))
then
state[#state + 1] = 'disabled'
end
end
end
items[#items + 1] = {
title = build_track_title(track, prefix, filename),
shortcut = (track.lang and track.lang ~= '') and track.lang or nil,
cmd = string.format('set %s %d', prop, track.id),
state = state,
}
end
end
-- add an extra item to disable or re-enable the track
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
-- update menu item to a submenu
local function to_submenu(item)
item.type = 'submenu'
item.submenu = {}
item.cmd = nil
menu_items_dirty = true
return item.submenu
end
-- handle #@tracks menu update
local function update_tracks_menu(menu)
local submenu = to_submenu(menu.item)
local track_list = get('track-list', {})
if #track_list == 0 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)
-- append video/audio/sub tracks into one submenu, separated by a separator
for _, item in ipairs(items_v) do append_menu(submenu, item) end
if #submenu > 0 and #items_a > 0 then append_menu(submenu, { type = 'separator' }) end
for _, item in ipairs(items_a) do append_menu(submenu, item) end
if #submenu > 0 and #items_s > 0 then append_menu(submenu, { type = 'separator' }) end
for _, item in ipairs(items_s) do append_menu(submenu, item) end
end
-- handle #@tracks/<type> menu update for given type
local function update_track_menu(menu, type, prop)
local submenu = to_submenu(menu.item)
local track_list = get('track-list', {})
if #track_list == 0 then return end
local items = build_track_items(track_list, type, prop, false)
for _, item in ipairs(items) do append_menu(submenu, item) end
end
-- handle #@chapters menu update
local function update_chapters_menu(menu)
local submenu = to_submenu(menu.item)
local chapter_list = get('chapter-list', {})
if #chapter_list == 0 then return end
local pos = get('chapter', -1)
for id, chapter in ipairs(chapter_list) do
local title = abbr_title(chapter.title)
if title == '' then title = 'Chapter ' .. id end
append_menu(submenu, {
title = title,
shortcut = string.format('[%02d:%02d:%02d]', chapter.time / 3600, chapter.time / 60 % 60, chapter.time % 60),
cmd = string.format('seek %f absolute', chapter.time),
state = id == pos + 1 and { 'checked' } or {},
})
end
end
-- handle #@edition menu update
local function update_editions_menu(menu)
local submenu = to_submenu(menu.item)
local edition_list = get('edition-list', {})
if #edition_list == 0 then return end
local current = get('current-edition', -1)
for id, edition in ipairs(edition_list) do
local title = abbr_title(edition.title)
if title == '' then title = 'Edition ' .. id end
if edition.default then title = title .. ' [default]' end
append_menu(submenu, {
title = title,
cmd = string.format('set edition %d', id - 1),
state = id == current + 1 and { 'checked' } or {},
})
end
end
-- handle #@audio-devices menu update
local function update_audio_devices_menu(menu)
local submenu = to_submenu(menu.item)
local device_list = get('audio-device-list', {})
if #device_list == 0 then return end
local current = get('audio-device', '')
for _, device in ipairs(device_list) do
append_menu(submenu, {
title = device.description or device.name,
cmd = string.format('set audio-device %s', device.name),
state = device.name == current and { 'checked' } or {},
})
end
end
-- build playlist item title
local function build_playlist_title(item, id)
local title = item.title or ''
local ext = ''
if item.filename and item.filename ~= '' then
local _, filename = utils.split_path(item.filename)
local n, e = filename:match('^(.+)%.([%w-_]+)$')
if title == '' then title = n and n or filename end
if e then ext = e end
end
title = title ~= '' and abbr_title(title) or 'Item ' .. id
return title, ext
end
-- handle #@playlist menu update
local function update_playlist_menu(menu)
local submenu = to_submenu(menu.item)
local playlist = get('playlist', {})
if #playlist == 0 then return end
local from, to = 1, #playlist
if o.max_playlist_items > 0 then
local pos = get('playlist-playing-pos', -1)
if pos == -1 then pos = get('playlist-pos', -1) end
local mid = math.floor(o.max_playlist_items / 2)
from, to = pos + 1 - mid, pos + (o.max_playlist_items - mid)
if from < 1 then from, to = 1, o.max_playlist_items end
if to > #playlist then from, to = #playlist - o.max_playlist_items + 1, #playlist end
end
if from > 1 then
append_menu(submenu, {
title = '...',
shortcut = string.format('[%d]', from - 1),
cmd = has_uosc and 'script-message-to uosc playlist' or 'ignore',
})
end
for id = from, to do
local item = playlist[id]
if item then
local title, ext = build_playlist_title(item, id - 1)
append_menu(submenu, {
title = build_playlist_title(item, id - 1),
shortcut = (ext and ext ~= '') and ext:upper() or nil,
cmd = string.format('playlist-play-index %d', id - 1),
state = (item.playing or item.current) and { 'checked' } or {},
})
end
end
if to < #playlist then
append_menu(submenu, {
title = '...',
shortcut = string.format('[%d]', #playlist - to),
cmd = has_uosc and 'script-message-to uosc playlist' or 'ignore',
})
end
end
-- handle #@profiles menu update
local function update_profiles_menu(menu)
local submenu = to_submenu(menu.item)
local profile_list = get('profile-list', {})
if #profile_list == 0 then return end
for _, profile in ipairs(profile_list) do
if not (profile.name == 'default' or profile.name:find('gui') or
profile.name == 'encoding' or profile.name == 'libmpv') then
append_menu(submenu, {
title = profile.name,
cmd = string.format('show-text %s; apply-profile %s', profile.name, profile.name),
})
end
end
end
-- handle menu state update
local function update_menu_state(menu)
if not menu.state then return end
local status, res = pcall(menu.state)
if not status then
msg.verbose("state expr error on evaluating: " .. res)
return
end
local state = {}
if type(res) == 'string' then
for s in res:gmatch('[^,%s]+') do state[#state + 1] = s end
end
menu.item.state = state
menu_items_dirty = true
end
-- dynamic menu updaters
local dyn_updaters = {
['tracks'] = update_tracks_menu,
['tracks/video'] = function(menu) update_track_menu(menu, 'video', 'vid') end,
['tracks/audio'] = function(menu) update_track_menu(menu, 'audio', 'aid') end,
['tracks/sub'] = function(menu) update_track_menu(menu, 'sub', 'sid') end,
['tracks/sub-secondary'] = function(menu) update_track_menu(menu, 'sub', 'secondary-sid') end,
['chapters'] = update_chapters_menu,
['editions'] = update_editions_menu,
['audio-devices'] = update_audio_devices_menu,
['playlist'] = update_playlist_menu,
['profiles'] = update_profiles_menu,
}
-- handle dynamic menu update
local function update_menu(menu)
if menu.updater then
msg.debug('update menu: ' .. menu.item.title)
current_menu = menu
menu.updater(menu)
current_menu = nil
end
end
-- load dynamic menu item
local function dyn_menu_load(item, keyword)
local menu = {
item = item,
updater = nil,
state = nil,
dirty = false,
}
dyn_menus[#dyn_menus + 1] = menu
keyword_to_menu[keyword] = menu
local expr = keyword:match('^state=(.-)%s*$')
if expr then
menu.updater = update_menu_state
menu.state = compile_expr(string.format('[%s]:%s', item.title, keyword), expr)
else
keyword = keyword:match('^([%S]+).*$')
menu.updater = dyn_updaters[keyword]
end
-- update menu immediately
if menu.updater then update_menu(menu) end
end
-- find #@keyword for dynamic menu and handle updates
--
-- cplugin will keep the trailing comments in the cmd field, so we can
-- parse the keyword from it.
--
-- example: ignore #menu: Chapters #@chapters # extra comment
local function dyn_menu_check(items)
if not items then return end
for _, item in ipairs(items) do
if item.type == 'submenu' then
dyn_menu_check(item.submenu)
else
if item.type ~= 'separator' and item.cmd then
local keyword = item.cmd:match('%s*#@(.-)%s*$') or ''
if keyword ~= '' then
msg.debug('load menu: ' .. item.title, ', keyword: ' .. keyword)
dyn_menu_load(item, keyword)
end
end
end
end
end
-- load dynamic menus
local function load_dyn_menus()
dyn_menu_check(menu_items)
-- broadcast menu ready message
mp.commandv('script-message', 'menu-ready', mp.get_script_name())
end
-- read input.conf content
local function get_input_conf()
local prop = mp.get_property_native('input-conf')
if prop:sub(1, 9) == 'memory://' then return prop:sub(10) end
prop = prop == '' and '~~/input.conf' or prop
local conf_path = mp.command_native({ 'expand-path', prop })
local f, err = io.open(conf_path, 'rb')
if not f then
msg.error('failed to open file: ' .. conf_path)
return nil
end
local conf = f:read('*all')
f:close()
return conf
end
-- parse input.conf, return menu items
local function parse_input_conf(conf)
local function parse_line(line)
local c = line:match('^%s*#')
if c and (not o.uosc_syntax) then return end
local key, cmd = line:match('%s*([%S]+)%s+(.-)%s*$')
if key and key:match('^#%S+') then return end
return ((o.uosc_syntax and c) and '' or key), cmd
end
local function extract_title(cmd)
if not cmd or cmd == '' then return '' end
local title = cmd:match('#menu:%s*(.*)%s*')
if not title and o.uosc_syntax then title = cmd:match('#!%s*(.*)%s*') end
if title then title = title:match('(.-)%s*#.*$') or title end
return title or ''
end
local function split_title(title)
local list = {}
if not title or title == '' then return list end
local pattern = '(.-)%s*>%s*'
local last_ends = 1
local starts, ends, match = title:find(pattern)
while starts do
list[#list + 1] = match
last_ends = ends + 1
starts, ends, match = title:find(pattern, last_ends)
end
if last_ends < (#title + 1) then list[#list + 1] = title:sub(last_ends) end
return list
end
local items = {}
local by_id = {}
for line in conf:gmatch('[^\r\n]+') do
local key, cmd = parse_line(line)
local list = split_title(extract_title(cmd))
local submenu_id = ''
local target_menu = items
for id, name in ipairs(list) do
if id < #list then
submenu_id = submenu_id .. name
if not by_id[submenu_id] then
local submenu = {}
by_id[submenu_id] = submenu
append_menu(target_menu, { type = 'submenu', title = name, submenu = submenu })
end
target_menu = by_id[submenu_id]
else
if name == '-' or (o.uosc_syntax and name:sub(1, 3) == '---') then
append_menu(target_menu, { type = 'separator' })
else
local shortcut = (key ~= '' and key ~= '_') and key or nil
append_menu(target_menu, { title = name, shortcut = shortcut, cmd = cmd })
end
end
end
end
return items
end
-- script message: get <keyword> <src>
mp.register_script_message('get', function(keyword, src)
if not src or src == '' then
msg.debug('get: ignored message with empty src')
return
end
local menu = keyword_to_menu[keyword]
local reply = { keyword = keyword }
if menu then reply.item = menu.item else reply.error = 'keyword not found' end
mp.commandv('script-message-to', src, 'menu-get-reply', utils.format_json(reply))
end)
-- script message: update <keyword> <json>
mp.register_script_message('update', function(keyword, json)
local menu = keyword_to_menu[keyword]
if not menu then
msg.debug('update: ignored message with invalid keyword:', keyword)
return
end
local data, err = utils.parse_json(json)
if err then msg.error('update: failed to parse json:', err) end
if not data or next(data) == nil then
msg.debug('update: ignored message with invalid json:', json)
return
end
local item = menu.item
if not data.title or data.title == '' then data.title = item.title end
if not data.type or data.type == '' then data.type = item.type end
for k, _ in pairs(item) do item[k] = nil end
for k, v in pairs(data) do item[k] = v end
menu_items_dirty = true
end)
-- detect uosc installation
mp.register_script_message('uosc-version', function() has_uosc = true end)
-- update menu on idle, this reduces the update frequency
mp.register_idle(function()
if have_dirty_menus then
for _, menu in ipairs(dyn_menus) do
if menu.dirty then
update_menu(menu)
menu.dirty = false
end
end
have_dirty_menus = false
end
if menu_items_dirty then
msg.debug('commit menu items: ' .. menu_prop)
mp.set_property_native(menu_prop, menu_items)
menu_items_dirty = false
end
end)
-- menu implementation related initialization
if use_mpv_impl then
-- IMPORTANT: make menu work on vo change
mp.observe_property('current-vo', 'native', function(name, val)
if val then menu_items_dirty = true end
end)
mp.add_key_binding('MBTN_RIGHT', nil, function()
mp.commandv('context-menu')
end)
else
local menu_native = 'menu'
mp.register_script_message('menu-init', function(name)
menu_native = name
end)
mp.add_key_binding('MBTN_RIGHT', 'show', function()
mp.commandv('script-message-to', menu_native, 'show')
end)
end
-- load menu data from input.conf
--
-- NOTE: to simplify the code, we don't watch for the menu data change event, this
-- make it conflict with other scripts that also update the menu data property.
local conf = get_input_conf()
if conf then
menu_items = parse_input_conf(conf)
menu_items_dirty = true
load_dyn_menus()
end