local mp = require("mp") -- isn't actually required, mp still gonna be defined
local utils = require("mp.utils")
-- NOTE: should not be altered here, edit options in corresponding .conf file
local opts = {
executable = "rofi",
executable_flags = "-dmenu -i -markup-rows",
strip_cmd_at = 65,
sort_commands_by = "priority",
toggle_menu_binding = "t",
pause_on_open = true,
resume_on_exit = "only-if-was-paused", -- another possible value is true
}
(require("mp.options")).read_options(opts, mp.get_script_name())
local mx = {
list = {}, -- list of all commands (tables)
lines = {}, -- command tables brought to pango markup strings concat with '\n'
was_paused = false, -- flag that indicates that vid was paused by this script
}
function mx:init()
setmetatable({}, self)
self.__index = self
self:get_cmd_list()
self:sort_cmd_list()
self:form_lines()
-- keybind to launch menu
mp.add_key_binding(opts.toggle_menu_binding, "M-x-rofi", function()
self:handler()
end)
end
function mx:get_cmd_list()
local bindings = mp.get_property_native("input-bindings")
-- sets a flag 'shadowed' to all binding that have a binding with higher
-- priority using same key binding
for _, v in ipairs(bindings) do
for _, v1 in ipairs(bindings) do
if v.key == v1.key and v.priority < v1.priority then
v.shadowed = true
break
end
end
end
self.list = bindings
end
function mx:sort_cmd_list()
table.sort(self.list, function(i, j)
if opts.sort_commands_by == "priority" then
return tonumber(i.priority) > tonumber(j.priority)
end
-- sort by command name by default
return i.cmd < j.cmd
end)
end
function mx:form_lines()
for _, v in ipairs(self.list) do
table.insert(self.lines, self.get_line(v))
end
end
function mx.get_line(v)
local cmd = v.cmd
local function escape_pango(text)
local escapedText = text:gsub("[&<>]", {
["&"] = "&",
["<"] = "<",
[">"] = ">",
})
return escapedText
end
if #cmd > opts.strip_cmd_at then
cmd = string.sub(cmd, 1, opts.strip_cmd_at - 3) .. "..."
end
local a = cmd .. " (" .. escape_pango(v.key) .. ") "
-- handle inactive keybindings
if v.shadowed or v.priority == -1 then
local why_inactive = (v.priority == -1) and "inactive keybinding"
or "that binding is currently shadowed by another one"
a = '' .. a .. "(" .. why_inactive .. ")"
return a
end
if v.comment then
a = a .. '' .. v.comment .. ""
end
return a
end
function mx:merge_leader_bindings(le, leader_key)
-- REVIEW: sadly mpvs 'input-bindings' is read only and i can't force set
-- priority -1 for bindings that are overwritten by leader bindings.
-- Since leader script needs keybinding to be defined in 'input-bindings'.
-- Therefore i just merge those leader bindings in my own 'data.list'.
local bindings_to_append = {}
local function split_with_spaces(str)
local result_str = ""
for char in str:gmatch(".") do
result_str = result_str .. char .. " "
end
-- REVIEW: needed?
return result_str:gsub("(.-)%s*$", "%1") -- strip spaces
end
for _, lb in ipairs(le) do
-- overwriting binding in data.list
for y, b in ipairs(self.list) do
if b.cmd:find(lb.cmd, 1, true) then
self.list[y].priority = 13
self.list[y].key = leader_key .. " " .. split_with_spaces(lb.key)
-- if it's a script binding - initially it won't have comment field
-- but leader binding can (and should) have comment field, so we set it
-- and if it is normal keybinding and it had it's own comment field then
-- leave it as it was
self.list[y].comment = lb.comment or self.list[y].comment
goto continue1
end
-- if binding was not found - append it to list
if y == #self.list then
local binding = {}
binding.priority = 13
binding.key = leader_key .. " " .. split_with_spaces(lb.key)
binding.cmd = lb.cmd
-- if it's a script binding - initially it won't have comment field
-- but leader binding can (and should) have comment field, so we set it
-- and if it is normal keybinding and it had it's own comment field then
-- leave it as it was
binding.comment = lb.comment
table.insert(bindings_to_append, binding)
end
end
::continue1::
end
for _, v in ipairs(bindings_to_append) do
table.insert(self.list, v)
end
-- TODO: handle warning about not found leader kbd better
-- for i,v in ipairs(not_found_leader_kbds) do
-- print(v, 'not found')
-- end
end
function mx:handler()
local function update_bindings()
mx:get_cmd_list()
self:sort_cmd_list()
self:form_lines()
mp.commandv("script-message-to", "leader", "leader-bindings-request")
end
-- set flag 'was_paused' only if vid wasn't paused before EM init
if opts.pause_on_open and not mp.get_property_bool("pause", false) then
mp.set_property_bool("pause", true)
self.was_paused = true
end
-- NOTE: when using external tool to view list it is necessary to reopen it
-- when command list updates to see changes
mp.observe_property("input-bindings", "native", update_bindings)
self:register_script_message()
local command, status = self:get_rofi_choice()
if status == 0 and command then
mp.command(string.match(command, "(.-)<"))
end
self.unregister_script_message()
mp.unobserve_property(update_bindings)
if opts.resume_on_exit == true or (opts.resume_on_exit == "only-if-was-paused" and self.was_paused) then
mp.set_property_bool("pause", false)
end
collectgarbage()
end
function mx:get_rofi_choice()
local flags = {}
for flag in string.gmatch(opts.executable_flags, "%S+") do
table.insert(flags, flag)
end
local rofi = mp.command_native({
name = "subprocess",
args = { opts.executable, unpack(flags) },
capture_stdout = true,
playback_only = false,
stdin_data = table.concat(self.lines, "\n"),
})
return rofi.stdout, rofi.status
end
function mx:register_script_message()
mp.register_script_message("merge-leader-bindings", function(bindings, leader_key)
bindings = utils.parse_json(bindings)
self:merge_leader_bindings(bindings, leader_key)
self:sort_cmd_list()
end)
end
function mx.unregister_script_message()
mp.unregister_script_message("merge-leader-bindings")
end
mx:init()