Files
SubMiner/plugin/subminer/session_bindings.lua
T

399 lines
10 KiB
Lua

local M = {}
local unpack_fn = table.unpack or unpack
local KEY_NAME_MAP = {
Space = "SPACE",
Tab = "TAB",
Enter = "ENTER",
Escape = "ESC",
Backspace = "BS",
Delete = "DEL",
ArrowUp = "UP",
ArrowDown = "DOWN",
ArrowLeft = "LEFT",
ArrowRight = "RIGHT",
Slash = "/",
Backslash = "\\",
Minus = "-",
Equal = "=",
Comma = ",",
Period = ".",
Quote = "'",
Semicolon = ";",
BracketLeft = "[",
BracketRight = "]",
Backquote = "`",
}
local MODIFIER_MAP = {
ctrl = "Ctrl",
alt = "Alt",
shift = "Shift",
meta = "Meta",
}
local SHIFTED_KEY_NAME_MAP = {
Digit1 = "!",
Digit2 = "@",
Digit3 = "SHARP",
Digit4 = "$",
Digit5 = "%",
Digit6 = "^",
Digit7 = "&",
Digit8 = "*",
Digit9 = "(",
Digit0 = ")",
Minus = "_",
Equal = "+",
BracketLeft = "{",
BracketRight = "}",
Backslash = "|",
Semicolon = ":",
Quote = '"',
Comma = "<",
Period = ">",
Slash = "?",
Backquote = "~",
}
function M.create(ctx)
local mp = ctx.mp
local utils = ctx.utils
local state = ctx.state
local process = ctx.process
local environment = ctx.environment
local subminer_log = ctx.log.subminer_log
local show_osd = ctx.log.show_osd
local function read_file(path)
local handle = io.open(path, "r")
if not handle then
return nil
end
local content = handle:read("*a")
handle:close()
return content
end
local function remove_binding_names(names)
for _, name in ipairs(names) do
mp.remove_key_binding(name)
end
for index = #names, 1, -1 do
names[index] = nil
end
end
local function key_code_to_mpv_name(code)
if KEY_NAME_MAP[code] then
return KEY_NAME_MAP[code]
end
local letter = code:match("^Key([A-Z])$")
if letter then
return string.lower(letter)
end
local digit = code:match("^Digit([0-9])$")
if digit then
return digit
end
local function_key = code:match("^(F%d+)$")
if function_key then
return function_key
end
return nil
end
local function contains_value(values, target)
for _, value in ipairs(values) do
if value == target then
return true
end
end
return false
end
local function append_unique(values, value)
if not contains_value(values, value) then
values[#values + 1] = value
end
end
local function key_spec_to_mpv_bindings(key)
if type(key) ~= "table" then
return nil
end
if type(key.code) ~= "string" then
return nil
end
if type(key.modifiers) ~= "table" then
return nil
end
local shifted_letter = key.code:match("^Key([A-Z])$")
local has_shift = false
for _, modifier in ipairs(key.modifiers) do
if modifier == "shift" then
has_shift = true
break
end
end
local key_name = key_code_to_mpv_name(key.code)
if shifted_letter and has_shift then
key_name = shifted_letter
end
if not key_name then
return nil
end
local parts = {}
for _, modifier in ipairs(key.modifiers) do
if not (modifier == "shift" and shifted_letter) then
local mapped = MODIFIER_MAP[modifier]
if mapped then
parts[#parts + 1] = mapped
end
end
end
parts[#parts + 1] = key_name
local bindings = { table.concat(parts, "+") }
local shifted_key_name = SHIFTED_KEY_NAME_MAP[key.code]
if has_shift and shifted_key_name then
local shifted_parts = {}
for _, modifier in ipairs(key.modifiers) do
if modifier ~= "shift" then
local mapped = MODIFIER_MAP[modifier]
if mapped then
shifted_parts[#shifted_parts + 1] = mapped
end
end
end
shifted_parts[#shifted_parts + 1] = shifted_key_name
append_unique(bindings, table.concat(shifted_parts, "+"))
end
return bindings
end
local function build_cli_args(action_id, payload)
if action_id == "toggleVisibleOverlay" then
return { "--toggle-visible-overlay" }
elseif action_id == "toggleStatsOverlay" then
return { "--toggle-stats-overlay" }
elseif action_id == "copySubtitle" then
return { "--copy-subtitle" }
elseif action_id == "copySubtitleMultiple" then
if payload and payload.count then
return { "--copy-subtitle-count", tostring(payload.count) }
end
return { "--copy-subtitle-multiple" }
elseif action_id == "updateLastCardFromClipboard" then
return { "--update-last-card-from-clipboard" }
elseif action_id == "triggerFieldGrouping" then
return { "--trigger-field-grouping" }
elseif action_id == "triggerSubsync" then
return { "--trigger-subsync" }
elseif action_id == "mineSentence" then
return { "--mine-sentence" }
elseif action_id == "mineSentenceMultiple" then
if payload and payload.count then
return { "--mine-sentence-count", tostring(payload.count) }
end
return { "--mine-sentence-multiple" }
elseif action_id == "toggleSecondarySub" then
return { "--toggle-secondary-sub" }
elseif action_id == "toggleSubtitleSidebar" then
return { "--toggle-subtitle-sidebar" }
elseif action_id == "markAudioCard" then
return { "--mark-audio-card" }
elseif action_id == "markWatched" then
return { "--mark-watched" }
elseif action_id == "openRuntimeOptions" then
return { "--open-runtime-options" }
elseif action_id == "openJimaku" then
return { "--open-jimaku" }
elseif action_id == "openYoutubePicker" then
return { "--open-youtube-picker" }
elseif action_id == "openSessionHelp" then
return { "--open-session-help" }
elseif action_id == "openCharacterDictionary" then
return { "--open-character-dictionary" }
elseif action_id == "openControllerSelect" then
return { "--open-controller-select" }
elseif action_id == "openControllerDebug" then
return { "--open-controller-debug" }
elseif action_id == "openPlaylistBrowser" then
return { "--open-playlist-browser" }
elseif action_id == "replayCurrentSubtitle" then
return { "--replay-current-subtitle" }
elseif action_id == "playNextSubtitle" then
return { "--play-next-subtitle" }
elseif action_id == "shiftSubDelayPrevLine" then
return { "--shift-sub-delay-prev-line" }
elseif action_id == "shiftSubDelayNextLine" then
return { "--shift-sub-delay-next-line" }
elseif action_id == "cycleRuntimeOption" then
local runtime_option_id = payload and payload.runtimeOptionId or nil
if type(runtime_option_id) ~= "string" or runtime_option_id == "" then
return nil
end
local direction = payload and payload.direction == -1 and "prev" or "next"
return { "--cycle-runtime-option", runtime_option_id .. ":" .. direction }
end
return nil
end
local function invoke_cli_action(action_id, payload)
if not process.check_binary_available() then
show_osd("Error: binary not found")
return
end
local cli_args = build_cli_args(action_id, payload)
if not cli_args then
subminer_log("warn", "session-bindings", "No CLI mapping for action: " .. tostring(action_id))
return
end
local args = { state.binary_path }
for _, arg in ipairs(cli_args) do
args[#args + 1] = arg
end
local runner = process.run_binary_command_async
if type(runner) ~= "function" then
runner = function(binary_args, callback)
mp.command_native_async({
name = "subprocess",
args = binary_args,
playback_only = false,
capture_stdout = true,
capture_stderr = true,
}, function(success, result, error)
local ok = success and (result == nil or result.status == 0)
if callback then
callback(ok, result, error)
end
end)
end
end
runner(args, function(ok, result, error)
if ok then
return
end
local reason = error or (result and result.stderr) or "unknown error"
subminer_log("warn", "session-bindings", "Session action failed: " .. tostring(reason))
show_osd("Session action failed")
end)
end
local function execute_mpv_command(command)
if type(command) ~= "table" or command[1] == nil then
return
end
mp.commandv(unpack_fn(command))
end
local function handle_binding(binding)
if binding.actionType == "mpv-command" then
execute_mpv_command(binding.command)
return
end
if binding.actionId == "toggleVisibleOverlay" and type(process.toggle_overlay) == "function" then
process.toggle_overlay()
return
end
invoke_cli_action(binding.actionId, binding.payload)
end
local function load_artifact()
local artifact_path = environment.resolve_session_bindings_artifact_path()
local raw = read_file(artifact_path)
if not raw or raw == "" then
return nil, "Missing session binding artifact: " .. tostring(artifact_path)
end
local parsed, parse_error = utils.parse_json(raw)
if not parsed then
return nil, "Failed to parse session binding artifact: " .. tostring(parse_error)
end
if type(parsed) ~= "table" or type(parsed.bindings) ~= "table" then
return nil, "Invalid session binding artifact"
end
return parsed, nil
end
local function clear_bindings()
remove_binding_names(state.session_binding_names)
end
local function register_bindings()
local artifact, load_error = load_artifact()
if not artifact then
subminer_log("warn", "session-bindings", load_error)
return false
end
local previous_binding_names = state.session_binding_names
local next_binding_names = {}
state.session_binding_generation = (state.session_binding_generation or 0) + 1
local generation = state.session_binding_generation
for index, binding in ipairs(artifact.bindings) do
local key_names = key_spec_to_mpv_bindings(binding.key)
if key_names then
for key_index, key_name in ipairs(key_names) do
local name = "subminer-session-binding-"
.. tostring(generation)
.. "-"
.. tostring(index)
.. "-"
.. tostring(key_index)
next_binding_names[#next_binding_names + 1] = name
mp.add_forced_key_binding(key_name, name, function()
handle_binding(binding)
end)
end
else
subminer_log(
"warn",
"session-bindings",
"Skipped unsupported key code from artifact: " .. tostring(binding.key and binding.key.code or "unknown")
)
end
end
remove_binding_names(previous_binding_names)
state.session_binding_names = next_binding_names
subminer_log(
"info",
"session-bindings",
"Registered " .. tostring(#next_binding_names) .. " shared session bindings"
)
return true
end
local function reload_bindings()
return register_bindings()
end
return {
register_bindings = register_bindings,
reload_bindings = reload_bindings,
clear_bindings = clear_bindings,
}
end
return M