mirror of
https://github.com/mpv-player/mpv.git
synced 2026-05-07 20:02:49 +00:00
612 lines
20 KiB
Lua
612 lines
20 KiB
Lua
--[[
|
|
This file is part of mpv.
|
|
|
|
mpv is free software; you can redistribute it and/or
|
|
modify it under the terms of the GNU Lesser General Public
|
|
License as published by the Free Software Foundation; either
|
|
version 2.1 of the License, or (at your option) any later version.
|
|
|
|
mpv is distributed in the hope that it will be useful,
|
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
GNU Lesser General Public License for more details.
|
|
|
|
You should have received a copy of the GNU Lesser General Public
|
|
License along with mpv. If not, see <http://www.gnu.org/licenses/>.
|
|
]]
|
|
|
|
local options = {
|
|
persist_history = false,
|
|
history_path = "~~state/command_history.txt",
|
|
remember_input = true,
|
|
}
|
|
|
|
local input = require "mp.input"
|
|
local utils = require "mp.utils"
|
|
|
|
local styles = {
|
|
-- Colors are stolen from base16 Eighties by Chris Kempson
|
|
-- and converted to BGR as is required by ASS.
|
|
-- 2d2d2d 393939 515151 697374
|
|
-- 939fa0 c8d0d3 dfe6e8 ecf0f2
|
|
-- 7a77f2 5791f9 66ccff 99cc99
|
|
-- cccc66 cc9966 cc99cc 537bd2
|
|
|
|
debug = "{\\1c&Ha09f93&}",
|
|
v = "{\\1c&H99cc99&}",
|
|
warn = "{\\1c&H66ccff&}",
|
|
error = "{\\1c&H7a77f2&}",
|
|
fatal = "{\\1c&H5791f9&}",
|
|
completion = "{\\1c&Hcc99cc&}",
|
|
}
|
|
for key, style in pairs(styles) do
|
|
styles[key] = style .. "{\\3c&H111111&}"
|
|
end
|
|
|
|
local terminal_styles = {
|
|
debug = "\027[90m",
|
|
v = "\027[32m",
|
|
warn = "\027[33m",
|
|
error = "\027[31m",
|
|
fatal = "\027[91m",
|
|
}
|
|
|
|
local platform = mp.get_property("platform")
|
|
local path_separator = platform == "windows" and "\\" or "/"
|
|
local completion_pos
|
|
local completion_append
|
|
local last_text
|
|
local last_cursor_position
|
|
|
|
local cache = {}
|
|
|
|
|
|
local function get_commands()
|
|
if cache["commands"] then
|
|
return cache["commands"]
|
|
end
|
|
|
|
cache["commands"] = mp.get_property_native("command-list")
|
|
table.sort(cache["commands"], function(c1, c2)
|
|
return c1.name < c2.name
|
|
end)
|
|
|
|
return cache["commands"]
|
|
end
|
|
|
|
local function help_command(param)
|
|
local output = ""
|
|
if param == "" then
|
|
output = "Available commands:\n"
|
|
for _, cmd in ipairs(get_commands()) do
|
|
output = output .. " " .. cmd.name
|
|
end
|
|
output = output .. "\n"
|
|
output = output .. 'Use "help command" to show information about a command.\n'
|
|
output = output .. "ESC or Ctrl+d exits the console.\n"
|
|
else
|
|
local cmd = nil
|
|
for _, curcmd in ipairs(get_commands()) do
|
|
if curcmd.name:find(param, 1, true) then
|
|
cmd = curcmd
|
|
if curcmd.name == param then
|
|
break -- exact match
|
|
end
|
|
end
|
|
end
|
|
|
|
if not cmd then
|
|
input.log('No command matches "' .. param .. '"!', styles.error,
|
|
terminal_styles.error)
|
|
return
|
|
end
|
|
|
|
output = output .. 'Command "' .. cmd.name .. '"\n'
|
|
|
|
for _, arg in ipairs(cmd.args) do
|
|
output = output .. " " .. arg.name .. " (" .. arg.type .. ")"
|
|
if arg.optional then
|
|
output = output .. " (optional)"
|
|
end
|
|
output = output .. "\n"
|
|
end
|
|
|
|
if cmd.vararg then
|
|
output = output .. "This command supports variable arguments.\n"
|
|
end
|
|
end
|
|
|
|
input.log(output:sub(1, -2))
|
|
end
|
|
|
|
local function submit(line)
|
|
-- match "help [<text>]", return <text> or "", strip all whitespace
|
|
local help = line:match("^%s*help%s+(.-)%s*$") or
|
|
(line:match("^%s*help$") and "")
|
|
|
|
if help then
|
|
help_command(help)
|
|
elseif line ~= "" then
|
|
mp.command(line)
|
|
end
|
|
end
|
|
|
|
local function opened()
|
|
mp.enable_messages("terminal-default")
|
|
end
|
|
|
|
local function closed(text, cursor_position)
|
|
mp.enable_messages("silent:terminal-default")
|
|
|
|
if options.remember_input then
|
|
last_text = text
|
|
last_cursor_position = cursor_position
|
|
end
|
|
end
|
|
|
|
local function command_list()
|
|
local cmds = {}
|
|
for i, command in ipairs(get_commands()) do
|
|
cmds[i] = command.name
|
|
end
|
|
|
|
return cmds
|
|
end
|
|
|
|
local function property_list()
|
|
if cache["property-list"] then
|
|
return cache["property-list"]
|
|
end
|
|
|
|
cache["property-list"] = mp.get_property_native("property-list")
|
|
|
|
for _, sub_property in pairs({"video", "audio", "sub", "sub2"}) do
|
|
table.insert(cache["property-list"], "current-tracks/" .. sub_property)
|
|
end
|
|
|
|
for _, sub_property in pairs({"text", "text-primary"}) do
|
|
table.insert(cache["property-list"], "clipboard/" .. sub_property)
|
|
end
|
|
|
|
return cache["property-list"]
|
|
end
|
|
|
|
local function profile_list()
|
|
local profiles = {}
|
|
|
|
for i, profile in ipairs(mp.get_property_native("profile-list")) do
|
|
profiles[i] = profile.name
|
|
end
|
|
|
|
return profiles
|
|
end
|
|
|
|
local function option_info(option, prop, default)
|
|
local key = "option-info/" .. option
|
|
cache[key] = cache[key] or mp.get_property_native(key, false)
|
|
|
|
if not cache[key] then
|
|
return default
|
|
end
|
|
|
|
if prop then
|
|
if cache[key][prop] ~= nil then
|
|
return cache[key][prop]
|
|
end
|
|
return default
|
|
end
|
|
|
|
return cache[key]
|
|
end
|
|
|
|
local function list_option_list()
|
|
if cache["option-list"] then
|
|
return cache["option-list"]
|
|
end
|
|
cache["option-list"] = {}
|
|
|
|
-- Don't log errors for renamed and removed properties.
|
|
-- (Just mp.enable_messages("fatal") still logs them to the terminal.)
|
|
local msg_level_backup = mp.get_property("msg-level")
|
|
mp.set_property("msg-level", msg_level_backup == "" and "cplayer=no"
|
|
or msg_level_backup .. ",cplayer=no")
|
|
|
|
for _, option in pairs(mp.get_property_native("options")) do
|
|
if option_info(option, "type", ""):find(" list$") then
|
|
table.insert(cache["option-list"], option)
|
|
end
|
|
end
|
|
|
|
mp.set_property("msg-level", msg_level_backup)
|
|
|
|
return cache["option-list"]
|
|
end
|
|
|
|
local function list_option_action_list(option)
|
|
local type = option_info(option, "type")
|
|
|
|
if type == "Key/value list" then
|
|
return {"add", "append", "set", "remove"}
|
|
end
|
|
|
|
if type == "String list" or type == "Object settings list" then
|
|
return {"add", "append", "clr", "pre", "set", "remove", "toggle"}
|
|
end
|
|
end
|
|
|
|
local function list_option_value_list(option)
|
|
local values = mp.get_property_native(option)
|
|
|
|
if type(values) ~= "table" then
|
|
return
|
|
end
|
|
|
|
if type(values[1]) ~= "table" then
|
|
return values
|
|
end
|
|
|
|
for i, value in ipairs(values) do
|
|
values[i] = value.label and "@" .. value.label or value.name
|
|
end
|
|
|
|
return values
|
|
end
|
|
|
|
local function has_file_argument(candidate_command)
|
|
local key = "command-file/" .. candidate_command
|
|
if cache[key] ~= nil then
|
|
return cache[key]
|
|
end
|
|
|
|
for _, command in pairs(get_commands()) do
|
|
if command.name == candidate_command then
|
|
cache[key] = command.args[1] and
|
|
(command.args[1].name == "filename" or command.args[1].name == "url")
|
|
return cache[key]
|
|
end
|
|
end
|
|
end
|
|
|
|
local function file_list(directory)
|
|
if directory == "" then
|
|
directory = "."
|
|
else
|
|
directory = mp.command_native({"expand-path", directory})
|
|
end
|
|
|
|
local files = utils.readdir(directory, "files") or {}
|
|
|
|
for _, dir in pairs(utils.readdir(directory, "dirs") or {}) do
|
|
files[#files + 1] = dir .. path_separator
|
|
end
|
|
|
|
return files
|
|
end
|
|
|
|
local function handle_file_completion(before_cur)
|
|
local directory, last_component_pos =
|
|
before_cur:sub(completion_pos):match("(.-)()[^" .. path_separator .."]*$")
|
|
|
|
completion_pos = completion_pos + last_component_pos - 1
|
|
|
|
-- Don"t use completion_append for file completion to not add quotes after
|
|
-- directories whose entries you may want to complete afterwards.
|
|
completion_append = ""
|
|
|
|
return file_list(directory)
|
|
end
|
|
|
|
local function handle_choice_completion(option, before_cur)
|
|
local info = option_info(option, nil, {})
|
|
|
|
if info.type == "Flag" then
|
|
return { "no", "yes" }, before_cur
|
|
end
|
|
|
|
if info["expects-file"] then
|
|
return handle_file_completion(before_cur)
|
|
end
|
|
|
|
-- Fix completing the empty value for --dscale and --cscale.
|
|
if info.choices and info.choices[1] == "" and completion_append == "" then
|
|
info.choices[1] = '""'
|
|
end
|
|
|
|
return info.choices
|
|
end
|
|
|
|
local function command_flags_at_1st_argument_list(command)
|
|
local flags = {
|
|
["playlist-next"] = {"weak", "force"},
|
|
["playlist-play-index"] = {"current", "none"},
|
|
["playlist-remove"] = {"current"},
|
|
["rescan-external-files"] = {"reselect", "keep-selection"},
|
|
["revert-seek"] = {"mark", "mark-permanent"},
|
|
["screenshot"] = {"subtitles", "video", "window", "osd", "scaled", "each-frame"},
|
|
["stop"] = {"keep-playlist"},
|
|
}
|
|
flags["playlist-prev"] = flags["playlist-next"]
|
|
flags["screenshot-raw"] = flags.screenshot
|
|
|
|
return flags[command]
|
|
end
|
|
|
|
local function command_flags_at_2nd_argument_list(command)
|
|
local flags = {
|
|
["apply-profile"] = {"apply", "restore"},
|
|
["frame-step"] = {"play", "seek", "mute"},
|
|
["loadfile"] = {"replace", "append", "append-play", "insert-next",
|
|
"insert-next-play", "insert-at", "insert-at-play"},
|
|
["screenshot-to-file"] = {"subtitles", "video", "window", "osd", "scaled", "each-frame"},
|
|
["screenshot-raw"] = {"bgr0", "bgra", "rgba", "rgba64"},
|
|
["seek"] = {"relative", "absolute", "absolute-percent",
|
|
"relative-percent", "keyframes", "exact"},
|
|
["sub-add"] = {"select", "auto", "cached"},
|
|
["sub-seek"] = {"primary", "secondary"},
|
|
}
|
|
flags.loadlist = flags.loadfile
|
|
flags["audio-add"] = flags["sub-add"]
|
|
flags["video-add"] = flags["sub-add"]
|
|
flags["sub-step"] = flags["sub-seek"]
|
|
|
|
return flags[command]
|
|
end
|
|
|
|
local function handle_flags(command, arg_index, flags)
|
|
for _, cmd in pairs(get_commands()) do
|
|
if cmd.name == command then
|
|
if cmd.args[arg_index] and cmd.args[arg_index].type == "Flags" then
|
|
break
|
|
else
|
|
return
|
|
end
|
|
end
|
|
end
|
|
|
|
local plus_pos = flags:find("%+[^%+]*$")
|
|
|
|
if plus_pos then
|
|
completion_pos = completion_pos + plus_pos
|
|
end
|
|
end
|
|
|
|
local function executable_list()
|
|
local executable_map = {}
|
|
local path = os.getenv("PATH") or ""
|
|
local separator = platform == "windows" and ";" or ":"
|
|
local exts = {}
|
|
|
|
for ext in (os.getenv("PATHEXT") or ""):gmatch("[^;]+") do
|
|
exts[ext:lower()] = true
|
|
end
|
|
|
|
for directory in path:gmatch("[^" .. separator .. "]+") do
|
|
for _, executable in pairs(utils.readdir(directory, "files") or {}) do
|
|
if not next(exts) or exts[(executable:match("%.%w+$") or ""):lower()] then
|
|
executable_map[executable] = true
|
|
end
|
|
end
|
|
end
|
|
|
|
local executables = {}
|
|
for executable, _ in pairs(executable_map) do
|
|
executables[#executables + 1] = executable
|
|
end
|
|
|
|
return executables
|
|
end
|
|
|
|
local function filter_label_list(type)
|
|
local values = {"all"}
|
|
|
|
for _, value in pairs(mp.get_property_native(type)) do
|
|
if value.label then
|
|
values[#values + 1] = value.label
|
|
end
|
|
end
|
|
|
|
return values
|
|
end
|
|
|
|
local function complete(before_cur)
|
|
local tokens = {}
|
|
local first_useful_token_index = 1
|
|
local completions
|
|
|
|
local begin_new_token = true
|
|
local last_quote
|
|
for pos, char in before_cur:gmatch("()(.)") do
|
|
if char:find("[%s;]") and not last_quote then
|
|
begin_new_token = true
|
|
if char == ";" then
|
|
first_useful_token_index = #tokens + 1
|
|
end
|
|
elseif begin_new_token then
|
|
tokens[#tokens + 1] = { text = char, pos = pos }
|
|
last_quote = char:match("['\"]")
|
|
begin_new_token = false
|
|
else
|
|
tokens[#tokens].text = tokens[#tokens].text .. char
|
|
if char == last_quote then
|
|
last_quote = nil
|
|
end
|
|
end
|
|
end
|
|
|
|
completion_append = last_quote or ""
|
|
|
|
-- Strip quotes from tokens.
|
|
for _, token in pairs(tokens) do
|
|
if token.text:find('^"') then
|
|
token.text = token.text:sub(2):gsub('"$', "")
|
|
token.pos = token.pos + 1
|
|
elseif token.text:find("^'") then
|
|
token.text = token.text:sub(2):gsub('"$', "")
|
|
token.pos = token.pos + 1
|
|
end
|
|
end
|
|
|
|
-- Skip command prefixes because it is not worth lumping them together with
|
|
-- command completions when they are useless for interactive usage.
|
|
local command_prefixes = {
|
|
["osd-auto"] = true, ["no-osd"] = true, ["osd-bar"] = true,
|
|
["osd-msg"] = true, ["osd-msg-bar"] = true, ["raw"] = true,
|
|
["expand-properties"] = true, ["repeatable"] = true,
|
|
["nonrepeatable"] = true, ["nonscalable"] = true,
|
|
["async"] = true, ["sync"] = true
|
|
}
|
|
|
|
-- Add an empty token if the cursor is after whitespace or ; to simplify
|
|
-- comparisons.
|
|
if before_cur == "" or before_cur:find("[%s;]$") then
|
|
tokens[#tokens + 1] = { text = "", pos = #before_cur + 1 }
|
|
end
|
|
|
|
while tokens[first_useful_token_index] and
|
|
command_prefixes[tokens[first_useful_token_index].text] do
|
|
if first_useful_token_index == #tokens then
|
|
return
|
|
end
|
|
|
|
first_useful_token_index = first_useful_token_index + 1
|
|
end
|
|
|
|
completion_pos = tokens[#tokens].pos
|
|
|
|
local add_actions = {
|
|
["add"] = true, ["append"] = true, ["pre"] = true, ["set"] = true
|
|
}
|
|
|
|
local first_useful_token = tokens[first_useful_token_index]
|
|
|
|
local property_pos = before_cur:match("${[=>]?()[%w_/-]*$")
|
|
if property_pos then
|
|
completion_pos = property_pos
|
|
completions = property_list()
|
|
completion_append = "}"
|
|
elseif #tokens == first_useful_token_index then
|
|
completions = command_list()
|
|
completions[#completions + 1] = "help"
|
|
elseif #tokens == first_useful_token_index + 1 then
|
|
if first_useful_token.text == "set" or
|
|
first_useful_token.text == "add" or
|
|
first_useful_token.text == "cycle" or
|
|
first_useful_token.text == "cycle-values" or
|
|
first_useful_token.text == "multiply" then
|
|
completions = property_list()
|
|
elseif first_useful_token.text == "help" then
|
|
completions = command_list()
|
|
elseif first_useful_token.text == "apply-profile" then
|
|
completions = profile_list()
|
|
elseif first_useful_token.text == "change-list" then
|
|
completions = list_option_list()
|
|
elseif first_useful_token.text == "run" then
|
|
completions = executable_list()
|
|
elseif first_useful_token.text == "vf" or
|
|
first_useful_token.text == "af" then
|
|
completions = list_option_action_list(first_useful_token.text)
|
|
elseif first_useful_token.text == "vf-command" or
|
|
first_useful_token.text == "af-command" then
|
|
completions = filter_label_list(first_useful_token.text:sub(1,2))
|
|
elseif has_file_argument(first_useful_token.text) then
|
|
completions = handle_file_completion(before_cur)
|
|
else
|
|
completions = command_flags_at_1st_argument_list(first_useful_token.text)
|
|
handle_flags(first_useful_token.text, 1, tokens[#tokens].text)
|
|
end
|
|
elseif first_useful_token.text == "cycle-values" then
|
|
completions = handle_choice_completion(tokens[first_useful_token_index + 1].text,
|
|
before_cur)
|
|
elseif first_useful_token.text == "run" then
|
|
completions = handle_file_completion(before_cur)
|
|
elseif #tokens == first_useful_token_index + 2 then
|
|
if first_useful_token.text == "set" then
|
|
completions = handle_choice_completion(tokens[first_useful_token_index + 1].text,
|
|
before_cur)
|
|
elseif first_useful_token.text == "change-list" then
|
|
completions = list_option_action_list(tokens[first_useful_token_index + 1].text)
|
|
elseif first_useful_token.text == "vf" or
|
|
first_useful_token.text == "af" then
|
|
if add_actions[tokens[first_useful_token_index + 1].text] then
|
|
completions = handle_choice_completion(first_useful_token.text, before_cur)
|
|
elseif tokens[first_useful_token_index + 1].text == "remove" then
|
|
completions = list_option_value_list(first_useful_token.text)
|
|
end
|
|
else
|
|
completions = command_flags_at_2nd_argument_list(first_useful_token.text)
|
|
handle_flags(first_useful_token.text, 2, tokens[#tokens].text)
|
|
end
|
|
elseif #tokens == first_useful_token_index + 3 then
|
|
if first_useful_token.text == "change-list" then
|
|
if add_actions[tokens[first_useful_token_index + 2].text] then
|
|
completions = handle_choice_completion(tokens[first_useful_token_index + 1].text,
|
|
before_cur)
|
|
elseif tokens[first_useful_token_index + 2].text == "remove" then
|
|
completions = list_option_value_list(tokens[first_useful_token_index + 1].text)
|
|
end
|
|
elseif first_useful_token.text == "dump-cache" then
|
|
completions = handle_file_completion(before_cur)
|
|
end
|
|
end
|
|
|
|
return completions or {}, completion_pos, completion_append
|
|
end
|
|
|
|
local function open(text, cursor_position, keep_open)
|
|
input.get({
|
|
prompt = ">",
|
|
submit = submit,
|
|
keep_open = keep_open,
|
|
opened = opened,
|
|
closed = closed,
|
|
complete = complete,
|
|
autoselect_completion = true,
|
|
default_text = text,
|
|
cursor_position = cursor_position,
|
|
history_path = options.persist_history
|
|
and mp.command_native({"expand-path", options.history_path}) or nil,
|
|
})
|
|
end
|
|
|
|
mp.add_key_binding(nil, "open", function ()
|
|
open(last_text, last_cursor_position, true)
|
|
end)
|
|
|
|
-- Open the console with passed text and cursor_position arguments
|
|
mp.register_script_message("type", open)
|
|
|
|
mp.register_event("log-message", function(e)
|
|
-- Ignore log messages from the OSD because of paranoia, since writing them
|
|
-- to the OSD could generate more messages in an infinite loop.
|
|
if e.prefix:sub(1, 3) == "osd" then return end
|
|
|
|
-- Ignore messages output by this script.
|
|
if e.prefix == mp.get_script_name() then return end
|
|
|
|
-- Ignore buffer overflow warning messages. Overflowed log messages would
|
|
-- have been offscreen anyway.
|
|
if e.prefix == "overflow" then return end
|
|
|
|
-- Filter out trace-level log messages, even if the terminal-default log
|
|
-- level includes them. These aren"t too useful for an on-screen display
|
|
-- without scrollback and they include messages that are generated from the
|
|
-- OSD display itself.
|
|
if e.level == "trace" then return end
|
|
|
|
-- Avoid logging debug messages infinitely.
|
|
if e.prefix == "cplayer" and
|
|
e.text:find('^Run command: script%-message%-to, flags=64, args=%[target="console"') then
|
|
return
|
|
end
|
|
|
|
-- Use color for debug/v/warn/error/fatal messages.
|
|
input.log("[" .. e.prefix .. "] " .. e.text:sub(1, -2), styles[e.level],
|
|
terminal_styles[e.level])
|
|
end)
|
|
|
|
-- Enable log messages. In silent mode, mpv will queue log messages in a buffer
|
|
-- until enable_messages is called again without the silent: prefix.
|
|
mp.enable_messages("silent:terminal-default")
|
|
|
|
require "mp.options".read_options(options)
|