-- Copyright (C) 2019 the mpv developers---- Permission to use, copy, modify, and/or distribute this software for any-- purpose with or without fee is hereby granted, provided that the above-- copyright notice and this permission notice appear in all copies.---- THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES-- WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF-- MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY-- SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES-- WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION-- OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN-- CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. local utils = require 'mp.utils'local assdraw = require 'mp.assdraw' -- Default optionslocal 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 platform = mp.get_property_native('platform') if platform == 'darwin' or platform == 'windows' then return platform elseif os.getenv('WAYLAND_DISPLAY') then return 'wayland' end return 'x11'end -- Pick a better default font for Windows and macOSlocal platform = detect_platform()if platform == 'windows' then opts.font = 'Consolas' opts.case_sensitive = falseelseif platform == 'darwin' then opts.font = 'Menlo'else opts.font = 'monospace'end -- Apply user-set optionsrequire 'mp.options'.read_options(opts) local styles = { -- Colors are stolen from base16 Eighties by Chris Kempson -- and converted to BGR as is required by ASS. -- 2d2d2d 393939 515151 697374 -- 939fa0 c8d0d3 dfe6e8 ecf0f2 -- 7a77f2 5791f9 66ccff 99cc99 -- cccc66 cc9966 cc99cc 537bd2 debug = '{\\1c&Ha09f93&}', v = '{\\1c&H99cc99&}', warn = '{\\1c&H66ccff&}', error = '{\\1c&H7a77f2&}', fatal = '{\\1c&H5791f9&\\b1}', suggestion = '{\\1c&Hcc99cc&}', selected_suggestion = '{\\1c&H2fbdfa&\\b1}', disabled = '{\\1c&Hcccccc&}',} local terminal_styles = { debug = '\027[1;30m', v = '\027[32m', warn = '\027[33m', error = '\027[31m', fatal = '\027[1;31m', selected_suggestion = '\027[7m', disabled = '\027[38;5;8m',} local repl_active = falselocal insert_mode = falselocal pending_update = falselocal line = ''local cursor = 1local default_prompt = '>'local prompt = default_promptlocal default_id = 'default'local id = default_idlocal histories = {[id] = {}}local history = histories[id]local history_pos = 1local log_buffers = {[id] = {}}local key_bindings = {}local global_margins = { t = 0, b = 0 }local input_caller local suggestion_buffer = {}local selected_suggestion_indexlocal completion_start_positionlocal completion_appendlocal file_commands = {}local path_separator = platform == 'windows' and '\\' or '/'local completion_old_linelocal completion_old_cursor local selectable_itemslocal matches = {}local selected_match = 1local first_match_to_print = 1 local update_timer = nilupdate_timer = mp.add_periodic_timer(0.05, function() if pending_update then update() else update_timer:kill() endend)update_timer:kill() 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) 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 endend -- 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) end if repl_active then if not update_timer:is_enabled() then update() update_timer:resume() else pending_update = true end endend -- Escape a string for verbatim display on the OSDfunction 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 -- Subtract 1.5 to account for the input line. 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 - 1.5)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 countfunction format_table(list, width_max, rows_max) if #list == 0 then return '', 0 end local spaces_min = 2 local spaces_max = 8 local list_size = #list local column_count = 1 local row_count = list_size local column_widths -- total width without spacing local width_total = 0 local list_widths = {} for i, item in ipairs(list) do list_widths[i] = len_utf8(item) end -- 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)) for rows = rows_upper_bound, rows_lower_bound, -1 do cw = {} width_total = 0 -- find out width of each column for column = 1, columns do local width = 0 for row = 1, rows do local i = row + (column - 1) * rows local item_width = list_widths[i] if not item_width then break end if width < item_width then width = item_width end end cw[column] = width width_total = width_total + width if width_total + (columns - 1) * spaces_min > width_max then break end end if width_total + (columns - 1) * spaces_min <= width_max then row_count = rows column_count = columns column_widths = cw else break end end if width_total + (columns - 1) * spaces_min > width_max then break end end 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 ass_escape(string.format('%' .. spaces .. 's', ' ')) or '' local rows = {} for row = 1, row_count do local columns = {} for column = 1, column_count do local i = row + (column - 1) * row_count if i > #list then break end -- 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] = 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, ass_escape('\n')), row_countend 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 resultend local function populate_log_with_matches() if not selectable_items then return end log_buffers[id] = {} local log = log_buffers[id] -- Subtract 2 for the "(n hidden items)" lines. local max_log_lines = calculate_max_log_lines() - 2 if selected_match < first_match_to_print then first_match_to_print = selected_match elseif selected_match > first_match_to_print + max_log_lines - 1 then first_match_to_print = selected_match - max_log_lines + 1 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 local last_match_to_print = math.min(first_match_to_print + max_log_lines - 1, #matches) for i = first_match_to_print, last_match_to_print do log[#log + 1] = { text = matches[i].text .. '\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, } endend local function print_to_terminal() -- Clear the log after closing the console. if not repl_active then mp.osd_message('') return end populate_log_with_matches() local log = '' for _, log_line in ipairs(log_buffers[id]) do log = log .. log_line.terminal_style .. log_line.text .. '\027[0m' end 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) local after_cur = line:sub(cursor) -- Ensure there is a character with inverted colors to print. if after_cur == '' then after_cur = ' ' end 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 OSDfunction 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 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, '') return end 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 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' .. '\\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 -- of the drawing. So the cursor doesn't affect layout too much, make it as -- 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 = '{\\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 .. '{\\p0}' local before_cur = ass_escape(line:sub(1, cursor - 1)) local after_cur = ass_escape(line:sub(cursor)) -- Render log messages as ASS. -- This will render at most screeny / font_size - 1 messages. 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())Lines 1–500 of 1629. Load more
-- Copyright (C) 2019 the mpv developers -- -- Permission to use, copy, modify, and/or distribute this software for any -- purpose with or without fee is hereby granted, provided that the above -- copyright notice and this permission notice appear in all copies. -- -- THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES -- WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF -- MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY -- SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES -- WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION -- OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN -- CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. local utils = require 'mp.utils' 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 platform = mp.get_property_native('platform') if platform == 'darwin' or platform == 'windows' then return platform elseif os.getenv('WAYLAND_DISPLAY') then return 'wayland' end return 'x11' end -- Pick a better default font for Windows and macOS local platform = detect_platform() if platform == 'windows' then opts.font = 'Consolas' opts.case_sensitive = false elseif platform == 'darwin' then opts.font = 'Menlo' else opts.font = 'monospace' end -- Apply user-set options require 'mp.options'.read_options(opts) local styles = { -- Colors are stolen from base16 Eighties by Chris Kempson -- and converted to BGR as is required by ASS. -- 2d2d2d 393939 515151 697374 -- 939fa0 c8d0d3 dfe6e8 ecf0f2 -- 7a77f2 5791f9 66ccff 99cc99 -- cccc66 cc9966 cc99cc 537bd2 debug = '{\\1c&Ha09f93&}', v = '{\\1c&H99cc99&}', warn = '{\\1c&H66ccff&}', error = '{\\1c&H7a77f2&}', fatal = '{\\1c&H5791f9&\\b1}', suggestion = '{\\1c&Hcc99cc&}', selected_suggestion = '{\\1c&H2fbdfa&\\b1}', disabled = '{\\1c&Hcccccc&}', } local terminal_styles = { debug = '\027[1;30m', v = '\027[32m', warn = '\027[33m', error = '\027[31m', fatal = '\027[1;31m', selected_suggestion = '\027[7m', disabled = '\027[38;5;8m', } local repl_active = false local insert_mode = false local pending_update = false local line = '' local cursor = 1 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_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 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 update() else update_timer:kill() end end) update_timer:kill() 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) 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(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 if repl_active then if not update_timer:is_enabled() then update() update_timer:resume() else pending_update = true end end end -- Escape a string for verbatim display on the OSD 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 -- Subtract 1.5 to account for the input line. 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 - 1.5) 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) if #list == 0 then return '', 0 end local spaces_min = 2 local spaces_max = 8 local list_size = #list local column_count = 1 local row_count = list_size local column_widths -- total width without spacing local width_total = 0 local list_widths = {} for i, item in ipairs(list) do list_widths[i] = len_utf8(item) end -- 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)) for rows = rows_upper_bound, rows_lower_bound, -1 do cw = {} width_total = 0 -- find out width of each column for column = 1, columns do local width = 0 for row = 1, rows do local i = row + (column - 1) * rows local item_width = list_widths[i] if not item_width then break end if width < item_width then width = item_width end end cw[column] = width width_total = width_total + width if width_total + (columns - 1) * spaces_min > width_max then break end end if width_total + (columns - 1) * spaces_min <= width_max then row_count = rows column_count = columns column_widths = cw else break end end if width_total + (columns - 1) * spaces_min > width_max then break end end 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 ass_escape(string.format('%' .. spaces .. 's', ' ')) or '' local rows = {} for row = 1, row_count do local columns = {} for column = 1, column_count do local i = row + (column - 1) * row_count if i > #list then break end -- 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] = 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, 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() if not selectable_items then return end log_buffers[id] = {} local log = log_buffers[id] -- Subtract 2 for the "(n hidden items)" lines. local max_log_lines = calculate_max_log_lines() - 2 if selected_match < first_match_to_print then first_match_to_print = selected_match elseif selected_match > first_match_to_print + max_log_lines - 1 then first_match_to_print = selected_match - max_log_lines + 1 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 local last_match_to_print = math.min(first_match_to_print + max_log_lines - 1, #matches) for i = first_match_to_print, last_match_to_print do log[#log + 1] = { text = matches[i].text .. '\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 mp.osd_message('') return end populate_log_with_matches() local log = '' for _, log_line in ipairs(log_buffers[id]) do log = log .. log_line.terminal_style .. log_line.text .. '\027[0m' end 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) local after_cur = line:sub(cursor) -- Ensure there is a character with inverted colors to print. if after_cur == '' then after_cur = ' ' end 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 -- 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 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, '') return end 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 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' .. '\\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 -- of the drawing. So the cursor doesn't affect layout too much, make it as -- 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 = '{\\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 .. '{\\p0}' local before_cur = ass_escape(line:sub(1, cursor - 1)) local after_cur = ass_escape(line:sub(cursor)) -- Render log messages as ASS. -- This will render at most screeny / font_size - 1 messages. 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())