stats.lua: add page 4: active key-bindings list

This is a scrollable page which also works nicely with the terminal
OSD. Typically there are more than 100 bound keys.

It groups the binding using fuzzy property/command/script name after
simple analysis of the command string, and then further sorts the
binding in each group according to the "complexity" of the key itself
(plain keys -> keys with modifiers, alphabetically, etc).

The alignment/grouping style is heavily inspired by @medhefgo's #8924
but otherwise it's an independent implementation.
This commit is contained in:
Avi Halachmi (:avih) 2021-07-16 11:17:50 +03:00 committed by avih
parent 59c10274b4
commit 9fb200b641
2 changed files with 168 additions and 0 deletions

View File

@ -24,6 +24,7 @@ stats:
1 Show usual stats
2 Show frame timings (scroll)
3 Input cache stats
4 Active key bindings (scroll)
0 Internal stuff (scroll)
==== ==================
@ -64,6 +65,8 @@ Configurable Options
Default: 2
``key_page_3``
Default: 3
``key_page_4``
Default: 4
``key_page_0``
Default: 0
@ -183,6 +186,20 @@ Using ``input.conf``, it is also possible to directly display a certain page::
i script-binding stats/display-page-1
e script-binding stats/display-page-2
Active key bindings page
~~~~~~~~~~~~~~~~~~~~~~~~
Lists the active key bindings and the commands they're bound to, excluding the
interactive keys of the stats script itself. See also `--input-test`_ for more
detailed view of each binding.
The keys are grouped automatically using a simple analysis of the command
string, and one should not expect documentation-level grouping accuracy,
however, it should still be reasonably useful.
Like with ``--input-test``, the list includes bindings from ``input.conf`` and
from user scripts. Use `--no-config`` to list only built-in bindings.
Internal stuff page
~~~~~~~~~~~~~~~~~~~

View File

@ -18,6 +18,7 @@ local o = {
key_page_1 = "1",
key_page_2 = "2",
key_page_3 = "3",
key_page_4 = "4",
key_page_0 = "0",
-- For pages which support scrolling
key_scroll_up = "UP",
@ -351,6 +352,132 @@ local function append_perfdata(s, dedicated_page)
end
end
local function ellipsis(s, maxlen)
if not maxlen or s:len() <= maxlen then return s end
return s:sub(1, maxlen - 3) .. "..."
end
-- command prefix tokens to strip - includes generic property commands
local cmd_prefixes = {
osd_auto=1, no_osd=1, osd_bar=1, osd_msg=1, osd_msg_bar=1, raw=1, sync=1,
async=1, expand_properties=1, repeatable=1, set=1, add=1, multiply=1,
toggle=1, cycle=1, cycle_values=1, ["!reverse"]=1, change_list=1,
}
-- commands/writable-properties prefix sub-words (followed by -) to strip
local name_prefixes = {
define=1, delete=1, enable=1, disable=1, dump=1, write=1, drop=1, revert=1,
ab=1, hr=1, secondary=1,
}
-- extract a command "subject" from a command string, by removing all
-- generic prefix tokens and then returning the first interesting sub-word
-- of the next token. For target-script name we also check another token.
-- The tokenizer works fine for things we care about - valid mpv commands,
-- properties and script names, possibly quoted, white-space[s]-separated.
-- It's decent in practice, and worst case is "incorrect" subject.
local function cmd_subject(cmd)
cmd = cmd:gsub(";.*", ""):gsub("%-", "_") -- only first cmd, s/-/_/
local TOKEN = '^%s*"?([%w_!]*)' -- captures+ends before a (maybe) final "
local tok, sname, subw
repeat tok, cmd = cmd:match(TOKEN .. '"?(.*)')
until not cmd_prefixes[tok]
-- tok is the 1st non-generic command/property name token, cmd is the rest
sname = tok == "script_message_to" and cmd:match(TOKEN)
or tok == "script_binding" and cmd:match(TOKEN .. "/")
if sname and sname ~= "" then
return "script: " .. sname
end
-- return the first sub-word of tok which is not a useless prefix
repeat subw, tok = tok:match("([^_]*)_?(.*)")
until tok == "" or not name_prefixes[subw]
return subw:len() > 1 and subw or "[unknown]"
end
local function get_kbinfo_lines()
-- active keys: only highest priotity of each key, and not our (stats) keys
local bindings = mp.get_property_native("input-bindings", {})
local active = {} -- map: key-name -> bind-info
for _, bind in pairs(bindings) do
if bind.priority >= 0 and (
not active[bind.key] or
(active[bind.key].is_weak and not bind.is_weak) or
(bind.is_weak == active[bind.key].is_weak and
bind.priority > active[bind.key].priority)
) and not bind.cmd:find("script-binding stats/__key", 1, true)
then
active[bind.key] = bind
end
end
-- make an array, find max key len, add sort keys (.subject/.mods[_count])
local ordered = {}
local kspaces = "" -- as many spaces as the longest key name
for _, bind in pairs(active) do
bind.subject = cmd_subject(bind.cmd)
if bind.subject ~= "ignore" then
ordered[#ordered+1] = bind
_,_, bind.mods = bind.key:find("(.*)%+.")
_, bind.mods_count = bind.key:gsub("%+.", "")
if bind.key:len() > kspaces:len() then
kspaces = string.rep(" ", bind.key:len())
end
end
end
local function align_right(key)
return kspaces:sub(key:len()) .. key
end
-- sort by: subject, mod(ifier)s count, mods, key-len, lowercase-key, key
table.sort(ordered, function(a, b)
if a.subject ~= b.subject then
return a.subject < b.subject
elseif a.mods_count ~= b.mods_count then
return a.mods_count < b.mods_count
elseif a.mods ~= b.mods then
return a.mods < b.mods
elseif a.key:len() ~= b.key:len() then
return a.key:len() < b.key:len()
elseif a.key:lower() ~= b.key:lower() then
return a.key:lower() < b.key:lower()
else
return a.key > b.key -- only case differs, lowercase first
end
end)
-- key/subject pre/post formatting for terminal/ass.
-- key/subject alignment uses spaces (with mono font if ass)
-- word-wrapping is disabled for ass, or cut at 79 for the terminal
local term = not o.use_ass
local kpre = term and "" or format("{\\q2\\fn%s}", o.font_mono)
local kpost = term and " " or format(" {\\fn%s}", o.font)
local spre = term and kspaces .. " "
or format("{\\q2\\fn%s}%s {\\fn%s}{\\fs%d\\u1}",
o.font_mono, kspaces, o.font, 1.3*o.font_size)
local spost = term and "" or format("{\\u0\\fs%d}", o.font_size)
local _, itabs = o.indent:gsub("\t", "")
local cutoff = term and 79 - o.indent:len() - itabs * 7 - spre:len()
-- create the display lines
local info_lines = {}
local subject = nil
for _, bind in ipairs(ordered) do
if bind.subject ~= subject then -- new subject (title)
subject = bind.subject
append(info_lines, "", {})
append(info_lines, "", { prefix = spre .. subject .. spost })
end
if bind.comment then
bind.cmd = bind.cmd .. " # " .. bind.comment
end
append(info_lines, ellipsis(bind.cmd, cutoff),
{ prefix = kpre .. no_ASS(align_right(bind.key)) .. kpost })
end
return info_lines
end
local function append_general_perfdata(s, offset)
local perf_info = mp.get_property_native("perf-info") or {}
local count = 0
@ -665,6 +792,29 @@ local function vo_stats()
return table.concat(stats)
end
local kbinfo_lines = nil
local function keybinding_info(after_scroll)
local header = {}
local page = pages[o.key_page_4]
eval_ass_formatting()
add_header(header)
append(header, "", {prefix=o.nl .. page.desc .. ":", nl="", indent=""})
if not kbinfo_lines or not after_scroll then
kbinfo_lines = get_kbinfo_lines()
end
-- up to 20 lines for the terminal - so that mpv can also print
-- the status line without scrolling, and up to 40 lines for libass
-- because it can put a big performance toll on libass to process
-- many lines which end up outside (below) the screen.
local term = not o.use_ass
local nlines = #kbinfo_lines
page.offset = max(1, min((page.offset or 1), term and nlines - 20 or nlines))
local maxline = min(nlines, page.offset + (term and 20 or 40))
return table.concat(header) ..
table.concat(kbinfo_lines, "", page.offset, maxline)
end
local function perf_stats()
local stats = {}
eval_ass_formatting()
@ -800,6 +950,7 @@ pages = {
[o.key_page_1] = { f = default_stats, desc = "Default" },
[o.key_page_2] = { f = vo_stats, desc = "Extended Frame Timings", scroll = true },
[o.key_page_3] = { f = cache_stats, desc = "Cache Statistics" },
[o.key_page_4] = { f = keybinding_info, desc = "Active key bindings", scroll = true },
[o.key_page_0] = { f = perf_stats, desc = "Internal performance info", scroll = true },
}