diff options
Diffstat (limited to 'player/lua/console.lua')
-rw-r--r-- | player/lua/console.lua | 561 |
1 files changed, 427 insertions, 134 deletions
diff --git a/player/lua/console.lua b/player/lua/console.lua index 44e9436..bbfaf47 100644 --- a/player/lua/console.lua +++ b/player/lua/console.lua @@ -27,11 +27,13 @@ local opts = { -- 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. - font_hw_ratio = 2.0, + -- Values in the range 1.8..2.5 make sense for common monospace fonts. + font_hw_ratio = 'auto', } function detect_platform() @@ -48,6 +50,7 @@ end local platform = detect_platform() if platform == 'windows' then opts.font = 'Consolas' + opts.case_sensitive = false elseif platform == 'darwin' then opts.font = 'Menlo' else @@ -66,11 +69,21 @@ local styles = { -- cccc66 cc9966 cc99cc 537bd2 debug = '{\\1c&Ha09f93&}', - verbose = '{\\1c&H99cc99&}', + v = '{\\1c&H99cc99&}', warn = '{\\1c&H66ccff&}', error = '{\\1c&H7a77f2&}', fatal = '{\\1c&H5791f9&\\b1}', suggestion = '{\\1c&Hcc99cc&}', + selected_suggestion = '{\\1c&H2fbdfa&\\b1}', +} + +local terminal_styles = { + debug = '\027[1;30m', + v = '\027[32m', + warn = '\027[33m', + error = '\027[31m', + fatal = '\027[1;31m', + selected_suggestion = '\027[7m', } local repl_active = false @@ -78,15 +91,26 @@ local insert_mode = false local pending_update = false local line = '' local cursor = 1 -local history = {} +local default_prompt = '>' +local prompt = default_prompt +local default_id = 'default' +local id = default_id +local histories = {[id] = {}} +local history = histories[id] local history_pos = 1 -local log_buffer = {} -local suggestion_buffer = {} +local log_buffers = {[id] = {}} local key_bindings = {} local global_margins = { t = 0, b = 0 } +local input_caller +local suggestion_buffer = {} +local selected_suggestion_index +local completion_start_position +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() @@ -107,9 +131,88 @@ mp.observe_property("user-data/osc/margins", "native", function(_, val) update() end) +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 + 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 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 + end + + 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 + 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 +end + -- Add a line to the log buffer (which is limited to 100 lines) -function log_add(style, text) - log_buffer[#log_buffer + 1] = { style = style, text = text } +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 @@ -126,19 +229,7 @@ end -- Escape a string for verbatim display on the OSD function ass_escape(str) - -- There is no escape for '\' in ASS (I think?) but '\' is used verbatim if - -- it isn't followed by a recognised character, so add a zero-width - -- non-breaking space - str = str:gsub('\\', '\\\239\187\191') - str = str:gsub('{', '\\{') - str = str:gsub('}', '\\}') - -- Precede newlines with a ZWNBSP to prevent ASS's weird collapsing of - -- consecutive newlines - str = str:gsub('\n', '\239\187\191\\N') - -- Turn leading spaces into hard spaces to prevent ASS from stripping them - str = str:gsub('\\N ', '\\N\\h') - str = str:gsub('^ ', '\\h') - return str + return mp.command_native({'escape-ass', str}) end -- Takes a list of strings, a max width in characters and @@ -206,7 +297,9 @@ function format_table(list, width_max, rows_max) local spaces = math.floor((width_max - width_total) / (column_count - 1)) spaces = math.max(spaces_min, math.min(spaces_max, spaces)) - local spacing = column_count > 1 and string.format('%' .. spaces .. 's', ' ') or '' + local spacing = column_count > 1 + and ass_escape(string.format('%' .. spaces .. 's', ' ')) + or '' local rows = {} for row = 1, row_count do @@ -217,12 +310,17 @@ function format_table(list, width_max, rows_max) -- more then 99 leads to 'invalid format (width or precision too long)' local format_string = column == column_count and '%s' or '%-' .. math.min(column_widths[column], 99) .. 's' - columns[column] = string.format(format_string, list[i]) + columns[column] = ass_escape(string.format(format_string, list[i])) + + if i == selected_suggestion_index then + columns[column] = styles.selected_suggestion .. columns[column] + .. '{\\b0}'.. styles.suggestion + end end -- first row is at the bottom rows[row_count - row + 1] = table.concat(columns, spacing) end - return table.concat(rows, '\n'), row_count + return table.concat(rows, ass_escape('\n')), row_count end local function print_to_terminal() @@ -233,13 +331,19 @@ local function print_to_terminal() end local log = '' - for _, log_line in ipairs(log_buffer) do - log = log .. log_line.text + for _, log_line in ipairs(log_buffers[id]) do + log = log .. log_line.terminal_style .. log_line.text .. '\027[0m' end - local suggestions = table.concat(suggestion_buffer, '\t') - if suggestions ~= '' then - suggestions = suggestions .. '\n' + local suggestions = '' + for i, suggestion in ipairs(suggestion_buffer) do + if i == selected_suggestion_index then + suggestions = suggestions .. terminal_styles.selected_suggestion .. + suggestion .. '\027[0m' + else + suggestions = suggestions .. suggestion + end + suggestions = suggestions .. (i < #suggestion_buffer and '\t' or '\n') end local before_cur = line:sub(1, cursor - 1) @@ -249,22 +353,18 @@ local function print_to_terminal() after_cur = ' ' end - mp.osd_message(log .. suggestions .. '> ' .. before_cur .. '\027[7m' .. - after_cur:sub(1, 1) .. '\027[0m' .. after_cur:sub(2), 999) + mp.osd_message(log .. suggestions .. prompt .. ' ' .. before_cur .. + '\027[7m' .. after_cur:sub(1, 1) .. '\027[0m' .. + after_cur:sub(2), 999) end -- Render the REPL and console as an ASS OSD function update() pending_update = false - -- Print to the terminal when there is no VO. Check both vo-configured so - -- it works with --force-window --idle and no video tracks, and whether - -- there is a video track so that the condition doesn't become true while - -- switching VO at runtime, making mp.osd_message() print to the VO's OSD. - -- This issue does not happen when switching VO without any video track - -- regardless of the condition used. - if not mp.get_property_native('vo-configured') - and not mp.get_property('current-tracks/video') then + -- 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 print_to_terminal() return end @@ -300,8 +400,9 @@ function update() -- thin as possible and make it appear to be 1px wide by giving it 0.5px -- horizontal borders. local cheight = opts.font_size * 8 - local cglyph = '{\\r' .. - '\\1a&H44&\\3a&H44&\\4a&H99&' .. + local cglyph = '{\\rDefault' .. + (mp.get_property_native('focused') == false + and '\\alpha&HFF&' or '\\1a&H44&\\3a&H44&\\4a&H99&') .. '\\1c&Heeeeee&\\3c&Heeeeee&\\4c&H000000&' .. '\\xbord0.5\\ybord0\\xshad0\\yshad1\\p4\\pbo24}' .. 'm 0 0 l 1 0 l 1 ' .. cheight .. ' l 0 ' .. cheight .. @@ -317,12 +418,13 @@ function update() local screeny_factor = (1 - global_margins.t - global_margins.b) local lines_max = math.ceil(screeny * screeny_factor / opts.font_size - 1.5) -- Estimate how many characters fit in one line - local width_max = math.ceil(screenx / opts.font_size * opts.font_hw_ratio) + 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 .. ass_escape(suggestions) + local suggestion_ass = style .. styles.suggestion .. suggestions local log_ass = '' + local log_buffer = log_buffers[id] local log_messages = #log_buffer local log_max_lines = math.max(0, lines_max - rows) if log_max_lines < log_messages then @@ -339,7 +441,7 @@ function update() if #suggestions > 0 then ass:append(suggestion_ass .. '\\N') end - ass:append(style .. '> ' .. before_cur) + ass:append(style .. ass_escape(prompt) .. ' ' .. before_cur) ass:append(cglyph) ass:append(style .. after_cur) @@ -348,7 +450,7 @@ function update() ass:new_event() ass:an(1) ass:pos(2, screeny - 2 - global_margins.b * screeny) - ass:append(style .. '{\\alpha&HFF&}> ' .. before_cur) + ass:append(style .. '{\\alpha&HFF&}' .. ass_escape(prompt) .. ' ' .. before_cur) ass:append(cglyph) ass:append(style .. '{\\alpha&HFF&}' .. after_cur) @@ -362,12 +464,28 @@ function set_active(active) repl_active = true insert_mode = false mp.enable_key_bindings('console-input', 'allow-hide-cursor+allow-vo-dragging') - mp.enable_messages('terminal-default') 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 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() end update() @@ -429,6 +547,16 @@ function len_utf8(str) 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) + end +end + -- Insert a character at the current cursor position (any_unicode) function handle_char_input(c) if insert_mode then @@ -437,8 +565,7 @@ function handle_char_input(c) line = line:sub(1, cursor - 1) .. c .. line:sub(cursor) end cursor = cursor + #c - suggestion_buffer = {} - update() + handle_edit() end -- Remove the character behind the cursor (Backspace) @@ -447,16 +574,14 @@ function handle_backspace() local prev = prev_utf8(line, cursor) line = line:sub(1, prev - 1) .. line:sub(cursor) cursor = prev - suggestion_buffer = {} - update() + handle_edit() end -- Remove the character in front of the cursor (Del) function handle_del() if cursor > line:len() then return end line = line:sub(1, cursor - 1) .. line:sub(next_utf8(line, cursor)) - suggestion_buffer = {} - update() + handle_edit() end -- Toggle insert mode (Ins) @@ -467,12 +592,14 @@ end -- Move the cursor to the next character (Right) function next_char(amount) cursor = next_utf8(line, cursor) + suggestion_buffer = {} update() end -- Move the cursor to the previous character (Left) function prev_char(amount) cursor = prev_utf8(line, cursor) + suggestion_buffer = {} update() end @@ -482,8 +609,7 @@ function clear() cursor = 1 insert_mode = false history_pos = #history + 1 - suggestion_buffer = {} - update() + handle_edit() end -- Close the REPL if the current line is empty, otherwise delete the next @@ -521,7 +647,8 @@ function help_command(param) end end if not cmd then - log_add(styles.error, 'No command matches "' .. param .. '"!') + log_add('No command matches "' .. param .. '"!\n', styles.error, + terminal_styles.error) return end output = output .. 'Command "' .. cmd.name .. '"\n' @@ -536,7 +663,7 @@ function help_command(param) output = output .. 'This command supports variable arguments.\n' end end - log_add('', output) + log_add(output) end -- Add a line to the history and deduplicate @@ -556,20 +683,25 @@ end -- Run the current command and clear the line (Enter) function handle_enter() - if line == '' then + if line == '' and input_caller == nil then return end - if history[#history] ~= line then + if history[#history] ~= line and line ~= '' then history_add(line) end - -- 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) + if input_caller then + mp.commandv('script-message-to', input_caller, 'input-event', 'submit', + line) else - mp.command(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) + else + mp.command(line) + end end clear() @@ -607,6 +739,7 @@ function go_history(new_pos) end cursor = line:len() + 1 insert_mode = false + suggestion_buffer = {} update() end @@ -632,6 +765,7 @@ function prev_word() -- 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. cursor = line:len() - select(2, line:reverse():find('%s*[^%s]*', line:len() - cursor + 2)) + 1 + suggestion_buffer = {} update() end @@ -639,6 +773,7 @@ end -- the next word. (Ctrl+Right) function next_word() cursor = select(2, line:find('%s*[^%s]*', cursor)) + 1 + suggestion_buffer = {} update() end @@ -659,19 +794,21 @@ local function command_list_and_help() end local function property_list() - local option_info = { - 'name', 'type', 'set-from-commandline', 'set-locally', 'default-value', - 'min', 'max', 'choices', - } - local properties = mp.get_property_native('property-list') + for _, sub_property in pairs({'video', 'audio', 'sub', 'sub2'}) do + properties[#properties + 1] = 'current-tracks/' .. sub_property + end + for _, option in ipairs(mp.get_property_native('options')) do properties[#properties + 1] = 'options/' .. option properties[#properties + 1] = 'file-local-options/' .. option properties[#properties + 1] = 'option-info/' .. option - for _, sub_property in ipairs(option_info) do + for _, sub_property in pairs({ + 'name', 'type', 'set-from-commandline', 'set-locally', + 'default-value', 'min', 'max', 'choices', + }) do properties[#properties + 1] = 'option-info/' .. option .. '/' .. sub_property end @@ -789,7 +926,7 @@ function build_completers() { 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 = '}' }, + { pattern = '${[=>]?()[%w_/-]*$', list = property_list, append = '}' }, } for _, command in pairs({'set', 'add', 'cycle', 'cycle[-_]values', 'multiply'}) do @@ -820,32 +957,6 @@ function build_completers() return completers end --- Use 'list' to find possible tab-completions for 'part.' --- Returns a list of all potential completions and the longest --- common prefix of all the matching list items. -function complete_match(part, list) - local completions = {} - local prefix = nil - - for _, candidate in ipairs(list) do - if candidate:sub(1, part:len()) == part then - if prefix and prefix ~= candidate then - local prefix_len = part:len() - while prefix:sub(1, prefix_len + 1) - == candidate:sub(1, prefix_len + 1) do - prefix_len = prefix_len + 1 - end - prefix = candidate:sub(1, prefix_len) - else - prefix = candidate - end - completions[#completions + 1] = candidate - end - end - - return completions, prefix -end - function common_prefix_length(s1, s2) local common_count = 0 for i = 1, #s1 do @@ -873,8 +984,101 @@ function max_overlap_length(s1, s2) return 0 end +-- If str starts with the first or last characters of prefix, strip them. +local function strip_common_characters(str, prefix) + return str:sub(1 + math.max( + common_prefix_length(prefix, str), + max_overlap_length(prefix, str))) +end + +-- Find the longest common case-sensitive prefix of the entries in "list". +local function find_common_prefix(list) + local prefix = list[1] + + for i = 2, #list do + prefix = prefix:sub(1, common_prefix_length(prefix, list[i])) + end + + return prefix +end + +-- Return the entries of "list" beginning with "part" and the longest common +-- prefix of the matches. +local function complete_match(part, list) + local completions = {} + + for _, candidate in pairs(list) do + if candidate:sub(1, part:len()) == part then + completions[#completions + 1] = candidate + end + end + + local prefix = find_common_prefix(completions) + + if opts.case_sensitive then + return completions, prefix + end + + completions = {} + local lower_case_completions = {} + local lower_case_part = part:lower() + + for _, candidate in pairs(list) do + if candidate:sub(1, part:len()):lower() == lower_case_part then + completions[#completions + 1] = candidate + lower_case_completions[#lower_case_completions + 1] = candidate:lower() + end + end + + local lower_case_prefix = find_common_prefix(lower_case_completions) + + -- Behave like GNU readline with completion-ignore-case On. + -- part = 'fooBA', completions = {'foobarbaz', 'fooBARqux'} => + -- prefix = 'fooBARqux', lower_case_prefix = 'foobar', return 'fooBAR' + if prefix then + return completions, prefix:sub(1, lower_case_prefix:len()) + end + + -- part = 'fooba', completions = {'fooBARbaz', 'fooBarqux'} => + -- prefix = nil, lower_case_prefix ='foobar', return 'fooBAR' + if lower_case_prefix then + return completions, completions[1]:sub(1, lower_case_prefix:len()) + end + + return {}, part +end + +local function cycle_through_suggestions(backwards) + selected_suggestion_index = selected_suggestion_index + (backwards and -1 or 1) + + if selected_suggestion_index > #suggestion_buffer then + selected_suggestion_index = 1 + elseif selected_suggestion_index < 1 then + selected_suggestion_index = #suggestion_buffer + end + + local before_cur = line:sub(1, completion_start_position - 1) .. + suggestion_buffer[selected_suggestion_index] .. completion_append + line = before_cur .. strip_common_characters(line:sub(cursor), completion_append) + cursor = before_cur:len() + 1 + update() +end + -- Complete the option or property at the cursor (TAB) -function complete() +function complete(backwards) + if #suggestion_buffer > 0 then + cycle_through_suggestions(backwards) + return + end + + if input_caller then + completion_old_line = line + completion_old_cursor = cursor + mp.commandv('script-message-to', input_caller, 'input-event', + 'complete', line:sub(1, cursor - 1)) + return + end + local before_cur = line:sub(1, cursor - 1) local after_cur = line:sub(cursor) @@ -882,47 +1086,56 @@ function complete() for _, completer in ipairs(build_completers()) do -- Completer patterns should return the start of the word to be -- completed as the first capture. - local s, s2 = before_cur:match(completer.pattern) - if not s then + 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. - s, s2 = before_cur:match(completer.pattern:gsub('^^', ';')) + completion_start_position, s2 = + before_cur:match(completer.pattern:gsub('^^', ';')) end - if s then + if completion_start_position then local hint if s2 then - hint = s - s = s2 + hint = completion_start_position + completion_start_position = s2 + 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 -- If the completer's pattern found a word, check the completer's -- list for possible completions - local part = before_cur:sub(s) + 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. - local after_cur_index = 1 + completion_append = completer.append or '' if #completions == 1 then - local append = completer.append or '' - prefix = prefix .. append - - -- calculate offset into after_cur - local prefix_len = common_prefix_length(append, after_cur) - local overlap_size = max_overlap_length(append, after_cur) - after_cur_index = math.max(prefix_len, overlap_size) + 1 + 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 -- Insert the completion and update - before_cur = before_cur:sub(1, s - 1) .. prefix + before_cur = before_cur:sub(1, completion_start_position - 1) .. + prefix cursor = before_cur:len() + 1 - line = before_cur .. after_cur:sub(after_cur_index) + line = before_cur .. after_cur update() return end @@ -933,12 +1146,14 @@ end -- Move the cursor to the beginning of the line (HOME) function go_home() cursor = 1 + suggestion_buffer = {} update() end -- Move the cursor to the end of the line (END) function go_end() cursor = line:len() + 1 + suggestion_buffer = {} update() end @@ -950,7 +1165,7 @@ function del_word() before_cur = before_cur:gsub('[^%s]+%s*$', '', 1) line = before_cur .. after_cur cursor = before_cur:len() + 1 - update() + handle_edit() end -- Delete from the cursor to the end of the word (Ctrl+Del) @@ -962,25 +1177,25 @@ function del_next_word() after_cur = after_cur:gsub('^%s*[^%s]+', '', 1) line = before_cur .. after_cur - update() + handle_edit() end -- Delete from the cursor to the end of the line (Ctrl+K) function del_to_eol() line = line:sub(1, cursor - 1) - update() + handle_edit() end -- Delete from the cursor back to the start of the line (Ctrl+U) function del_to_start() line = line:sub(cursor) cursor = 1 - update() + handle_edit() end -- Empty the log buffer of all messages (Ctrl+L) function clear_log_buffer() - log_buffer = {} + log_buffers[id] = {} update() end @@ -1047,7 +1262,7 @@ function paste(clip) local after_cur = line:sub(cursor) line = before_cur .. text .. after_cur cursor = cursor + text:len() - update() + handle_edit() end -- List of input bindings. This is a weird mashup between common GUI text-input @@ -1087,6 +1302,7 @@ function get_bindings() { 'alt+f', next_word }, { 'tab', complete }, { 'ctrl+i', complete }, + { 'shift+tab', function() complete(true) end }, { 'ctrl+a', go_home }, { 'home', go_home }, { 'ctrl+e', go_end }, @@ -1151,16 +1367,105 @@ mp.add_key_binding(nil, 'enable', function() set_active(true) end) +mp.register_script_message('disable', function() + set_active(false) +end) + -- Add a script-message to show the REPL and fill it with the provided text mp.register_script_message('type', function(text, cursor_pos) show_and_type(text, cursor_pos) end) +mp.register_script_message('get-input', function (script_name, args) + if repl_active then + return + end + + input_caller = script_name + 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 + id = args.id or script_name .. prompt + if histories[id] == nil then + histories[id] = {} + log_buffers[id] = {} + end + history = histories[id] + history_pos = #history + 1 + + set_active(true) + mp.commandv('script-message-to', input_caller, 'input-event', 'opened') +end) + +mp.register_script_message('log', function (message) + -- input.get's edited handler is invoked after submit, so avoid modifying + -- the default log. + if input_caller == nil then + return + end + + message = utils.parse_json(message) + + log_add(message.text .. '\n', + message.error and styles.error or message.style, + message.error and terminal_styles.error or message.terminal_style) +end) + +mp.register_script_message('set-log', function (log) + if input_caller == nil then + return + end + + log = utils.parse_json(log) + log_buffers[id] = {} + + for i = 1, #log do + if type(log[i]) == 'table' then + log[i].text = log[i].text .. '\n' + log[i].style = log[i].style or '' + log[i].terminal_style = log[i].terminal_style or '' + log_buffers[id][i] = log[i] + else + log_buffers[id][i] = { + text = log[i] .. '\n', + style = '', + terminal_style = '', + } + end + end + + update() +end) + +mp.register_script_message('complete', function(list, start_pos) + if line ~= completion_old_line or cursor ~= completion_old_cursor then + return + end + + local completions, prefix = complete_match(line:sub(start_pos, cursor), + utils.parse_json(list)) + local before_cur = line:sub(1, start_pos - 1) .. prefix + local after_cur = line:sub(cursor) + cursor = before_cur:len() + 1 + line = before_cur .. after_cur + + if #completions > 1 then + suggestion_buffer = completions + selected_suggestion_index = 0 + completion_start_position = start_pos + completion_append = '' + end + + update() +end) + -- Redraw the REPL when the OSD size changes. This is needed because the -- PlayRes of the OSD will need to be adjusted. mp.observe_property('osd-width', 'native', update) mp.observe_property('osd-height', 'native', update) mp.observe_property('display-hidpi-scale', 'native', update) +mp.observe_property('focused', 'native', update) -- Enable log messages. In silent mode, mpv will queue log messages in a buffer -- until enable_messages is called again without the silent: prefix. @@ -1185,20 +1490,8 @@ mp.register_event('log-message', function(e) if e.level == 'trace' then return end -- Use color for debug/v/warn/error/fatal messages. - local style = '' - if e.level == 'debug' then - style = styles.debug - elseif e.level == 'v' then - style = styles.verbose - elseif e.level == 'warn' then - style = styles.warn - elseif e.level == 'error' then - style = styles.error - elseif e.level == 'fatal' then - style = styles.fatal - end - - log_add(style, '[' .. e.prefix .. '] ' .. e.text) + log_add('[' .. e.prefix .. '] ' .. e.text, styles[e.level], + terminal_styles[e.level]) end) collectgarbage() |