From 9fb200b6419c2707a5ec8d0786ae24ed02194297 Mon Sep 17 00:00:00 2001 From: "Avi Halachmi (:avih)" Date: Fri, 16 Jul 2021 11:17:50 +0300 Subject: 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. --- player/lua/stats.lua | 151 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 151 insertions(+) (limited to 'player') diff --git a/player/lua/stats.lua b/player/lua/stats.lua index 93dc453da4..367c019cd4 100644 --- a/player/lua/stats.lua +++ b/player/lua/stats.lua @@ -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 }, } -- cgit v1.2.3