diff options
Diffstat (limited to 'player/lua/console.lua')
-rw-r--r-- | player/lua/console.lua | 1386 |
1 files changed, 871 insertions, 515 deletions
diff --git a/player/lua/console.lua b/player/lua/console.lua index bbfaf478f7..4ebedc560c 100644 --- a/player/lua/console.lua +++ b/player/lua/console.lua @@ -17,26 +17,15 @@ local assdraw = require 'mp.assdraw' -- Default options local opts = { - -- All drawing is scaled by this value, including the text borders and the - -- cursor. Change it if you have a high-DPI display. - scale = 1, - -- Set the font used for the REPL and the console. - -- This has to be a monospaced font. font = "", - -- Set the font size used for the REPL and the console. This will be - -- multiplied by "scale". font_size = 16, border_size = 1, case_sensitive = true, - -- Remove duplicate entries in history as to only keep the latest one. history_dedup = true, - -- The ratio of font height to font width. - -- Adjusts table width of completion suggestions. - -- Values in the range 1.8..2.5 make sense for common monospace fonts. font_hw_ratio = 'auto', } -function detect_platform() +local function detect_platform() local platform = mp.get_property_native('platform') if platform == 'darwin' or platform == 'windows' then return platform @@ -72,18 +61,20 @@ local styles = { v = '{\\1c&H99cc99&}', warn = '{\\1c&H66ccff&}', error = '{\\1c&H7a77f2&}', - fatal = '{\\1c&H5791f9&\\b1}', + fatal = '{\\1c&H5791f9&}', suggestion = '{\\1c&Hcc99cc&}', selected_suggestion = '{\\1c&H2fbdfa&\\b1}', + disabled = '{\\1c&Hcccccc&}', } local terminal_styles = { - debug = '\027[1;30m', + debug = '\027[90m', v = '\027[32m', warn = '\027[33m', error = '\027[31m', - fatal = '\027[1;31m', + fatal = '\027[91m', selected_suggestion = '\027[7m', + disabled = '\027[38;5;8m', } local repl_active = false @@ -98,6 +89,7 @@ local id = default_id local histories = {[id] = {}} local history = histories[id] local history_pos = 1 +local searching_history = false local log_buffers = {[id] = {}} local key_bindings = {} local global_margins = { t = 0, b = 0 } @@ -105,139 +97,178 @@ 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 update_timer = nil -update_timer = mp.add_periodic_timer(0.05, function() - if pending_update then - update() - else - update_timer:kill() - end -end) -update_timer:kill() +local selectable_items +local matches = {} +local selected_match = 1 +local first_match_to_print = 1 -mp.observe_property("user-data/osc/margins", "native", function(_, val) - if val then - global_margins = val - else - global_margins = { t = 0, b = 0 } - end - update() -end) +local set_active -do - local width_length_ratio = 0.5 - local osd_width, osd_height = 100, 100 - ---Update osd resolution if valid - local function update_osd_resolution() - local dim = mp.get_property_native('osd-dimensions') - if not dim or dim.w == 0 or dim.h == 0 then - return +-- Naive helper function to find the next UTF-8 character in 'str' after 'pos' +-- by skipping continuation bytes. Assumes 'str' contains valid UTF-8. +local function next_utf8(str, pos) + if pos > str:len() then return pos end + repeat + pos = pos + 1 + until pos > str:len() or str:byte(pos) < 0x80 or str:byte(pos) > 0xbf + return pos +end + +-- As above, but finds the previous UTF-8 character in 'str' before 'pos' +local function prev_utf8(str, pos) + if pos <= 1 then return pos end + repeat + pos = pos - 1 + until pos <= 1 or str:byte(pos) < 0x80 or str:byte(pos) > 0xbf + return pos +end + +local function len_utf8(str) + local len = 0 + local pos = 1 + while pos <= str:len() do + pos = next_utf8(str, pos) + len = len + 1 + end + return len +end + +local function truncate_utf8(str, max_length) + local len = 0 + local pos = 1 + while pos <= #str do + local last_pos = pos + pos = next_utf8(str, pos) + len = len + 1 + if pos > last_pos + 1 then + if len == max_length - 1 then + pos = last_pos + else + len = len + 1 + end + end + if len == max_length - 1 then + return str:sub(1, pos - 1) .. '⋯' end - osd_width = dim.w - osd_height = dim.h end + return str +end - local text_osd = mp.create_osd_overlay('ass-events') - text_osd.compute_bounds, text_osd.hidden = true, true - local function measure_bounds(ass_text) - update_osd_resolution() - text_osd.res_x, text_osd.res_y = osd_width, osd_height - text_osd.data = ass_text - local res = text_osd:update() - return res.x0, res.y0, res.x1, res.y1 - end - - ---Measure text width and normalize to a font size of 1 - ---text has to be ass safe - 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 repetitions_left = 5 - repeat - 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) - return width / size, horizontal and osd_width or osd_height - end - - local function fit_on_osd(text) - local estimated_width = #text * width_length_ratio - if osd_width >= osd_height then - -- Fill the osd as much as possible, bigger is more accurate. - return math.min(osd_width / estimated_width, osd_height), true - else - return math.min(osd_height / estimated_width, osd_width), false - end +-- Functions to calculate the font width. +local width_length_ratio = 0.5 +local osd_width, osd_height = 100, 100 + +---Update osd resolution if valid +local function update_osd_resolution() + local dim = mp.get_property_native('osd-dimensions') + if not dim or dim.w == 0 or dim.h == 0 then + return end + osd_width = dim.w + osd_height = dim.h +end + +local text_osd = mp.create_osd_overlay('ass-events') +text_osd.compute_bounds, text_osd.hidden = true, true - local measured_font_hw_ratio = nil - function get_font_hw_ratio() - local font_hw_ratio = tonumber(opts.font_hw_ratio) - if font_hw_ratio then - return font_hw_ratio +local function measure_bounds(ass_text) + update_osd_resolution() + text_osd.res_x, text_osd.res_y = osd_width, osd_height + text_osd.data = ass_text + local res = text_osd:update() + return res.x0, res.y0, res.x1, res.y1 +end + +---Measure text width and normalize to a font size of 1 +---text has to be ass safe +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' + size = size / 0.8 + local width + -- Limit to 5 iterations + local repetitions_left = 5 + 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) + 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 not measured_font_hw_ratio then - local alphabet = 'abcdefghijklmnopqrstuvwxyz' - local text = alphabet:rep(3) - update_osd_resolution() - local size, horizontal = fit_on_osd(text) - local normalized_width = normalized_text_width(text, size * 0.9, horizontal) - measured_font_hw_ratio = #text / normalized_width * 0.95 + if i == repetitions_left then + width = 0 end - return measured_font_hw_ratio end + return width / size, horizontal and osd_width or osd_height end --- Add a line to the log buffer (which is limited to 100 lines) -function log_add(text, style, terminal_style) - local log_buffer = log_buffers[id] - log_buffer[#log_buffer + 1] = { - text = text, - style = style or '', - terminal_style = terminal_style or '', - } - if #log_buffer > 100 then - table.remove(log_buffer, 1) +local function fit_on_osd(text) + local estimated_width = #text * width_length_ratio + if osd_width >= osd_height then + -- Fill the osd as much as possible, bigger is more accurate. + return math.min(osd_width / estimated_width, osd_height), true + else + return math.min(osd_height / estimated_width, osd_width), false end +end - if repl_active then - if not update_timer:is_enabled() then - update() - update_timer:resume() - else - pending_update = true - end +local measured_font_hw_ratio = nil +local function get_font_hw_ratio() + local font_hw_ratio = tonumber(opts.font_hw_ratio) + if font_hw_ratio then + return font_hw_ratio + end + if not measured_font_hw_ratio then + local alphabet = 'abcdefghijklmnopqrstuvwxyz' + local text = alphabet:rep(3) + update_osd_resolution() + local size, horizontal = fit_on_osd(text) + local normalized_width = normalized_text_width(text, size * 0.9, horizontal) + measured_font_hw_ratio = #text / normalized_width * 0.95 end + return measured_font_hw_ratio end + -- Escape a string for verbatim display on the OSD -function ass_escape(str) +local 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) + * (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. -- Rows are cut off from the top if rows_max is specified. -- returns a string containing the formatted table and the row count -function format_table(list, width_max, rows_max) +local function format_table(list, width_max, rows_max) if #list == 0 then return '', 0 end @@ -259,9 +290,10 @@ function format_table(list, width_max, rows_max) -- use as many columns as possible for columns = 2, list_size do local rows_lower_bound = math.min(rows_max, math.ceil(list_size / columns)) - local rows_upper_bound = math.min(rows_max, list_size, math.ceil(list_size / (columns - 1) - 1)) + local rows_upper_bound = math.min(rows_max, list_size, + math.ceil(list_size / (columns - 1) - 1)) for rows = rows_upper_bound, rows_lower_bound, -1 do - cw = {} + local cw = {} width_total = 0 -- find out width of each column @@ -323,6 +355,118 @@ 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) + if line ~= '' then -- Prevent table.sort() from reordering the items. + table.sort(result, function (i, j) + return i[3] > j[3] + end) + end + for i, value in ipairs(result) do + result[i] = value[1] + end + return result +end + +local function calculate_max_terminal_width() + local max_width = mp.get_property_native('term-size/w', 80) + + -- The longest module name is vo/gpu-next/libplacebo. + if mp.get_property_native('msg-module') then + max_width = max_width - 24 + end + + if mp.get_property_native('msg-time') then + max_width = max_width - 13 + end + + return max_width +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)', + style = styles.disabled, + terminal_style = terminal_styles.disabled, + } + end + + for i = first_match_to_print, last_match_to_print do + log[#log + 1] = { + text = (max_width and truncate_utf8(matches[i].text, max_width) + or matches[i].text), + 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)', + 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,9 +474,11 @@ local function print_to_terminal() return end + populate_log_with_matches(calculate_max_terminal_width()) + local log = '' for _, log_line in ipairs(log_buffers[id]) do - log = log .. log_line.terminal_style .. log_line.text .. '\027[0m' + log = log .. log_line.terminal_style .. log_line.text .. '\027[0m\n' end local suggestions = '' @@ -359,40 +505,40 @@ local function print_to_terminal() end -- Render the REPL and console as an ASS OSD -function update() +local function update() pending_update = false -- Unlike vo-configured, current-vo doesn't become falsy while switching VO, -- which would print the log to the OSD. - if not mp.get_property('current-vo') then + if not mp.get_property('current-vo') or not mp.get_property_native('video-osd') then print_to_terminal() return end - local dpi_scale = mp.get_property_native("display-hidpi-scale", 1.0) - - dpi_scale = dpi_scale * opts.scale - - local screenx, screeny, aspect = mp.get_osd_size() - screenx = screenx / dpi_scale - screeny = screeny / dpi_scale - -- Clear the OSD if the REPL is not active if not repl_active then - mp.set_osd_ass(screenx, screeny, '') + mp.set_osd_ass(0, 0, '') return end + local screenx, screeny = mp.get_osd_size() + local dpi_scale = mp.get_property_native('display-hidpi-scale', 1) + screenx = screenx / dpi_scale + screeny = screeny / dpi_scale + + local bottom_left_margin = 6 + local coordinate_top = math.floor(global_margins.t * screeny + 0.5) local clipping_coordinates = '0,' .. coordinate_top .. ',' .. screenx .. ',' .. screeny local ass = assdraw.ass_new() - local has_shadow = mp.get_property('osd-back-color'):sub(2, 3) == '00' + local has_shadow = mp.get_property('osd-border-style'):find('box$') == nil local style = '{\\r' .. '\\1a&H00&\\3a&H00&\\1c&Heeeeee&\\3c&H111111&' .. (has_shadow and '\\4a&H99&\\4c&H000000&' or '') .. '\\fn' .. opts.font .. '\\fs' .. opts.font_size .. - '\\bord' .. opts.border_size .. '\\xshad0\\yshad1\\fsp0\\q1' .. + '\\bord' .. opts.border_size .. '\\xshad0\\yshad1\\fsp0' .. + (selectable_items and '\\q2' or '\\q1') .. '\\clip(' .. clipping_coordinates .. ')}' -- Create the cursor glyph as an ASS drawing. ASS will draw the cursor -- inline with the surrounding text, but it sets the advance to the width @@ -413,16 +559,17 @@ 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 width_max = math.floor((screenx - bottom_left_margin - + mp.get_property_native('osd-margin-x') * 2 * screeny / 720) / + 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() + local log_ass = '' local log_buffer = log_buffers[id] local log_messages = #log_buffer @@ -431,12 +578,13 @@ function update() log_messages = log_max_lines end for i = #log_buffer - log_messages + 1, #log_buffer do - log_ass = log_ass .. style .. log_buffer[i].style .. ass_escape(log_buffer[i].text) + log_ass = log_ass .. style .. log_buffer[i].style .. + ass_escape(log_buffer[i].text) .. '\\N' end ass:new_event() ass:an(1) - ass:pos(2, screeny - 2 - global_margins.b * screeny) + ass:pos(bottom_left_margin, screeny - bottom_left_margin - global_margins.b * screeny) ass:append(log_ass .. '\\N') if #suggestions > 0 then ass:append(suggestion_ass .. '\\N') @@ -449,7 +597,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(bottom_left_margin, screeny - bottom_left_margin - global_margins.b * screeny) ass:append(style .. '{\\alpha&HFF&}' .. ass_escape(prompt) .. ' ' .. before_cur) ass:append(cglyph) ass:append(style .. '{\\alpha&HFF&}' .. after_cur) @@ -457,108 +605,74 @@ function update() mp.set_osd_ass(screenx, screeny, ass.text) end --- Set the REPL visibility ("enable", Esc) -function set_active(active) - if active == repl_active then return end - if active then - repl_active = true - insert_mode = false - mp.enable_key_bindings('console-input', 'allow-hide-cursor+allow-vo-dragging') - define_key_bindings() - - if not input_caller then - prompt = default_prompt - id = default_id - history = histories[id] - history_pos = #history + 1 - mp.enable_messages('terminal-default') - end +local update_timer = nil +update_timer = mp.add_periodic_timer(0.05, function() + if pending_update then + update() else - repl_active = false - suggestion_buffer = {} - undefine_key_bindings() - mp.enable_messages('silent:terminal-default') - - if input_caller then - mp.commandv('script-message-to', input_caller, 'input-event', - 'closed', line, cursor) - input_caller = nil - line = '' - cursor = 1 - end - collectgarbage() + update_timer:kill() end - update() -end - --- Show the repl if hidden and replace its contents with 'text' --- (script-message-to repl type) -function show_and_type(text, cursor_pos) - text = text or '' - cursor_pos = tonumber(cursor_pos) +end) +update_timer:kill() - -- Save the line currently being edited, just in case - if line ~= text and line ~= '' and history[#history] ~= line then - history_add(line) +-- Add a line to the log buffer (which is limited to 100 lines) +local function log_add(text, style, terminal_style) + local log_buffer = log_buffers[id] + log_buffer[#log_buffer + 1] = { + text = text, + style = style or '', + terminal_style = terminal_style or '', + } + if #log_buffer > 100 then + table.remove(log_buffer, 1) end - line = text - if cursor_pos ~= nil and cursor_pos >= 1 - and cursor_pos <= line:len() + 1 then - cursor = math.floor(cursor_pos) - else - cursor = line:len() + 1 - end - history_pos = #history + 1 - insert_mode = false if repl_active then - update() - else - set_active(true) + if not update_timer:is_enabled() then + update() + update_timer:resume() + else + pending_update = true + end end end --- Naive helper function to find the next UTF-8 character in 'str' after 'pos' --- by skipping continuation bytes. Assumes 'str' contains valid UTF-8. -function next_utf8(str, pos) - if pos > str:len() then return pos end - repeat - pos = pos + 1 - until pos > str:len() or str:byte(pos) < 0x80 or str:byte(pos) > 0xbf - return pos -end +-- Add a line to the history and deduplicate +local function history_add(text) + if opts.history_dedup then + -- More recent entries are more likely to be repeated + for i = #history, 1, -1 do + if history[i] == text then + table.remove(history, i) + break + end + end + end --- As above, but finds the previous UTF-8 character in 'str' before 'pos' -function prev_utf8(str, pos) - if pos <= 1 then return pos end - repeat - pos = pos - 1 - until pos <= 1 or str:byte(pos) < 0x80 or str:byte(pos) > 0xbf - return pos + history[#history + 1] = text end -function len_utf8(str) - local len = 0 - local pos = 1 - while pos <= str:len() do - pos = next_utf8(str, pos) - len = len + 1 +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 - return len -end -local function handle_edit() suggestion_buffer = {} update() if input_caller then mp.commandv('script-message-to', input_caller, 'input-event', 'edited', - line) + utils.format_json({line})) end end -- Insert a character at the current cursor position (any_unicode) -function handle_char_input(c) +local function handle_char_input(c) if insert_mode then line = line:sub(1, cursor - 1) .. c .. line:sub(next_utf8(line, cursor)) else @@ -569,7 +683,7 @@ function handle_char_input(c) end -- Remove the character behind the cursor (Backspace) -function handle_backspace() +local function handle_backspace() if cursor <= 1 then return end local prev = prev_utf8(line, cursor) line = line:sub(1, prev - 1) .. line:sub(cursor) @@ -578,33 +692,33 @@ function handle_backspace() end -- Remove the character in front of the cursor (Del) -function handle_del() +local function handle_del() if cursor > line:len() then return end line = line:sub(1, cursor - 1) .. line:sub(next_utf8(line, cursor)) handle_edit() end -- Toggle insert mode (Ins) -function handle_ins() +local function handle_ins() insert_mode = not insert_mode end -- Move the cursor to the next character (Right) -function next_char(amount) +local function next_char() cursor = next_utf8(line, cursor) suggestion_buffer = {} update() end -- Move the cursor to the previous character (Left) -function prev_char(amount) +local function prev_char() cursor = prev_utf8(line, cursor) suggestion_buffer = {} update() end -- Clear the current line (Ctrl+C) -function clear() +local function clear() line = '' cursor = 1 insert_mode = false @@ -614,7 +728,7 @@ end -- Close the REPL if the current line is empty, otherwise delete the next -- character (Ctrl+D) -function maybe_exit() +local function maybe_exit() if line == '' then set_active(false) else @@ -622,7 +736,7 @@ function maybe_exit() end end -function help_command(param) +local function help_command(param) local cmdlist = mp.get_property_native('command-list') table.sort(cmdlist, function(c1, c2) return c1.name < c2.name @@ -647,7 +761,7 @@ function help_command(param) end end if not cmd then - log_add('No command matches "' .. param .. '"!\n', styles.error, + log_add('No command matches "' .. param .. '"!', styles.error, terminal_styles.error) return end @@ -663,26 +777,21 @@ function help_command(param) output = output .. 'This command supports variable arguments.\n' end end - log_add(output) + log_add(output:sub(1, -2)) end --- Add a line to the history and deduplicate -function history_add(text) - if opts.history_dedup then - -- More recent entries are more likely to be repeated - for i = #history, 1, -1 do - if history[i] == text then - table.remove(history, i) - break - end - end +-- Run the current command and clear the line (Enter) +local function handle_enter() + if searching_history then + searching_history = false + selectable_items = nil + line = #matches > 0 and matches[selected_match].text or '' + cursor = #line + 1 + log_buffers[id] = {} + update() + return end - history[#history + 1] = text -end - --- Run the current command and clear the line (Enter) -function handle_enter() if line == '' and input_caller == nil then return end @@ -690,9 +799,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 @@ -708,7 +823,7 @@ function handle_enter() end -- Go to the specified position in the command history -function go_history(new_pos) +local function go_history(new_pos) local old_pos = history_pos history_pos = new_pos @@ -744,23 +859,89 @@ function go_history(new_pos) end -- Go to the specified relative position in the command history (Up, Down) -function move_history(amount) +local 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() +local 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() +local 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 search_history() + if selectable_items or #history == 0 then + return + end + + searching_history = true + selectable_items = {} + matches = {} + selected_match = 1 + first_match_to_print = 1 + + for i = 1, #history do + selectable_items[i] = history[#history + 1 - i] + end + + for i, match in ipairs(fuzzy_find(line, selectable_items)) do + matches[i] = { index = match, text = selectable_items[match] } + end + + update() +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() +local function prev_word() -- This is basically the same as next_word() but backwards, so reverse the -- string in order to do a "backwards" find. This wouldn't be as annoying -- to do if Lua didn't insist on 1-based indexing. @@ -771,12 +952,142 @@ end -- Move to the end of the current word, or if already at the end, the end of -- the next word. (Ctrl+Right) -function next_word() +local function next_word() cursor = select(2, line:find('%s*[^%s]*', cursor)) + 1 suggestion_buffer = {} update() end +-- Move the cursor to the beginning of the line (HOME) +local function go_home() + cursor = 1 + suggestion_buffer = {} + update() +end + +-- Move the cursor to the end of the line (END) +local function go_end() + cursor = line:len() + 1 + suggestion_buffer = {} + update() +end + +-- Delete from the cursor to the beginning of the word (Ctrl+Backspace) +local function del_word() + local before_cur = line:sub(1, cursor - 1) + local after_cur = line:sub(cursor) + + before_cur = before_cur:gsub('[^%s]+%s*$', '', 1) + line = before_cur .. after_cur + cursor = before_cur:len() + 1 + handle_edit() +end + +-- Delete from the cursor to the end of the word (Ctrl+Del) +local function del_next_word() + if cursor > line:len() then return end + + local before_cur = line:sub(1, cursor - 1) + local after_cur = line:sub(cursor) + + after_cur = after_cur:gsub('^%s*[^%s]+', '', 1) + line = before_cur .. after_cur + handle_edit() +end + +-- Delete from the cursor to the end of the line (Ctrl+K) +local function del_to_eol() + line = line:sub(1, cursor - 1) + handle_edit() +end + +-- Delete from the cursor back to the start of the line (Ctrl+U) +local function del_to_start() + line = line:sub(cursor) + cursor = 1 + handle_edit() +end + +-- Empty the log buffer of all messages (Ctrl+L) +local function clear_log_buffer() + log_buffers[id] = {} + update() +end + +-- Returns a string of UTF-8 text from the clipboard (or the primary selection) +local function get_clipboard(clip) + if platform == 'x11' then + local res = utils.subprocess({ + args = { 'xclip', '-selection', clip and 'clipboard' or 'primary', '-out' }, + playback_only = false, + }) + if not res.error then + return res.stdout + end + elseif platform == 'wayland' then + local res = utils.subprocess({ + args = { 'wl-paste', clip and '-n' or '-np' }, + playback_only = false, |