309 lines
13 KiB
Lua
309 lines
13 KiB
Lua
------------------------------------------------------------------------------------------
|
|
----------------------------------Keybind Implementation----------------------------------
|
|
------------------------------------------------------------------------------------------
|
|
------------------------------------------------------------------------------------------
|
|
|
|
local mp = require 'mp'
|
|
local msg = require 'mp.msg'
|
|
local utils = require 'mp.utils'
|
|
|
|
local o = require 'modules.options'
|
|
local g = require 'modules.globals'
|
|
local fb_utils = require 'modules.utils'
|
|
local addons = require 'modules.addons'
|
|
local playlist = require 'modules.playlist'
|
|
local controls = require 'modules.controls'
|
|
local movement = require 'modules.navigation.directory-movement'
|
|
local scanning = require 'modules.navigation.scanning'
|
|
local cursor = require 'modules.navigation.cursor'
|
|
local cache = require 'modules.cache'
|
|
|
|
g.state.keybinds = {
|
|
{'ENTER', 'play', function() playlist.add_files('replace', false) end},
|
|
{'Shift+ENTER', 'play_append', function() playlist.add_files('append-play', false) end},
|
|
{'Alt+ENTER', 'play_autoload',function() playlist.add_files('replace', true) end},
|
|
{'ESC', 'close', controls.escape},
|
|
{'RIGHT', 'down_dir', movement.down_dir},
|
|
{'LEFT', 'up_dir', movement.up_dir},
|
|
{'DOWN', 'scroll_down', function() cursor.scroll(1, o.wrap) end, {repeatable = true}},
|
|
{'UP', 'scroll_up', function() cursor.scroll(-1, o.wrap) end, {repeatable = true}},
|
|
{'PGDWN', 'page_down', function() cursor.scroll(o.num_entries) end, {repeatable = true}},
|
|
{'PGUP', 'page_up', function() cursor.scroll(-o.num_entries) end, {repeatable = true}},
|
|
{'Shift+PGDWN', 'list_bottom', function() cursor.scroll(math.huge) end},
|
|
{'Shift+PGUP', 'list_top', function() cursor.scroll(-math.huge) end},
|
|
{'HOME', 'goto_current', movement.goto_current_dir},
|
|
{'Shift+HOME', 'goto_root', movement.goto_root},
|
|
{'Ctrl+r', 'reload', function() cache:clear(); scanning.rescan() end},
|
|
{'s', 'select_mode', cursor.toggle_select_mode},
|
|
{'S', 'select_item', cursor.toggle_selection},
|
|
{'Ctrl+a', 'select_all', cursor.select_all}
|
|
}
|
|
|
|
--a map of key-keybinds - only saves the latest keybind if multiple have the same key code
|
|
local top_level_keys = {}
|
|
|
|
--format the item string for either single or multiple items
|
|
local function create_item_string(base_code_fn, items, state, cmd, quoted)
|
|
if not items[1] then return end
|
|
local func = quoted and function(...) return ("%q"):format(base_code_fn(...)) end or base_code_fn
|
|
|
|
local out = {}
|
|
for _, item in ipairs(items) do
|
|
table.insert(out, func(item, state))
|
|
end
|
|
|
|
return table.concat(out, cmd['concat-string'] or ' ')
|
|
end
|
|
|
|
local KEYBIND_CODE_PATTERN = fb_utils.get_code_pattern(fb_utils.code_fns)
|
|
local item_specific_codes = 'fnij'
|
|
|
|
--substitutes the key codes for the
|
|
local function substitute_codes(str, cmd, items, state)
|
|
local overrides = {}
|
|
|
|
for code in item_specific_codes:gmatch('.') do
|
|
overrides[code] = function(_,s) return create_item_string(fb_utils.code_fns[code], items, s, cmd) end
|
|
overrides[code:upper()] = function(_,s) return create_item_string(fb_utils.code_fns[code], items, s, cmd, true) end
|
|
end
|
|
|
|
return fb_utils.substitute_codes(str, overrides, items[1], state)
|
|
end
|
|
|
|
--iterates through the command table and substitutes special
|
|
--character codes for the correct strings used for custom functions
|
|
local function format_command_table(cmd, items, state)
|
|
local copy = {}
|
|
for i = 1, #cmd.command do
|
|
copy[i] = {}
|
|
|
|
for j = 1, #cmd.command[i] do
|
|
copy[i][j] = substitute_codes(cmd.command[i][j], cmd, items, state)
|
|
end
|
|
end
|
|
return copy
|
|
end
|
|
|
|
--runs all of the commands in the command table
|
|
--key.command must be an array of command tables compatible with mp.command_native
|
|
--items must be an array of multiple items (when multi-type ~= concat the array will be 1 long)
|
|
local function run_custom_command(cmd, items, state)
|
|
local custom_cmds = cmd.codes and format_command_table(cmd, items, state) or cmd.command
|
|
|
|
for _, custom_cmd in ipairs(custom_cmds) do
|
|
msg.debug("running command:", utils.to_string(custom_cmd))
|
|
mp.command_native(custom_cmd)
|
|
end
|
|
end
|
|
|
|
--returns true if the given code set has item specific codes (%f, %i, etc)
|
|
local function has_item_codes(codes)
|
|
for code in pairs(codes) do
|
|
if item_specific_codes:find(code:lower(), 1, true) then return true end
|
|
end
|
|
return false
|
|
end
|
|
|
|
--runs one of the custom commands
|
|
local function run_custom_keybind(cmd, state, co)
|
|
--evaluates a condition and passes through the correct values
|
|
local function evaluate_condition(condition, items)
|
|
local cond = substitute_codes(condition, cmd, items, state)
|
|
return fb_utils.evaluate_string('return '..cond) == true
|
|
end
|
|
|
|
-- evaluates the string condition to decide if the keybind should be run
|
|
local do_item_condition
|
|
if cmd.condition then
|
|
if has_item_codes(cmd.condition_codes) then
|
|
do_item_condition = true
|
|
elseif not evaluate_condition(cmd.condition, {}) then
|
|
return false
|
|
end
|
|
end
|
|
|
|
if cmd.parser then
|
|
local parser_str = ' '..cmd.parser..' '
|
|
if not parser_str:find( '%W'..(state.parser.keybind_name or state.parser.name)..'%W' ) then return false end
|
|
end
|
|
|
|
--these are for the default keybinds, or from addons which use direct functions
|
|
if type(cmd.command) == 'function' then return cmd.command(cmd, cmd.addon and fb_utils.copy_table(state) or state, co) end
|
|
|
|
--the function terminates here if we are running the command on a single item
|
|
if not (cmd.multiselect and next(state.selection)) then
|
|
if cmd.filter then
|
|
if not state.list[state.selected] then return false end
|
|
if state.list[state.selected].type ~= cmd.filter then return false end
|
|
end
|
|
|
|
if cmd.codes then
|
|
--if the directory is empty, and this command needs to work on an item, then abort and fallback to the next command
|
|
if not state.list[state.selected] and has_item_codes(cmd.codes) then return false end
|
|
end
|
|
|
|
if do_item_condition and not evaluate_condition(cmd.condition, { state.list[state.selected] }) then
|
|
return false
|
|
end
|
|
run_custom_command(cmd, { state.list[state.selected] }, state)
|
|
return true
|
|
end
|
|
|
|
--runs the command on all multi-selected items
|
|
local selection = fb_utils.sort_keys(state.selection, function(item)
|
|
if do_item_condition and not evaluate_condition(cmd.condition, { item }) then return false end
|
|
return not cmd.filter or item.type == cmd.filter
|
|
end)
|
|
if not next(selection) then return false end
|
|
|
|
if cmd["multi-type"] == "concat" then
|
|
run_custom_command(cmd, selection, state)
|
|
|
|
elseif cmd["multi-type"] == "repeat" or cmd["multi-type"] == nil then
|
|
for i,_ in ipairs(selection) do
|
|
run_custom_command(cmd, {selection[i]}, state)
|
|
|
|
if cmd.delay then
|
|
mp.add_timeout(cmd.delay, function() fb_utils.coroutine.resume_err(co) end)
|
|
coroutine.yield()
|
|
end
|
|
end
|
|
end
|
|
|
|
--we passthrough by default if the command is not run on every selected item
|
|
if cmd.passthrough ~= nil then return end
|
|
|
|
local num_selection = 0
|
|
for _ in pairs(state.selection) do num_selection = num_selection+1 end
|
|
return #selection == num_selection
|
|
end
|
|
|
|
--recursively runs the keybind functions, passing down through the chain
|
|
--of keybinds with the same key value
|
|
local function run_keybind_recursive(keybind, state, co)
|
|
msg.trace("Attempting custom command:", utils.to_string(keybind))
|
|
|
|
if keybind.passthrough ~= nil then
|
|
run_custom_keybind(keybind, state, co)
|
|
if keybind.passthrough == true and keybind.prev_key then
|
|
run_keybind_recursive(keybind.prev_key, state, co)
|
|
end
|
|
else
|
|
if run_custom_keybind(keybind, state, co) == false and keybind.prev_key then
|
|
run_keybind_recursive(keybind.prev_key, state, co)
|
|
end
|
|
end
|
|
end
|
|
|
|
--a wrapper to run a custom keybind as a lua coroutine
|
|
local function run_keybind_coroutine(key)
|
|
msg.debug("Received custom keybind "..key.key)
|
|
local co = coroutine.create(run_keybind_recursive)
|
|
|
|
local state_copy = {
|
|
directory = g.state.directory,
|
|
directory_label = g.state.directory_label,
|
|
list = g.state.list, --the list should remain unchanged once it has been saved to the global state, new directories get new tables
|
|
selected = g.state.selected,
|
|
selection = fb_utils.copy_table(g.state.selection),
|
|
parser = g.state.parser,
|
|
}
|
|
local success, err = coroutine.resume(co, key, state_copy, co)
|
|
if not success then
|
|
msg.error("error running keybind:", utils.to_string(key))
|
|
fb_utils.traceback(err, co)
|
|
end
|
|
end
|
|
|
|
--scans the given command table to identify if they contain any custom keybind codes
|
|
local function scan_for_codes(command_table, codes)
|
|
if type(command_table) ~= "table" then return codes end
|
|
for _, value in pairs(command_table) do
|
|
local type = type(value)
|
|
if type == "table" then
|
|
scan_for_codes(value, codes)
|
|
elseif type == "string" then
|
|
for code in value:gmatch(KEYBIND_CODE_PATTERN) do
|
|
codes[code] = true
|
|
end
|
|
end
|
|
end
|
|
return codes
|
|
end
|
|
|
|
--inserting the custom keybind into the keybind array for declaration when file-browser is opened
|
|
--custom keybinds with matching names will overwrite eachother
|
|
local function insert_custom_keybind(keybind)
|
|
-- api checking for the keybinds is optional, so set to a valid version if it does not exist
|
|
keybind.api_version = keybind.api_version or '1.0.0'
|
|
if not addons.check_api_version(keybind, 'keybind '..keybind.name) then return end
|
|
|
|
--we'll always save the keybinds as either an array of command arrays or a function
|
|
if type(keybind.command) == "table" and type(keybind.command[1]) ~= "table" then
|
|
keybind.command = {keybind.command}
|
|
end
|
|
|
|
keybind.codes = scan_for_codes(keybind.command, {})
|
|
if not next(keybind.codes) then keybind.codes = nil end
|
|
keybind.prev_key = top_level_keys[keybind.key]
|
|
|
|
if keybind.condition then
|
|
keybind.condition_codes = {}
|
|
for code in string.gmatch(keybind.condition, KEYBIND_CODE_PATTERN) do keybind.condition_codes[code] = true end
|
|
end
|
|
|
|
table.insert(g.state.keybinds, {keybind.key, keybind.name, function() run_keybind_coroutine(keybind) end, keybind.flags or {}})
|
|
top_level_keys[keybind.key] = keybind
|
|
end
|
|
|
|
--loading the custom keybinds
|
|
--can either load keybinds from the config file, from addons, or from both
|
|
local function setup_keybinds()
|
|
if not o.custom_keybinds and not o.addons then return end
|
|
|
|
--this is to make the default keybinds compatible with passthrough from custom keybinds
|
|
for _, keybind in ipairs(g.state.keybinds) do
|
|
top_level_keys[keybind[1]] = { key = keybind[1], name = keybind[2], command = keybind[3], flags = keybind[4] }
|
|
end
|
|
|
|
--this loads keybinds from addons
|
|
if o.addons then
|
|
for i = #g.parsers, 1, -1 do
|
|
local parser = g.parsers[i]
|
|
if parser.keybinds then
|
|
for i, keybind in ipairs(parser.keybinds) do
|
|
--if addons use the native array command format, then we need to convert them over to the custom command format
|
|
if not keybind.key then keybind = { key = keybind[1], name = keybind[2], command = keybind[3], flags = keybind[4] }
|
|
else keybind = fb_utils.copy_table(keybind) end
|
|
|
|
keybind.name = g.parsers[parser].id.."/"..(keybind.name or tostring(i))
|
|
keybind.addon = true
|
|
insert_custom_keybind(keybind)
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
--loads custom keybinds from file-browser-keybinds.json
|
|
if o.custom_keybinds then
|
|
local path = mp.command_native({"expand-path", "~~/script-opts"}).."/file-browser-keybinds.json"
|
|
local custom_keybinds, err = io.open( path )
|
|
if not custom_keybinds then return error(err) end
|
|
|
|
local json = custom_keybinds:read("*a")
|
|
custom_keybinds:close()
|
|
|
|
json = utils.parse_json(json)
|
|
if not json then return error("invalid json syntax for "..path) end
|
|
|
|
for i, keybind in ipairs(json) do
|
|
keybind.name = "custom/"..(keybind.name or tostring(i))
|
|
insert_custom_keybind(keybind)
|
|
end
|
|
end
|
|
end
|
|
|
|
return {
|
|
setup_keybinds = setup_keybinds,
|
|
}
|