summaryrefslogtreecommitdiffstats
path: root/player/lua/console.lua
diff options
context:
space:
mode:
Diffstat (limited to 'player/lua/console.lua')
-rw-r--r--player/lua/console.lua1386
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,