--[[ 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 . ]] 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 []", return 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_arg_choice_list(command, arg_index, flags) for _, cmd in pairs(get_commands()) do if cmd.name == command then if cmd.args[arg_index] then if cmd.args[arg_index].type == "Flags" then local plus_pos = flags:find("%+[^%+]*$") if plus_pos then completion_pos = completion_pos + plus_pos end end return cmd.args[arg_index].choices end return end 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, response) 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 response({}, 1) 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_arg_choice_list(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_arg_choice_list(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 response(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)