mirror of
https://github.com/ksyasuda/SubMiner.git
synced 2026-04-10 04:19:25 -07:00
feat: wire session bindings through main, ipc, and cli runtime
This commit is contained in:
@@ -14,7 +14,7 @@ function M.init()
|
||||
local utils = require("mp.utils")
|
||||
|
||||
local options_helper = require("options")
|
||||
local environment = require("environment").create({ mp = mp })
|
||||
local environment = require("environment").create({ mp = mp, utils = utils })
|
||||
local opts = options_helper.load(options_lib, environment.default_socket_path())
|
||||
local state = require("state").new()
|
||||
|
||||
@@ -61,6 +61,9 @@ function M.init()
|
||||
ctx.process = make_lazy_proxy("process", function()
|
||||
return require("process").create(ctx)
|
||||
end)
|
||||
ctx.session_bindings = make_lazy_proxy("session_bindings", function()
|
||||
return require("session_bindings").create(ctx)
|
||||
end)
|
||||
ctx.ui = make_lazy_proxy("ui", function()
|
||||
return require("ui").create(ctx)
|
||||
end)
|
||||
@@ -72,6 +75,7 @@ function M.init()
|
||||
end)
|
||||
|
||||
ctx.ui.register_keybindings()
|
||||
ctx.session_bindings.register_bindings()
|
||||
ctx.messages.register_script_messages()
|
||||
ctx.lifecycle.register_lifecycle_hooks()
|
||||
ctx.log.subminer_log("info", "lifecycle", "SubMiner plugin loaded")
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
local M = {}
|
||||
local unpack_fn = table.unpack or unpack
|
||||
|
||||
function M.create(ctx)
|
||||
local mp = ctx.mp
|
||||
local utils = ctx.utils
|
||||
|
||||
local detected_backend = nil
|
||||
local app_running_cache_value = nil
|
||||
@@ -30,6 +32,57 @@ function M.create(ctx)
|
||||
return "/tmp/subminer-socket"
|
||||
end
|
||||
|
||||
local function path_separator()
|
||||
return is_windows() and "\\" or "/"
|
||||
end
|
||||
|
||||
local function join_path(...)
|
||||
local parts = { ... }
|
||||
if utils and type(utils.join_path) == "function" then
|
||||
return utils.join_path(unpack_fn(parts))
|
||||
end
|
||||
return table.concat(parts, path_separator())
|
||||
end
|
||||
|
||||
local function file_exists(path)
|
||||
if not utils or type(utils.file_info) ~= "function" then
|
||||
return false
|
||||
end
|
||||
return utils.file_info(path) ~= nil
|
||||
end
|
||||
|
||||
local function resolve_subminer_config_dir()
|
||||
local home = os.getenv("HOME") or os.getenv("USERPROFILE") or ""
|
||||
local candidates = {}
|
||||
if is_windows() then
|
||||
local app_data = os.getenv("APPDATA") or join_path(home, "AppData", "Roaming")
|
||||
candidates = {
|
||||
join_path(app_data, "SubMiner"),
|
||||
}
|
||||
else
|
||||
local xdg_config_home = os.getenv("XDG_CONFIG_HOME")
|
||||
local primary_base = (type(xdg_config_home) == "string" and xdg_config_home ~= "")
|
||||
and xdg_config_home
|
||||
or join_path(home, ".config")
|
||||
candidates = {
|
||||
join_path(primary_base, "SubMiner"),
|
||||
join_path(home, ".config", "SubMiner"),
|
||||
}
|
||||
end
|
||||
|
||||
for _, dir in ipairs(candidates) do
|
||||
if file_exists(join_path(dir, "config.jsonc")) or file_exists(join_path(dir, "config.json")) or file_exists(dir) then
|
||||
return dir
|
||||
end
|
||||
end
|
||||
|
||||
return candidates[1]
|
||||
end
|
||||
|
||||
local function resolve_session_bindings_artifact_path()
|
||||
return join_path(resolve_subminer_config_dir(), "session-bindings.json")
|
||||
end
|
||||
|
||||
local function is_linux()
|
||||
return not is_windows() and not is_macos()
|
||||
end
|
||||
@@ -198,7 +251,10 @@ function M.create(ctx)
|
||||
is_windows = is_windows,
|
||||
is_macos = is_macos,
|
||||
is_linux = is_linux,
|
||||
join_path = join_path,
|
||||
default_socket_path = default_socket_path,
|
||||
resolve_subminer_config_dir = resolve_subminer_config_dir,
|
||||
resolve_session_bindings_artifact_path = resolve_session_bindings_artifact_path,
|
||||
is_subminer_process_running = is_subminer_process_running,
|
||||
is_subminer_app_running = is_subminer_app_running,
|
||||
is_subminer_app_running_async = is_subminer_app_running_async,
|
||||
|
||||
@@ -47,6 +47,9 @@ function M.create(ctx)
|
||||
mp.register_script_message("subminer-stats-toggle", function()
|
||||
mp.osd_message("Stats: press ` (backtick) in overlay", 3)
|
||||
end)
|
||||
mp.register_script_message("subminer-reload-session-bindings", function()
|
||||
ctx.session_bindings.reload_bindings()
|
||||
end)
|
||||
end
|
||||
|
||||
return {
|
||||
|
||||
310
plugin/subminer/session_bindings.lua
Normal file
310
plugin/subminer/session_bindings.lua
Normal file
@@ -0,0 +1,310 @@
|
||||
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",
|
||||
}
|
||||
|
||||
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 key_spec_to_mpv_binding(key)
|
||||
if type(key) ~= "table" then
|
||||
return nil
|
||||
end
|
||||
|
||||
local key_name = key_code_to_mpv_name(key.code)
|
||||
if not key_name then
|
||||
return nil
|
||||
end
|
||||
|
||||
local parts = {}
|
||||
for _, modifier in ipairs(key.modifiers or {}) do
|
||||
local mapped = MODIFIER_MAP[modifier]
|
||||
if mapped then
|
||||
parts[#parts + 1] = mapped
|
||||
end
|
||||
end
|
||||
parts[#parts + 1] = key_name
|
||||
return table.concat(parts, "+")
|
||||
end
|
||||
|
||||
local function build_cli_args(action_id, payload)
|
||||
if action_id == "toggleVisibleOverlay" then
|
||||
return { "--toggle-visible-overlay" }
|
||||
elseif action_id == "copySubtitle" then
|
||||
return { "--copy-subtitle" }
|
||||
elseif action_id == "copySubtitleMultiple" then
|
||||
return { "--copy-subtitle-count", tostring(payload and payload.count or 1) }
|
||||
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
|
||||
return { "--mine-sentence-count", tostring(payload and payload.count or 1) }
|
||||
elseif action_id == "toggleSecondarySub" then
|
||||
return { "--toggle-secondary-sub" }
|
||||
elseif action_id == "markAudioCard" then
|
||||
return { "--mark-audio-card" }
|
||||
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 == "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" }
|
||||
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
|
||||
process.run_binary_command_async(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 clear_numeric_selection(show_cancelled)
|
||||
if state.session_numeric_selection and state.session_numeric_selection.timeout then
|
||||
state.session_numeric_selection.timeout:kill()
|
||||
end
|
||||
state.session_numeric_selection = nil
|
||||
remove_binding_names(state.session_numeric_binding_names)
|
||||
if show_cancelled then
|
||||
show_osd("Cancelled")
|
||||
end
|
||||
end
|
||||
|
||||
local function start_numeric_selection(action_id, timeout_ms)
|
||||
clear_numeric_selection(false)
|
||||
for digit = 1, 9 do
|
||||
local digit_string = tostring(digit)
|
||||
local name = "subminer-session-digit-" .. digit_string
|
||||
state.session_numeric_binding_names[#state.session_numeric_binding_names + 1] = name
|
||||
mp.add_forced_key_binding(digit_string, name, function()
|
||||
clear_numeric_selection(false)
|
||||
invoke_cli_action(action_id, { count = digit })
|
||||
end)
|
||||
end
|
||||
|
||||
state.session_numeric_binding_names[#state.session_numeric_binding_names + 1] =
|
||||
"subminer-session-digit-cancel"
|
||||
mp.add_forced_key_binding("ESC", "subminer-session-digit-cancel", function()
|
||||
clear_numeric_selection(true)
|
||||
end)
|
||||
|
||||
state.session_numeric_selection = {
|
||||
action_id = action_id,
|
||||
timeout = mp.add_timeout((timeout_ms or 3000) / 1000, function()
|
||||
clear_numeric_selection(false)
|
||||
show_osd(action_id == "copySubtitleMultiple" and "Copy timeout" or "Mine timeout")
|
||||
end),
|
||||
}
|
||||
|
||||
show_osd(
|
||||
action_id == "copySubtitleMultiple"
|
||||
and "Copy how many lines? Press 1-9 (Esc to cancel)"
|
||||
or "Mine how many lines? Press 1-9 (Esc to cancel)"
|
||||
)
|
||||
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, numeric_selection_timeout_ms)
|
||||
if binding.actionType == "mpv-command" then
|
||||
execute_mpv_command(binding.command)
|
||||
return
|
||||
end
|
||||
|
||||
if binding.actionId == "copySubtitleMultiple" or binding.actionId == "mineSentenceMultiple" then
|
||||
start_numeric_selection(binding.actionId, numeric_selection_timeout_ms)
|
||||
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()
|
||||
clear_numeric_selection(false)
|
||||
remove_binding_names(state.session_binding_names)
|
||||
end
|
||||
|
||||
local function register_bindings()
|
||||
clear_bindings()
|
||||
|
||||
local artifact, load_error = load_artifact()
|
||||
if not artifact then
|
||||
subminer_log("warn", "session-bindings", load_error)
|
||||
return false
|
||||
end
|
||||
|
||||
local timeout_ms = tonumber(artifact.numericSelectionTimeoutMs) or 3000
|
||||
for index, binding in ipairs(artifact.bindings) do
|
||||
local key_name = key_spec_to_mpv_binding(binding.key)
|
||||
if key_name then
|
||||
local name = "subminer-session-binding-" .. tostring(index)
|
||||
state.session_binding_names[#state.session_binding_names + 1] = name
|
||||
mp.add_forced_key_binding(key_name, name, function()
|
||||
handle_binding(binding, timeout_ms)
|
||||
end)
|
||||
else
|
||||
subminer_log(
|
||||
"warn",
|
||||
"session-bindings",
|
||||
"Skipped unsupported key code from artifact: " .. tostring(binding.key and binding.key.code or "unknown")
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
subminer_log(
|
||||
"info",
|
||||
"session-bindings",
|
||||
"Registered " .. tostring(#state.session_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
|
||||
@@ -33,6 +33,9 @@ function M.new()
|
||||
auto_play_ready_timeout = nil,
|
||||
auto_play_ready_osd_timer = nil,
|
||||
suppress_ready_overlay_restore = false,
|
||||
session_binding_names = {},
|
||||
session_numeric_binding_names = {},
|
||||
session_numeric_selection = nil,
|
||||
}
|
||||
end
|
||||
|
||||
|
||||
Reference in New Issue
Block a user