summaryrefslogtreecommitdiffstats
path: root/player/lua
diff options
context:
space:
mode:
Diffstat (limited to 'player/lua')
-rw-r--r--player/lua/auto_profiles.lua8
-rw-r--r--player/lua/console.lua596
-rw-r--r--player/lua/defaults.lua13
-rw-r--r--player/lua/fzy.lua297
-rw-r--r--player/lua/input.lua38
-rw-r--r--player/lua/meson.build2
-rw-r--r--player/lua/options.lua22
-rw-r--r--player/lua/osc.lua249
-rw-r--r--player/lua/select.lua375
-rw-r--r--player/lua/stats.lua118
-rw-r--r--player/lua/ytdl_hook.lua16
11 files changed, 1382 insertions, 352 deletions
diff --git a/player/lua/auto_profiles.lua b/player/lua/auto_profiles.lua
index a0f580298b..167724344c 100644
--- a/player/lua/auto_profiles.lua
+++ b/player/lua/auto_profiles.lua
@@ -1,6 +1,5 @@
-- Note: anything global is accessible by profile condition expressions.
-local utils = require 'mp.utils'
local msg = require 'mp.msg'
local profiles = {}
@@ -128,7 +127,7 @@ end
local evil_magic = {}
setmetatable(evil_magic, {
- __index = function(table, key)
+ __index = function(_, key)
-- interpret everything as property, unless it already exists as
-- a non-nil global value
local v = _G[key]
@@ -141,7 +140,7 @@ setmetatable(evil_magic, {
p = {}
setmetatable(p, {
- __index = function(table, key)
+ __index = function(_, key)
return magic_get(key)
end,
})
@@ -149,6 +148,8 @@ setmetatable(p, {
local function compile_cond(name, s)
local code, chunkname = "return " .. s, "profile " .. name .. " condition"
local chunk, err
+ -- luacheck: push
+ -- luacheck: ignore setfenv loadstring
if setfenv then -- lua 5.1
chunk, err = loadstring(code, chunkname)
if chunk then
@@ -157,6 +158,7 @@ local function compile_cond(name, s)
else -- lua 5.2
chunk, err = load(code, chunkname, "t", evil_magic)
end
+ -- luacheck: pop
if not chunk then
msg.error("Profile '" .. name .. "' condition: " .. err)
chunk = function() return false end
diff --git a/player/lua/console.lua b/player/lua/console.lua
index bbfaf478f7..f8d52704cb 100644
--- a/player/lua/console.lua
+++ b/player/lua/console.lua
@@ -75,6 +75,7 @@ local styles = {
fatal = '{\\1c&H5791f9&\\b1}',
suggestion = '{\\1c&Hcc99cc&}',
selected_suggestion = '{\\1c&H2fbdfa&\\b1}',
+ disabled = '{\\1c&Hcccccc&}',
}
local terminal_styles = {
@@ -84,6 +85,7 @@ local terminal_styles = {
error = '\027[31m',
fatal = '\027[1;31m',
selected_suggestion = '\027[7m',
+ disabled = '\027[38;5;8m',
}
local repl_active = false
@@ -105,13 +107,17 @@ local input_caller
local suggestion_buffer = {}
local selected_suggestion_index
-local completion_start_position
+local completion_pos
local completion_append
-local file_commands = {}
local path_separator = platform == 'windows' and '\\' or '/'
local completion_old_line
local completion_old_cursor
+local selectable_items
+local matches = {}
+local selected_match = 1
+local first_match_to_print = 1
+
local update_timer = nil
update_timer = mp.add_periodic_timer(0.05, function()
if pending_update then
@@ -161,19 +167,24 @@ do
local function normalized_text_width(text, size, horizontal)
local align, rotation = horizontal and 7 or 1, horizontal and 0 or -90
local template = '{\\pos(0,0)\\rDefault\\blur0\\bord0\\shad0\\q2\\an%s\\fs%s\\fn%s\\frz%s}%s'
- local x1, y1 = nil, nil
size = size / 0.8
- -- prevent endless loop
+ local width
+ -- Limit to 5 iterations
local repetitions_left = 5
- repeat
+ for i = 1, repetitions_left do
size = size * 0.8
local ass = assdraw.ass_new()
ass.text = template:format(align, size, opts.font, rotation, text)
- _, _, x1, y1 = measure_bounds(ass.text)
- repetitions_left = repetitions_left - 1
- -- make sure nothing got clipped
- until (x1 and x1 < osd_width and y1 < osd_height) or repetitions_left == 0
- local width = (repetitions_left == 0 and not x1) and 0 or (horizontal and x1 or y1)
+ local _, _, x1, y1 = measure_bounds(ass.text)
+ -- Check if nothing got clipped
+ if x1 and x1 < osd_width and y1 < osd_height then
+ width = horizontal and x1 or y1
+ break
+ end
+ if i == repetitions_left then
+ width = 0
+ end
+ end
return width / size, horizontal and osd_width or osd_height
end
@@ -232,6 +243,24 @@ function ass_escape(str)
return mp.command_native({'escape-ass', str})
end
+local function calculate_max_log_lines()
+ if not mp.get_property_native('vo-configured') then
+ -- Subtract 1 for the input line and for each line in the status line.
+ -- This does not detect wrapped lines.
+ return mp.get_property_native('term-size/h', 24) - 2 -
+ select(2, mp.get_property('term-status-msg'):gsub('\\n', ''))
+ end
+
+ return math.floor(mp.get_property_native('osd-height')
+ / mp.get_property_native('display-hidpi-scale', 1)
+ / opts.scale
+ * (1 - global_margins.t - global_margins.b)
+ / opts.font_size
+ -- Subtract 1 for the input line and 1 for the newline
+ -- between the log and the input line.
+ - 2)
+end
+
-- Takes a list of strings, a max width in characters and
-- optionally a max row count.
-- The result contains at least one column.
@@ -323,6 +352,100 @@ function format_table(list, width_max, rows_max)
return table.concat(rows, ass_escape('\n')), row_count
end
+local function fuzzy_find(needle, haystacks)
+ local result = require 'mp.fzy'.filter(needle, haystacks)
+ table.sort(result, function (i, j)
+ return i[3] > j[3]
+ end)
+ for i, value in ipairs(result) do
+ result[i] = value[1]
+ end
+ return result
+end
+
+local function populate_log_with_matches(max_width)
+ if not selectable_items or selected_match == 0 then
+ return
+ end
+
+ log_buffers[id] = {}
+ local log = log_buffers[id]
+
+ local max_log_lines = calculate_max_log_lines()
+
+ if selected_match < first_match_to_print then
+ first_match_to_print = selected_match
+ end
+
+ if first_match_to_print > 1 then
+ -- Reserve the first line for "n hidden items".
+ max_log_lines = max_log_lines - 1
+ end
+
+ if selected_match > first_match_to_print + max_log_lines - 1 then
+ -- Reserve the first line for "n hidden items" if it wasn't already.
+ if first_match_to_print == 1 then
+ max_log_lines = max_log_lines - 1
+ end
+
+ first_match_to_print = selected_match - max_log_lines + 1
+ end
+
+ local last_match_to_print = math.min(first_match_to_print + max_log_lines - 1,
+ #matches)
+
+ if last_match_to_print < #matches then
+ -- Reserve the last line for "n hidden items".
+ last_match_to_print = last_match_to_print - 1
+
+ -- After decrementing the last match to print, we need to check if the
+ -- selected match is beyond the last match to print again, and shift
+ -- both the first and last match to print when it is.
+ if selected_match > last_match_to_print then
+ if first_match_to_print == 1 then
+ -- Reserve the first line for "2 hidden items".
+ first_match_to_print = first_match_to_print + 1
+ end
+
+ first_match_to_print = first_match_to_print + 1
+ last_match_to_print = last_match_to_print + 1
+ end
+ end
+
+ -- When there is only 1 hidden item, print it in the previously reserved
+ -- line instead of printing "1 hidden items".
+ if first_match_to_print == 2 then
+ first_match_to_print = 1
+ end
+ if last_match_to_print == #matches - 1 then
+ last_match_to_print = #matches
+ end
+
+ if first_match_to_print > 1 then
+ log[1] = {
+ text = '↑ (' .. (first_match_to_print - 1) .. ' hidden items)' .. '\n',
+ style = styles.disabled,
+ terminal_style = terminal_styles.disabled,
+ }
+ end
+
+ for i = first_match_to_print, last_match_to_print do
+ log[#log + 1] = {
+ text = truncate_utf8(matches[i].text, max_width) .. '\n',
+ style = i == selected_match and styles.selected_suggestion or '',
+ terminal_style = i == selected_match and terminal_styles.selected_suggestion or '',
+ }
+ end
+
+ if last_match_to_print < #matches then
+ log[#log + 1] = {
+ text = '↓ (' .. (#matches - last_match_to_print) .. ' hidden items)' .. '\n',
+ style = styles.disabled,
+ terminal_style = terminal_styles.disabled,
+ }
+ end
+end
+
local function print_to_terminal()
-- Clear the log after closing the console.
if not repl_active then
@@ -330,6 +453,8 @@ local function print_to_terminal()
return
end
+ populate_log_with_matches(mp.get_property_native('term-size/w', 80))
+
local log = ''
for _, log_line in ipairs(log_buffers[id]) do
log = log .. log_line.terminal_style .. log_line.text .. '\027[0m'
@@ -373,7 +498,7 @@ function update()
dpi_scale = dpi_scale * opts.scale
- local screenx, screeny, aspect = mp.get_osd_size()
+ local screenx, screeny = mp.get_osd_size()
screenx = screenx / dpi_scale
screeny = screeny / dpi_scale
@@ -413,16 +538,15 @@ function update()
-- Render log messages as ASS.
-- This will render at most screeny / font_size - 1 messages.
- -- lines above the prompt
- -- subtract 1.5 to account for the input line
- local screeny_factor = (1 - global_margins.t - global_margins.b)
- local lines_max = math.ceil(screeny * screeny_factor / opts.font_size - 1.5)
+ local lines_max = calculate_max_log_lines()
-- Estimate how many characters fit in one line
local width_max = math.ceil(screenx / opts.font_size * get_font_hw_ratio())
local suggestions, rows = format_table(suggestion_buffer, width_max, lines_max)
local suggestion_ass = style .. styles.suggestion .. suggestions
+ populate_log_with_matches(width_max)
+
local log_ass = ''
local log_buffer = log_buffers[id]
local log_messages = #log_buffer
@@ -436,7 +560,7 @@ function update()
ass:new_event()
ass:an(1)
- ass:pos(2, screeny - 2 - global_margins.b * screeny)
+ ass:pos(6, screeny - 6 - global_margins.b * screeny)
ass:append(log_ass .. '\\N')
if #suggestions > 0 then
ass:append(suggestion_ass .. '\\N')
@@ -449,7 +573,7 @@ function update()
-- cursor appear in front of the text.
ass:new_event()
ass:an(1)
- ass:pos(2, screeny - 2 - global_margins.b * screeny)
+ ass:pos(6, screeny - 6 - global_margins.b * screeny)
ass:append(style .. '{\\alpha&HFF&}' .. ass_escape(prompt) .. ' ' .. before_cur)
ass:append(cglyph)
ass:append(style .. '{\\alpha&HFF&}' .. after_cur)
@@ -481,10 +605,11 @@ function set_active(active)
if input_caller then
mp.commandv('script-message-to', input_caller, 'input-event',
- 'closed', line, cursor)
+ 'closed', utils.format_json({line, cursor}))
input_caller = nil
line = ''
cursor = 1
+ selectable_items = nil
end
collectgarbage()
end
@@ -547,13 +672,35 @@ function len_utf8(str)
return len
end
+function truncate_utf8(str, max_length)
+ local len = 0
+ local pos = 1
+ while pos <= #str do
+ pos = next_utf8(str, pos)
+ len = len + 1
+ if len == max_length - 1 then
+ return str:sub(1, pos - 1) .. '⋯'
+ end
+ end
+ return str
+end
+
local function handle_edit()
+ if selectable_items then
+ matches = {}
+ selected_match = 1
+
+ for i, match in ipairs(fuzzy_find(line, selectable_items)) do
+ matches[i] = { index = match, text = selectable_items[match] }
+ end
+ end
+
suggestion_buffer = {}
update()
if input_caller then
mp.commandv('script-message-to', input_caller, 'input-event', 'edited',
- line)
+ utils.format_json({line}))
end
end
@@ -590,14 +737,14 @@ function handle_ins()
end
-- Move the cursor to the next character (Right)
-function next_char(amount)
+function next_char()
cursor = next_utf8(line, cursor)
suggestion_buffer = {}
update()
end
-- Move the cursor to the previous character (Left)
-function prev_char(amount)
+function prev_char()
cursor = prev_utf8(line, cursor)
suggestion_buffer = {}
update()
@@ -690,9 +837,15 @@ function handle_enter()
history_add(line)
end
- if input_caller then
+ if selectable_items then
+ if #matches > 0 then
+ mp.commandv('script-message-to', input_caller, 'input-event', 'submit',
+ utils.format_json({matches[selected_match].index}))
+ end
+ set_active(false)
+ elseif input_caller then
mp.commandv('script-message-to', input_caller, 'input-event', 'submit',
- line)
+ utils.format_json({line}))
else
-- match "help [<text>]", return <text> or "", strip all whitespace
local help = line:match('^%s*help%s+(.-)%s*$') or
@@ -745,19 +898,63 @@ end
-- Go to the specified relative position in the command history (Up, Down)
function move_history(amount)
+ if selectable_items then
+ selected_match = selected_match + amount
+ if selected_match > #matches then
+ selected_match = 1
+ elseif selected_match < 1 then
+ selected_match = #matches
+ end
+ update()
+ return
+ end
+
go_history(history_pos + amount)
end
-- Go to the first command in the command history (PgUp)
function handle_pgup()
+ if selectable_items then
+ -- We don't know whether to count the "n hidden items" lines here; an
+ -- offset of 2 is better with 1 extra line because it scrolls from the
+ -- last to the first visible match, while with both extra lines that is
+ -- done with +3. When there are no "n hidden items" lines selected_match
+ -- becomes 1 with any offset >= 1.
+ selected_match = math.max(selected_match - calculate_max_log_lines() + 2, 1)
+ update()
+ return
+ end
+
go_history(1)
end
-- Stop browsing history and start editing a blank line (PgDown)
function handle_pgdown()
+ if selectable_items then
+ selected_match = math.min(selected_match + calculate_max_log_lines() - 2, #matches)
+ update()
+ return
+ end
+
go_history(#history + 1)
end
+local function page_up_or_prev_char()
+ if selectable_items then
+ handle_pgup()
+ else
+ prev_char()
+ end
+end
+
+local function page_down_or_next_char()
+ if selectable_items then
+ handle_pgdown()
+ else
+ next_char()
+ end
+end
+
-- Move to the start of the current word, or if already at the start, the start
-- of the previous word. (Ctrl+Left)
function prev_word()
@@ -786,13 +983,6 @@ local function command_list()
return commands
end
-local function command_list_and_help()
- local commands = command_list()
- commands[#commands + 1] = 'help'
-
- return commands
-end
-
local function property_list()
local properties = mp.get_property_native('property-list')
@@ -807,7 +997,7 @@ local function property_list()
for _, sub_property in pairs({
'name', 'type', 'set-from-commandline', 'set-locally',
- 'default-value', 'min', 'max', 'choices',
+ 'expects-file', 'default-value', 'min', 'max', 'choices',
}) do
properties[#properties + 1] = 'option-info/' .. option .. '/' ..
sub_property
@@ -847,7 +1037,7 @@ local function list_option_list()
return options
end
-local function list_option_verb_list(option)
+local function list_option_action_list(option)
local type = mp.get_property('option-info/' .. option .. '/type')
if type == 'Key/value list' then
@@ -857,33 +1047,33 @@ local function list_option_verb_list(option)
if type == 'String list' or type == 'Object settings list' then
return {'add', 'append', 'clr', 'pre', 'set', 'remove', 'toggle'}
end
-
- return {}
end
-local function choice_list(option)
- local info = mp.get_property_native('option-info/' .. option, {})
+local function list_option_value_list(option)
+ local values = mp.get_property_native(option)
- if info.type == 'Flag' then
- return { 'no', 'yes' }
+ if type(values) ~= 'table' then
+ return
end
- return info.choices or {}
-end
+ if type(values[1]) ~= 'table' then
+ return values
+ end
-local function find_commands_with_file_argument()
- if #file_commands > 0 then
- return file_commands
+ 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)
for _, command in pairs(mp.get_property_native('command-list')) do
- if command.args[1] and
- (command.args[1].name == 'filename' or command.args[1].name == 'url') then
- file_commands[#file_commands + 1] = command.name
+ if command.name == candidate_command then
+ return command.args[1] and
+ (command.args[1].name == 'filename' or command.args[1].name == 'url')
end
end
-
- return file_commands
end
local function file_list(directory)
@@ -900,61 +1090,39 @@ local function file_list(directory)
return files
end
--- List of tab-completions:
--- pattern: A Lua pattern used in string:match. It should return the start
--- position of the word to be completed in the first capture (using
--- the empty parenthesis notation "()"). In patterns with 2
--- captures, the first determines the completions, and the second is
--- the start of the word to be completed.
--- list: A function that returns a list of candidate completion values.
--- append: An extra string to be appended to the end of a successful
--- completion. It is only appended if 'list' contains exactly one
--- match.
-function build_completers()
- local completers = {
- { pattern = '^%s*()[%w_-]*$', list = command_list_and_help, append = ' ' },
- { pattern = '^%s*help%s+()[%w_-]*$', list = command_list },
- { pattern = '^%s*set%s+"?([%w_-]+)"?%s+()%S*$', list = choice_list },
- { pattern = '^%s*set%s+"?([%w_-]+)"?%s+"()%S*$', list = choice_list, append = '"' },
- { pattern = '^%s*cycle[-_]values%s+"?([%w_-]+)"?.-%s+()%S*$', list = choice_list, append = " " },
- { pattern = '^%s*cycle[-_]values%s+"?([%w_-]+)"?.-%s+"()%S*$', list = choice_list, append = '" ' },
- { pattern = '^%s*apply[-_]profile%s+"()%S*$', list = profile_list, append = '"' },
- { pattern = '^%s*apply[-_]profile%s+()%S*$', list = profile_list },
- { pattern = '^%s*change[-_]list%s+()[%w_-]*$', list = list_option_list, append = ' ' },
- { pattern = '^%s*change[-_]list%s+()"[%w_-]*$', list = list_option_list, append = '" ' },
- { pattern = '^%s*change[-_]list%s+"?([%w_-]+)"?%s+()%a*$', list = list_option_verb_list, append = ' ' },
- { pattern = '^%s*change[-_]list%s+"?([%w_-]+)"?%s+"()%a*$', list = list_option_verb_list, append = '" ' },
- { pattern = '^%s*([av]f)%s+()%a*$', list = list_option_verb_list, append = ' ' },
- { pattern = '^%s*([av]f)%s+"()%a*$', list = list_option_verb_list, append = '" ' },
- { pattern = '${[=>]?()[%w_/-]*$', list = property_list, append = '}' },
- }
+local function handle_file_completion(before_cur, path_pos)
+ local directory, last_component_pos =
+ before_cur:sub(path_pos):match('(.-)()[^' .. path_separator ..']*$')
+ completion_pos = path_pos + last_component_pos - 1
- for _, command in pairs({'set', 'add', 'cycle', 'cycle[-_]values', 'multiply'}) do
- completers[#completers + 1] = {
- pattern = '^%s*' .. command .. '%s+()[%w_/-]*$',
- list = property_list,
- append = ' ',
- }
- completers[#completers + 1] = {
- pattern = '^%s*' .. command .. '%s+"()[%w_/-]*$',
- list = property_list,
- append = '" ',
- }
+ if directory:find('^~' .. path_separator) then
+ local home = mp.command_native({'expand-path', '~/'})
+ before_cur = before_cur:sub(1, completion_pos - #directory - 1) ..
+ home ..
+ before_cur:sub(completion_pos - #directory + 1)
+ directory = home .. directory:sub(2)
+ completion_pos = completion_pos + #home - 1
end
+ -- Don't use completion_append for file completion to not add quotes after
+ -- directories whose entries you may want to complete afterwards.
+ completion_append = ''
- for _, command in pairs(find_commands_with_file_argument()) do
- completers[#completers + 1] = {
- pattern = '^%s*' .. command:gsub('-', '[-_]') ..
- '%s+["\']?(.-)()[^' .. path_separator ..']*$',
- list = file_list,
- -- Unfortunately appending " here would append it everytime a
- -- directory is fully completed, even if you intend to browse it
- -- afterwards.
- }
+ return file_list(directory), before_cur
+end
+
+local function handle_choice_completion(option, before_cur, path_pos)
+ local info = mp.get_property_native('option-info/' .. option, {})
+
+ if info.type == 'Flag' then
+ return { 'no', 'yes' }, before_cur
+ end
+
+ if info['expects-file'] then
+ return handle_file_completion(before_cur, path_pos)
end
- return completers
+ return info.choices, before_cur
end
function common_prefix_length(s1, s2)
@@ -1057,7 +1225,7 @@ local function cycle_through_suggestions(backwards)
selected_suggestion_index = #suggestion_buffer
end
- local before_cur = line:sub(1, completion_start_position - 1) ..
+ local before_cur = line:sub(1, completion_pos - 1) ..
suggestion_buffer[selected_suggestion_index] .. completion_append
line = before_cur .. strip_common_characters(line:sub(cursor), completion_append)
cursor = before_cur:len() + 1
@@ -1075,72 +1243,180 @@ function complete(backwards)
completion_old_line = line
completion_old_cursor = cursor
mp.commandv('script-message-to', input_caller, 'input-event',
- 'complete', line:sub(1, cursor - 1))
+ 'complete', utils.format_json({line:sub(1, cursor - 1)}))
return
end
local before_cur = line:sub(1, cursor - 1)
local after_cur = line:sub(cursor)
+ 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
- -- Try the first completer that works
- for _, completer in ipairs(build_completers()) do
- -- Completer patterns should return the start of the word to be
- -- completed as the first capture.
- local s2
- completion_start_position, s2 = before_cur:match(completer.pattern)
- if not completion_start_position then
- -- Multiple input commands can be separated by semicolons, so all
- -- completions that are anchored at the start of the string with
- -- '^' can start from a semicolon as well. Replace ^ with ; and try
- -- to match again.
- completion_start_position, s2 =
- before_cur:match(completer.pattern:gsub('^^', ';'))
+ 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
- if completion_start_position then
- local hint
- if s2 then
- hint = completion_start_position
- completion_start_position = s2
- end
+ end
- -- Expand ~ in file completion.
- if completer.list == file_list and hint:find('^~' .. path_separator) then
- local home = mp.command_native({'expand-path', '~/'})
- before_cur = before_cur:sub(1, completion_start_position - #hint - 1) ..
- home ..
- before_cur:sub(completion_start_position - #hint + 1)
- hint = home .. hint:sub(2)
- completion_start_position = completion_start_position + #home - 1
- 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, ['async'] = true, ['sync'] = true
+ }
- -- If the completer's pattern found a word, check the completer's
- -- list for possible completions
- local part = before_cur:sub(completion_start_position)
- local completions, prefix = complete_match(part, completer.list(hint))
- if #completions > 0 then
- -- If there was only one full match from the list, add
- -- completer.append to the final string. This is normally a
- -- space or a quotation mark followed by a space.
- completion_append = completer.append or ''
- if #completions == 1 then
- prefix = prefix .. completion_append
- after_cur = strip_common_characters(after_cur, completion_append)
- else
- table.sort(completions)
- suggestion_buffer = completions
- selected_suggestion_index = 0
- end
+ while tokens[first_useful_token_index] and
+ command_prefixes[tokens[first_useful_token_index].text] do
+ first_useful_token_index = first_useful_token_index + 1
+ end
+
+ -- Add an empty token if the cursor is after whitespace to simplify
+ -- comparisons.
+ if before_cur == '' or before_cur:find('%s$') then
+ tokens[#tokens + 1] = { text = "", pos = cursor }
+ end
- -- Insert the completion and update
- before_cur = before_cur:sub(1, completion_start_position - 1) ..
- prefix
- cursor = before_cur:len() + 1
- line = before_cur .. after_cur
- update()
- return
+ local add_actions = {
+ ['add'] = true, ['append'] = true, ['pre'] = true, ['set'] = true
+ }
+
+ local first_useful_token = tokens[first_useful_token_index]
+
+ completion_pos = before_cur:match('${[=>]?()[%w_/-]*$')
+ if completion_pos then
+ completions = property_list()
+ completion_append = '} '
+ elseif #tokens == first_useful_token_index then
+ completions = command_list()
+ completions[#completions + 1] = 'help'
+ completion_pos = first_useful_token.pos
+ completion_append = completion_append .. ' '
+ 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()
+ completion_pos = tokens[first_useful_token_index + 1].pos
+ completion_append = completion_append .. ' '
+ elseif first_useful_token.text == 'help' then
+ completions = command_list()
+ completion_pos = tokens[first_useful_token_index + 1].pos
+ elseif first_useful_token.text == 'apply-profile' then
+ completions = profile_list()
+ completion_pos = tokens[first_useful_token_index + 1].pos
+ elseif first_useful_token.text == 'change-list' then
+ completions = list_option_list()
+ completion_pos = tokens[first_useful_token_index + 1].pos
+ completion_append = completion_append .. ' '
+ elseif first_useful_token.text == 'vf' or
+ first_useful_token.text == 'af' then
+ completions = list_option_action_list(first_useful_token.text)
+ completion_pos = tokens[first_useful_token_index + 1].pos
+ completion_append = completion_append .. ' '
+ elseif has_file_argument(first_useful_token.text) then
+ completions, before_cur =
+ handle_file_completion(before_cur, tokens[first_useful_token_index + 1].pos)
+ end
+ elseif first_useful_token.text == 'cycle-values' then
+ completion_pos = tokens[#tokens].pos
+ completion_append = completion_append .. ' '
+ completions, before_cur =
+ handle_choice_completion(tokens[first_useful_token_index + 1].text,
+ before_cur, tokens[#tokens].pos)
+ elseif #tokens == first_useful_token_index + 2 then
+ if first_useful_token.text == 'set' then
+ completion_pos = tokens[#tokens].pos
+ completions, before_cur =
+ handle_choice_completion(tokens[first_useful_token_index + 1].text,
+ before_cur,
+ tokens[first_useful_token_index + 2].pos)
+ elseif first_useful_token.text == 'change-list' then
+ completions = list_option_action_list(tokens[first_useful_token_index + 1].text)
+ completion_pos = tokens[first_useful_token_index + 2].pos
+ completion_append = completion_append .. ' '
+ elseif first_useful_token.text == 'vf' or
+ first_useful_token.text == 'af' then
+ if add_actions[tokens[first_useful_token_index + 1].text] then
+ completion_pos = tokens[#tokens].pos
+ completions, before_cur =
+ handle_choice_completion(first_useful_token.text,
+ before_cur, tokens[#tokens].pos)
+ elseif tokens[first_useful_token_index + 1].text == 'remove' then
+ completions = list_option_value_list(first_useful_token.text)
+ completion_pos = tokens[#tokens].pos
end
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
+ completion_pos = tokens[#tokens].pos
+ completions, before_cur =
+ handle_choice_completion(tokens[first_useful_token_index + 1].text,
+ before_cur, tokens[#tokens].pos)
+ elseif tokens[first_useful_token_index + 2].text == 'remove' then
+ completion_pos = tokens[#tokens].pos
+ completions = list_option_value_list(tokens[first_useful_token_index + 1].text)
+ end
+ elseif first_useful_token.text == 'dump-cache' then
+ completions, before_cur =
+ handle_file_completion(before_cur,
+ tokens[first_useful_token_index + 3].pos)
+ end
end
+
+ if completions == nil then
+ return
+ end
+
+ local prefix
+ completions, prefix =
+ complete_match(before_cur:sub(completion_pos), completions)
+
+ if #completions == 1 then
+ prefix = prefix .. completion_append
+ after_cur = strip_common_characters(after_cur, completion_append)
+ else
+ table.sort(completions)
+ suggestion_buffer = completions
+ selected_suggestion_index = 0
+ end
+
+ before_cur = before_cur:sub(1, completion_pos - 1) .. prefix
+ cursor = before_cur:len() + 1
+ line = before_cur .. after_cur
+ update()
end
-- Move the cursor to the beginning of the line (HOME)
@@ -1285,9 +1561,9 @@ function get_bindings()
{ 'shift+ins', function() paste(false) end },
{ 'mbtn_mid', function() paste(false) end },
{ 'left', function() prev_char() end },
- { 'ctrl+b', function() prev_char() end },
+ { 'ctrl+b', function() page_up_or_prev_char() end },
{ 'right', function() next_char() end },
- { 'ctrl+f', function() next_char() end },
+ { 'ctrl+f', function() page_down_or_next_char() end},
{ 'up', function() move_history(-1) end },
{ 'ctrl+p', function() move_history(-1) end },
{ 'wheel_up', function() move_history(-1) end },
@@ -1385,7 +1661,7 @@ mp.register_script_message('get-input', function (script_name, args)
args = utils.parse_json(args)
prompt = args.prompt or default_prompt
line = args.default_text or ''
- cursor = tonumber(args.cursor_position) or line:len() + 1
+ cursor = args.cursor_position or line:len() + 1
id = args.id or script_name .. prompt
if histories[id] == nil then
histories[id] = {}
@@ -1394,6 +1670,16 @@ mp.register_script_message('get-input', function (script_name, args)
history = histories[id]
history_pos = #history + 1
+ selectable_items = args.items
+ if selectable_items then
+ matches = {}
+ selected_match = args.default_item or 1<