mirror of
https://github.com/mpv-player/mpv
synced 2025-03-20 02:09:52 +00:00
scripting: add mp.input.select()
This allows scripts to make the user choose from a list of entries by typing part of their text and/or by navigating them with keybindings, like dmenu or fzf. Closes #13964.
This commit is contained in:
parent
43b702d707
commit
2354d876da
1
DOCS/interface-changes/input-select.txt
Normal file
1
DOCS/interface-changes/input-select.txt
Normal file
@ -0,0 +1 @@
|
||||
`add mp.input.select()`
|
@ -196,7 +196,9 @@ meta-paths like ``~~/foo`` (other JS file functions do expand meta paths).
|
||||
``mp.options.read_options(obj [, identifier [, on_update]])`` (types:
|
||||
string/boolean/number)
|
||||
|
||||
``mp.input.get(obj)`` (LE)
|
||||
``mp.input.get(obj)``
|
||||
|
||||
``mp.input.select(obj)``
|
||||
|
||||
``mp.input.terminate()``
|
||||
|
||||
|
@ -888,9 +888,8 @@ REPL.
|
||||
present a list of options with ``input.set_log()``.
|
||||
|
||||
``edited``
|
||||
A callback invoked when the text changes. This can be used to filter a
|
||||
list of options based on what the user typed with ``input.set_log()``,
|
||||
like dmenu does. The first argument is the text in the console.
|
||||
A callback invoked when the text changes. The first argument is the text
|
||||
in the console.
|
||||
|
||||
``complete``
|
||||
A callback invoked when the user presses TAB. The first argument is the
|
||||
@ -951,6 +950,44 @@ REPL.
|
||||
}
|
||||
})
|
||||
|
||||
``input.select(table)``
|
||||
Specify a list of items that are presented to the user for selection. The
|
||||
user can type part of the desired item and/or navigate them with
|
||||
keybindings: ``Down`` and ``Ctrl+n`` go down, ``Up`` and ``Ctrl+p`` go up,
|
||||
``Page down`` and ``Ctrl+f`` scroll down one page, and ``Page up`` and
|
||||
``Ctrl+b`` scroll up one page.
|
||||
|
||||
The following entries of ``table`` are read:
|
||||
|
||||
``prompt``
|
||||
The string to be displayed before the input field.
|
||||
|
||||
``items``
|
||||
The table of the entries to choose from.
|
||||
|
||||
``default_item``
|
||||
The 1-based integer index of the preselected item.
|
||||
|
||||
``submit``
|
||||
The callback invoked when the user presses Enter. The first argument is
|
||||
the 1-based index of the selected item. You can close the console from
|
||||
within the callback by calling ``input.terminate()``.
|
||||
|
||||
Example:
|
||||
|
||||
::
|
||||
|
||||
input.select({
|
||||
items = {
|
||||
"First playlist entry",
|
||||
"Second playlist entry",
|
||||
},
|
||||
submit = function (id)
|
||||
mp.commandv("playlist-play-index", id - 1)
|
||||
input.terminate()
|
||||
end,
|
||||
})
|
||||
|
||||
Events
|
||||
------
|
||||
|
||||
|
@ -646,6 +646,22 @@ mp.options = { read_options: read_options };
|
||||
/**********************************************************************
|
||||
* input
|
||||
*********************************************************************/
|
||||
function register_event_handler(t) {
|
||||
mp.register_script_message("input-event", function (type, text, cursor_position) {
|
||||
if (t[type]) {
|
||||
var result = t[type](text, cursor_position);
|
||||
|
||||
if (type == "complete" && result) {
|
||||
mp.commandv("script-message-to", "console", "complete",
|
||||
JSON.stringify(result[0]), result[1]);
|
||||
}
|
||||
}
|
||||
|
||||
if (type == "closed")
|
||||
mp.unregister_script_message("input-event");
|
||||
})
|
||||
}
|
||||
|
||||
mp.input = {
|
||||
get: function(t) {
|
||||
mp.commandv("script-message-to", "console", "get-input", mp.script_name,
|
||||
@ -656,23 +672,18 @@ mp.input = {
|
||||
id: t.id,
|
||||
}));
|
||||
|
||||
mp.register_script_message("input-event", function (type, text, cursor_position) {
|
||||
if (t[type]) {
|
||||
var result = t[type](text, cursor_position);
|
||||
|
||||
if (type == "complete" && result) {
|
||||
mp.commandv("script-message-to", "console", "complete",
|
||||
JSON.stringify(result[0]), result[1]);
|
||||
}
|
||||
}
|
||||
|
||||
if (type == "closed") {
|
||||
mp.unregister_script_message("input-event");
|
||||
}
|
||||
})
|
||||
|
||||
return true;
|
||||
register_event_handler(t)
|
||||
},
|
||||
select: function () {
|
||||
mp.commandv("script-message-to", "console", "get-input", mp.script_name,
|
||||
JSON.stringify({
|
||||
prompt: t.prompt,
|
||||
items: t.items,
|
||||
default_item: t.default_item,
|
||||
}));
|
||||
|
||||
register_event_handler(t)
|
||||
}
|
||||
terminate: function () {
|
||||
mp.commandv("script-message-to", "console", "disable");
|
||||
},
|
||||
|
@ -57,6 +57,9 @@ static const char * const builtin_lua_scripts[][2] = {
|
||||
},
|
||||
{"mp.assdraw",
|
||||
# include "player/lua/assdraw.lua.inc"
|
||||
},
|
||||
{"mp.fzy",
|
||||
# include "player/lua/fzy.lua.inc"
|
||||
},
|
||||
{"mp.input",
|
||||
# include "player/lua/input.lua.inc"
|
||||
|
@ -75,6 +75,7 @@ local styles = {
|
||||
fatal = '{\\1c&H5791f9&\\b1}',
|
||||
suggestion = '{\\1c&Hcc99cc&}',
|
||||
selected_suggestion = '{\\1c&H2fbdfa&\\b1}',
|
||||
disabled = '{\\1c&Hcccccc&}',
|
||||
}
|
||||
|
||||
local terminal_styles = {
|
||||
@ -84,6 +85,7 @@ local terminal_styles = {
|
||||
error = '\027[31m',
|
||||
fatal = '\027[1;31m',
|
||||
selected_suggestion = '\027[7m',
|
||||
disabled = '\027[38;5;8m',
|
||||
}
|
||||
|
||||
local repl_active = false
|
||||
@ -112,6 +114,11 @@ 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
|
||||
@ -232,6 +239,23 @@ 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.
|
||||
@ -323,6 +347,62 @@ function format_table(list, width_max, rows_max)
|
||||
return table.concat(rows, ass_escape('\n')), row_count
|
||||
end
|
||||
|
||||
local function fuzzy_find(needle, haystacks)
|
||||
local result = require 'mp.fzy'.filter(needle, haystacks)
|
||||
table.sort(result, function (i, j)
|
||||
return i[3] > j[3]
|
||||
end)
|
||||
for i, value in ipairs(result) do
|
||||
result[i] = value[1]
|
||||
end
|
||||
return result
|
||||
end
|
||||
|
||||
local function populate_log_with_matches()
|
||||
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
|
||||
@ -330,6 +410,8 @@ local function print_to_terminal()
|
||||
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'
|
||||
@ -413,16 +495,15 @@ function update()
|
||||
-- Render log messages as ASS.
|
||||
-- This will render at most screeny / font_size - 1 messages.
|
||||
|
||||
-- lines above the prompt
|
||||
-- subtract 1.5 to account for the input line
|
||||
local screeny_factor = (1 - global_margins.t - global_margins.b)
|
||||
local lines_max = math.ceil(screeny * screeny_factor / opts.font_size - 1.5)
|
||||
local lines_max = calculate_max_log_lines()
|
||||
-- Estimate how many characters fit in one line
|
||||
local width_max = math.ceil(screenx / opts.font_size * get_font_hw_ratio())
|
||||
|
||||
local suggestions, rows = format_table(suggestion_buffer, width_max, lines_max)
|
||||
local suggestion_ass = style .. styles.suggestion .. suggestions
|
||||
|
||||
populate_log_with_matches()
|
||||
|
||||
local log_ass = ''
|
||||
local log_buffer = log_buffers[id]
|
||||
local log_messages = #log_buffer
|
||||
@ -485,6 +566,7 @@ function set_active(active)
|
||||
input_caller = nil
|
||||
line = ''
|
||||
cursor = 1
|
||||
selectable_items = nil
|
||||
end
|
||||
collectgarbage()
|
||||
end
|
||||
@ -548,6 +630,15 @@ function len_utf8(str)
|
||||
end
|
||||
|
||||
local function handle_edit()
|
||||
if selectable_items then
|
||||
matches = {}
|
||||
selected_match = 1
|
||||
|
||||
for i, match in ipairs(fuzzy_find(line, selectable_items)) do
|
||||
matches[i] = { index = match, text = selectable_items[match] }
|
||||
end
|
||||
end
|
||||
|
||||
suggestion_buffer = {}
|
||||
update()
|
||||
|
||||
@ -692,7 +783,7 @@ function handle_enter()
|
||||
|
||||
if input_caller then
|
||||
mp.commandv('script-message-to', input_caller, 'input-event', 'submit',
|
||||
line)
|
||||
selectable_items and matches[selected_match].index or line)
|
||||
else
|
||||
-- match "help [<text>]", return <text> or "", strip all whitespace
|
||||
local help = line:match('^%s*help%s+(.-)%s*$') or
|
||||
@ -745,19 +836,50 @@ end
|
||||
|
||||
-- Go to the specified relative position in the command history (Up, Down)
|
||||
function move_history(amount)
|
||||
if selectable_items then
|
||||
selected_match = selected_match + amount
|
||||
if selected_match > #matches then
|
||||
selected_match = 1
|
||||
elseif selected_match < 1 then
|
||||
selected_match = #matches
|
||||
end
|
||||
update()
|
||||
return
|
||||
end
|
||||
|
||||
go_history(history_pos + amount)
|
||||
end
|
||||
|
||||
-- Go to the first command in the command history (PgUp)
|
||||
function handle_pgup()
|
||||
if selectable_items then
|
||||
selected_match = math.max(selected_match - calculate_max_log_lines() + 1, 1)
|
||||
update()
|
||||
return
|
||||
end
|
||||
|
||||
go_history(1)
|
||||
end
|
||||
|
||||
-- Stop browsing history and start editing a blank line (PgDown)
|
||||
function handle_pgdown()
|
||||
if selectable_items then
|
||||
selected_match = math.min(selected_match + calculate_max_log_lines() - 1, #matches)
|
||||
update()
|
||||
return
|
||||
end
|
||||
|
||||
go_history(#history + 1)
|
||||
end
|
||||
|
||||
local function page_up_or_prev_char()
|
||||
return selectable_items and handle_pgup() or prev_char()
|
||||
end
|
||||
|
||||
local function page_down_or_next_char()
|
||||
return selectable_items and handle_pgdown() or next_char()
|
||||
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()
|
||||
@ -1285,9 +1407,9 @@ function get_bindings()
|
||||
{ 'shift+ins', function() paste(false) end },
|
||||
{ 'mbtn_mid', function() paste(false) end },
|
||||
{ 'left', function() prev_char() end },
|
||||
{ 'ctrl+b', function() prev_char() end },
|
||||
{ 'ctrl+b', function() page_up_or_prev_char() end },
|
||||
{ 'right', function() next_char() end },
|
||||
{ 'ctrl+f', function() next_char() end },
|
||||
{ 'ctrl+f', function() page_down_or_next_char() end},
|
||||
{ 'up', function() move_history(-1) end },
|
||||
{ 'ctrl+p', function() move_history(-1) end },
|
||||
{ 'wheel_up', function() move_history(-1) end },
|
||||
@ -1394,6 +1516,16 @@ mp.register_script_message('get-input', function (script_name, args)
|
||||
history = histories[id]
|
||||
history_pos = #history + 1
|
||||
|
||||
selectable_items = args.items
|
||||
if selectable_items then
|
||||
matches = {}
|
||||
selected_match = args.default_item or 1
|
||||
first_match_to_print = 1
|
||||
for i, item in ipairs(selectable_items) do
|
||||
matches[i] = { index = i, text = item }
|
||||
end
|
||||
end
|
||||
|
||||
set_active(true)
|
||||
mp.commandv('script-message-to', input_caller, 'input-event', 'opened')
|
||||
end)
|
||||
|
297
player/lua/fzy.lua
Normal file
297
player/lua/fzy.lua
Normal file
@ -0,0 +1,297 @@
|
||||
--[[ The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2020 Seth Warn
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE. ]]
|
||||
|
||||
-- The lua implementation of the fzy string matching algorithm
|
||||
|
||||
local SCORE_GAP_LEADING = -0.005
|
||||
local SCORE_GAP_TRAILING = -0.005
|
||||
local SCORE_GAP_INNER = -0.01
|
||||
local SCORE_MATCH_CONSECUTIVE = 1.0
|
||||
local SCORE_MATCH_SLASH = 0.9
|
||||
local SCORE_MATCH_WORD = 0.8
|
||||
local SCORE_MATCH_CAPITAL = 0.7
|
||||
local SCORE_MATCH_DOT = 0.6
|
||||
local SCORE_MAX = math.huge
|
||||
local SCORE_MIN = -math.huge
|
||||
local MATCH_MAX_LENGTH = 1024
|
||||
|
||||
local fzy = {}
|
||||
|
||||
-- Check if `needle` is a subsequence of the `haystack`.
|
||||
--
|
||||
-- Usually called before `score` or `positions`.
|
||||
--
|
||||
-- Args:
|
||||
-- needle (string)
|
||||
-- haystack (string)
|
||||
-- case_sensitive (bool, optional): defaults to false
|
||||
--
|
||||
-- Returns:
|
||||
-- bool
|
||||
function fzy.has_match(needle, haystack, case_sensitive)
|
||||
if not case_sensitive then
|
||||
needle = string.lower(needle)
|
||||
haystack = string.lower(haystack)
|
||||
end
|
||||
|
||||
local j = 1
|
||||
for i = 1, string.len(needle) do
|
||||
j = string.find(haystack, needle:sub(i, i), j, true)
|
||||
if not j then
|
||||
return false
|
||||
else
|
||||
j = j + 1
|
||||
end
|
||||
end
|
||||
|
||||
return true
|
||||
end
|
||||
|
||||
local function is_lower(c)
|
||||
return c:match("%l")
|
||||
end
|
||||
|
||||
local function is_upper(c)
|
||||
return c:match("%u")
|
||||
end
|
||||
|
||||
local function precompute_bonus(haystack)
|
||||
local match_bonus = {}
|
||||
|
||||
local last_char = "/"
|
||||
for i = 1, string.len(haystack) do
|
||||
local this_char = haystack:sub(i, i)
|
||||
if last_char == "/" or last_char == "\\" then
|
||||
match_bonus[i] = SCORE_MATCH_SLASH
|
||||
elseif last_char == "-" or last_char == "_" or last_char == " " then
|
||||
match_bonus[i] = SCORE_MATCH_WORD
|
||||
elseif last_char == "." then
|
||||
match_bonus[i] = SCORE_MATCH_DOT
|
||||
elseif is_lower(last_char) and is_upper(this_char) then
|
||||
match_bonus[i] = SCORE_MATCH_CAPITAL
|
||||
else
|
||||
match_bonus[i] = 0
|
||||
end
|
||||
|
||||
last_char = this_char
|
||||
end
|
||||
|
||||
return match_bonus
|
||||
end
|
||||
|
||||
local function compute(needle, haystack, D, M, case_sensitive)
|
||||
-- Note that the match bonuses must be computed before the arguments are
|
||||
-- converted to lowercase, since there are bonuses for camelCase.
|
||||
local match_bonus = precompute_bonus(haystack)
|
||||
local n = string.len(needle)
|
||||
local m = string.len(haystack)
|
||||
|
||||
if not case_sensitive then
|
||||
needle = string.lower(needle)
|
||||
haystack = string.lower(haystack)
|
||||
end
|
||||
|
||||
-- Because lua only grants access to chars through substring extraction,
|
||||
-- get all the characters from the haystack once now, to reuse below.
|
||||
local haystack_chars = {}
|
||||
for i = 1, m do
|
||||
haystack_chars[i] = haystack:sub(i, i)
|
||||
end
|
||||
|
||||
for i = 1, n do
|
||||
D[i] = {}
|
||||
M[i] = {}
|
||||
|
||||
local prev_score = SCORE_MIN
|
||||
local gap_score = i == n and SCORE_GAP_TRAILING or SCORE_GAP_INNER
|
||||
local needle_char = needle:sub(i, i)
|
||||
|
||||
for j = 1, m do
|
||||
if needle_char == haystack_chars[j] then
|
||||
local score = SCORE_MIN
|
||||
if i == 1 then
|
||||
score = ((j - 1) * SCORE_GAP_LEADING) + match_bonus[j]
|
||||
elseif j > 1 then
|
||||
local a = M[i - 1][j - 1] + match_bonus[j]
|
||||
local b = D[i - 1][j - 1] + SCORE_MATCH_CONSECUTIVE
|
||||
score = math.max(a, b)
|
||||
end
|
||||
D[i][j] = score
|
||||
prev_score = math.max(score, prev_score + gap_score)
|
||||
M[i][j] = prev_score
|
||||
else
|
||||
D[i][j] = SCORE_MIN
|
||||
prev_score = prev_score + gap_score
|
||||
M[i][j] = prev_score
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- Compute a matching score.
|
||||
--
|
||||
-- Args:
|
||||
-- needle (string): must be a subequence of `haystack`, or the result is
|
||||
-- undefined.
|
||||
-- haystack (string)
|
||||
-- case_sensitive (bool, optional): defaults to false
|
||||
--
|
||||
-- Returns:
|
||||
-- number: higher scores indicate better matches. See also `get_score_min`
|
||||
-- and `get_score_max`.
|
||||
function fzy.score(needle, haystack, case_sensitive)
|
||||
local n = string.len(needle)
|
||||
local m = string.len(haystack)
|
||||
|
||||
if n == 0 or m == 0 or m > MATCH_MAX_LENGTH or n > m then
|
||||
return SCORE_MIN
|
||||
elseif n == m then
|
||||
return SCORE_MAX
|
||||
else
|
||||
local D = {}
|
||||
local M = {}
|
||||
compute(needle, haystack, D, M, case_sensitive)
|
||||
return M[n][m]
|
||||
end
|
||||
end
|
||||
|
||||
-- Compute the locations where fzy matches a string.
|
||||
--
|
||||
-- Determine where each character of the `needle` is matched to the `haystack`
|
||||
-- in the optimal match.
|
||||
--
|
||||
-- Args:
|
||||
-- needle (string): must be a subequence of `haystack`, or the result is
|
||||
-- undefined.
|
||||
-- haystack (string)
|
||||
-- case_sensitive (bool, optional): defaults to false
|
||||
--
|
||||
-- Returns:
|
||||
-- {int,...}: indices, where `indices[n]` is the location of the `n`th
|
||||
-- character of `needle` in `haystack`.
|
||||
-- number: the same matching score returned by `score`
|
||||
function fzy.positions(needle, haystack, case_sensitive)
|
||||
local n = string.len(needle)
|
||||
local m = string.len(haystack)
|
||||
|
||||
if n == 0 or m == 0 or m > MATCH_MAX_LENGTH or n > m then
|
||||
return {}, SCORE_MIN
|
||||
elseif n == m then
|
||||
local consecutive = {}
|
||||
for i = 1, n do
|
||||
consecutive[i] = i
|
||||
end
|
||||
return consecutive, SCORE_MAX
|
||||
end
|
||||
|
||||
local D = {}
|
||||
local M = {}
|
||||
compute(needle, haystack, D, M, case_sensitive)
|
||||
|
||||
local positions = {}
|
||||
local match_required = false
|
||||
local j = m
|
||||
for i = n, 1, -1 do
|
||||
while j >= 1 do
|
||||
if D[i][j] ~= SCORE_MIN and (match_required or D[i][j] == M[i][j]) then
|
||||
match_required = (i ~= 1) and (j ~= 1) and (
|
||||
M[i][j] == D[i - 1][j - 1] + SCORE_MATCH_CONSECUTIVE)
|
||||
positions[i] = j
|
||||
j = j - 1
|
||||
break
|
||||
else
|
||||
j = j - 1
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return positions, M[n][m]
|
||||
end
|
||||
|
||||
-- Apply `has_match` and `positions` to an array of haystacks.
|
||||
--
|
||||
-- Args:
|
||||
-- needle (string)
|
||||
-- haystack ({string, ...})
|
||||
-- case_sensitive (bool, optional): defaults to false
|
||||
--
|
||||
-- Returns:
|
||||
-- {{idx, positions, score}, ...}: an array with one entry per matching line
|
||||
-- in `haystacks`, each entry giving the index of the line in `haystacks`
|
||||
-- as well as the equivalent to the return value of `positions` for that
|
||||
-- line.
|
||||
function fzy.filter(needle, haystacks, case_sensitive)
|
||||
local result = {}
|
||||
|
||||
for i, line in ipairs(haystacks) do
|
||||
if fzy.has_match(needle, line, case_sensitive) then
|
||||
local p, s = fzy.positions(needle, line, case_sensitive)
|
||||
table.insert(result, {i, p, s})
|
||||
end
|
||||
end
|
||||
|
||||
return result
|
||||
end
|
||||
|
||||
-- The lowest value returned by `score`.
|
||||
--
|
||||
-- In two special cases:
|
||||
-- - an empty `needle`, or
|
||||
-- - a `needle` or `haystack` larger than than `get_max_length`,
|
||||
-- the `score` function will return this exact value, which can be used as a
|
||||
-- sentinel. This is the lowest possible score.
|
||||
function fzy.get_score_min()
|
||||
return SCORE_MIN
|
||||
end
|
||||
|
||||
-- The score returned for exact matches. This is the highest possible score.
|
||||
function fzy.get_score_max()
|
||||
return SCORE_MAX
|
||||
end
|
||||
|
||||
-- The maximum size for which `fzy` will evaluate scores.
|
||||
function fzy.get_max_length()
|
||||
return MATCH_MAX_LENGTH
|
||||
end
|
||||
|
||||
-- The minimum score returned for normal matches.
|
||||
--
|
||||
-- For matches that don't return `get_score_min`, their score will be greater
|
||||
-- than than this value.
|
||||
function fzy.get_score_floor()
|
||||
return MATCH_MAX_LENGTH * SCORE_GAP_INNER
|
||||
end
|
||||
|
||||
-- The maximum score for non-exact matches.
|
||||
--
|
||||
-- For matches that don't return `get_score_max`, their score will be less than
|
||||
-- this value.
|
||||
function fzy.get_score_ceiling()
|
||||
return MATCH_MAX_LENGTH * SCORE_MATCH_CONSECUTIVE
|
||||
end
|
||||
|
||||
-- The name of the currently-running implmenetation, "lua" or "native".
|
||||
function fzy.get_implementation_name()
|
||||
return "lua"
|
||||
end
|
||||
|
||||
return fzy
|
@ -18,15 +18,7 @@ License along with mpv. If not, see <http://www.gnu.org/licenses/>.
|
||||
local utils = require "mp.utils"
|
||||
local input = {}
|
||||
|
||||
function input.get(t)
|
||||
mp.commandv("script-message-to", "console", "get-input",
|
||||
mp.get_script_name(), utils.format_json({
|
||||
prompt = t.prompt,
|
||||
default_text = t.default_text,
|
||||
cursor_position = t.cursor_position,
|
||||
id = t.id,
|
||||
}))
|
||||
|
||||
local function register_event_handler(t)
|
||||
mp.register_script_message("input-event", function (type, text, cursor_position)
|
||||
if t[type] then
|
||||
local suggestions, completion_start_position = t[type](text, cursor_position)
|
||||
@ -41,8 +33,29 @@ function input.get(t)
|
||||
mp.unregister_script_message("input-event")
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
return true
|
||||
function input.get(t)
|
||||
mp.commandv("script-message-to", "console", "get-input",
|
||||
mp.get_script_name(), utils.format_json({
|
||||
prompt = t.prompt,
|
||||
default_text = t.default_text,
|
||||
cursor_position = t.cursor_position,
|
||||
id = t.id,
|
||||
}))
|
||||
|
||||
register_event_handler(t)
|
||||
end
|
||||
|
||||
function input.select(t)
|
||||
mp.commandv("script-message-to", "console", "get-input",
|
||||
mp.get_script_name(), utils.format_json({
|
||||
prompt = t.prompt,
|
||||
items = t.items,
|
||||
default_item = t.default_item,
|
||||
}))
|
||||
|
||||
register_event_handler(t)
|
||||
end
|
||||
|
||||
function input.terminate()
|
||||
|
@ -1,6 +1,6 @@
|
||||
lua_files = ['defaults.lua', 'assdraw.lua', 'options.lua', 'osc.lua',
|
||||
'ytdl_hook.lua', 'stats.lua', 'console.lua', 'auto_profiles.lua',
|
||||
'input.lua']
|
||||
'input.lua', 'fzy.lua']
|
||||
foreach file: lua_files
|
||||
lua_file = custom_target(file,
|
||||
input: join_paths(source_root, 'player', 'lua', file),
|
||||
|
Loading…
Reference in New Issue
Block a user