diff options
| author | A Farzat <a@farzat.xyz> | 2024-10-13 12:02:01 +0900 | 
|---|---|---|
| committer | A Farzat <a@farzat.xyz> | 2024-10-20 12:02:01 +0900 | 
| commit | 188d1e9e55a52d7751309852579b4e40468f482c (patch) | |
| tree | df6af27cab5e9290019a78ef2593f4ed365f7084 /.config/mpv/scripts | |
| parent | 726bc6f623b4e765f923bf10267fa4a1d66b0097 (diff) | |
| download | dotfiles-188d1e9e55a52d7751309852579b4e40468f482c.tar.gz dotfiles-188d1e9e55a52d7751309852579b4e40468f482c.zip | |
Add mpv user-input script
From https://github.com/CogentRedTester/mpv-user-input
Diffstat (limited to '.config/mpv/scripts')
| -rw-r--r-- | .config/mpv/scripts/user-input.lua | 781 | 
1 files changed, 781 insertions, 0 deletions
| diff --git a/.config/mpv/scripts/user-input.lua b/.config/mpv/scripts/user-input.lua new file mode 100644 index 0000000..b378ddc --- /dev/null +++ b/.config/mpv/scripts/user-input.lua @@ -0,0 +1,781 @@ +--[[ +MIT License + +Copyright (c) 2021 Oscar Manglaras + +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. +]] + +local mp = require 'mp' +local msg = require 'mp.msg' +local utils = require 'mp.utils' +local options = require 'mp.options' + +-- 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 probably doesn't +    -- have 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, +} + +options.read_options(opts, "user_input") + +local API_VERSION = "0.1.0" +local API_MAJOR_MINOR = API_VERSION:match("%d+%.%d+") + +local co = nil +local queue  = {} +local active_ids = {} +local histories = {} +local request = nil + +local line = '' + + +--[[ +    The below code is a modified implementation of text input from mpv's console.lua: +    https://github.com/mpv-player/mpv/blob/7ca14d646c7e405f3fb1e44600e2a67fc4607238/player/lua/console.lua + +    Modifications: +        removed support for log messages, sending commands, tab complete, help commands +        removed update timer +        Changed esc key to call handle_esc function +        handle_esc and handle_enter now resume the main coroutine with a response table +        made history specific to request ids +        localised all functions - reordered some to fit +        keybindings use new names +]]-- + +------------------------------START ORIGINAL MPV CODE----------------------------------- +---------------------------------------------------------------------------------------- +---------------------------------------------------------------------------------------- +---------------------------------------------------------------------------------------- +---------------------------------------------------------------------------------------- + +-- 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 assdraw = require 'mp.assdraw' + +local function detect_platform() +    local o = {} +    -- Kind of a dumb way of detecting the platform but whatever +    if mp.get_property_native('options/vo-mmcss-profile', o) ~= o then +        return 'windows' +    elseif mp.get_property_native('options/macos-force-dedicated-gpu', o) ~= o then +        return 'macos' +    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' +elseif platform == 'macos' then +    opts.font = 'Menlo' +else +    opts.font = 'monospace' +end + +local repl_active = false +local insert_mode = false +local cursor = 1 +local key_bindings = {} +local global_margin_y = 0 + +-- Escape a string for verbatim display on the OSD +local 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 +end + +-- Render the REPL and console as an ASS OSD +local function update() +    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 ass = assdraw.ass_new() +    local style = '{\\r' .. +                  '\\1a&H00&\\3a&H00&\\4a&H99&' .. +                  '\\1c&Heeeeee&\\3c&H111111&\\4c&H000000&' .. +                  '\\fn' .. opts.font .. '\\fs' .. opts.font_size .. +                  '\\bord1\\xshad0\\yshad1\\fsp0\\q1}' + +    local queue_style = '{\\r' .. +                        '\\1a&H00&\\3a&H00&\\4a&H99&' .. +                        '\\1c&Heeeeee&\\3c&H111111&\\4c&H000000&' .. +                        '\\fn' .. opts.font .. '\\fs' .. opts.font_size .. '\\c&H66ccff&' .. +                        '\\bord1\\xshad0\\yshad1\\fsp0\\q1}' + +    -- 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 = '{\\r' .. +                   '\\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)) + +    ass:new_event() +    ass:an(1) +    ass:pos(2, screeny - 2 - global_margin_y * screeny) + +    if (#queue == 2) then ass:append(queue_style .. string.format("There is 1 more request queued\\N")) +    elseif (#queue > 2) then ass:append(queue_style .. string.format("There are %d more requests queued\\N", #queue-1)) end +    ass:append(style .. request.text .. '\\N') +    ass:append('> ' .. before_cur) +    ass:append(cglyph) +    ass:append(style .. after_cur) + +    -- Redraw the cursor with the REPL text invisible. This will make the +    -- cursor appear in front of the text. +    ass:new_event() +    ass:an(1) +    ass:pos(2, screeny - 2) +    ass:append(style .. '{\\alpha&HFF&}> ' .. before_cur) +    ass:append(cglyph) +    ass:append(style .. '{\\alpha&HFF&}' .. after_cur) + +    mp.set_osd_ass(screenx, screeny, ass.text) +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. +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 charcter 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 + +-- Insert a character at the current cursor position (any_unicode) +local function handle_char_input(c) +    if insert_mode then +        line = line:sub(1, cursor - 1) .. c .. line:sub(next_utf8(line, cursor)) +    else +        line = line:sub(1, cursor - 1) .. c .. line:sub(cursor) +    end +    cursor = cursor + #c +    update() +end + +-- Remove the character behind the cursor (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) +    cursor = prev +    update() +end + +-- Remove the character in front of the cursor (Del) +local function handle_del() +    if cursor > line:len() then return end +    line = line:sub(1, cursor - 1) .. line:sub(next_utf8(line, cursor)) +    update() +end + +-- Toggle insert mode (Ins) +local function handle_ins() +    insert_mode = not insert_mode +end + +-- Move the cursor to the next character (Right) +local function next_char(amount) +    cursor = next_utf8(line, cursor) +    update() +end + +-- Move the cursor to the previous character (Left) +local function prev_char(amount) +    cursor = prev_utf8(line, cursor) +    update() +end + +-- Clear the current line (Ctrl+C) +local function clear() +    line = '' +    cursor = 1 +    insert_mode = false +    request.history.pos = #request.history.list + 1 +    update() +end + +-- Close the REPL if the current line is empty, otherwise do nothing (Ctrl+D) +local function maybe_exit() +    if line == '' then +    else +        handle_del() +    end +end + +local function handle_esc() +    coroutine.resume(co, { +        line = nil, +        err = "exited" +    }) +end + +-- Run the current command and clear the line (Enter) +local function handle_enter() +    if request.history.list[#request.history.list] ~= line and line ~= "" then +        request.history.list[#request.history.list + 1] = line +    end +    coroutine.resume(co, { +        line = line +    }) +end + +-- Go to the specified position in the command history +local function go_history(new_pos) +    local old_pos = request.history.pos +    request.history.pos = new_pos + +    -- Restrict the position to a legal value +    if request.history.pos > #request.history.list + 1 then +        request.history.pos = #request.history.list + 1 +    elseif request.history.pos < 1 then +        request.history.pos = 1 +    end + +    -- Do nothing if the history position didn't actually change +    if request.history.pos == old_pos then +        return +    end + +    -- If the user was editing a non-history line, save it as the last history +    -- entry. This makes it much less frustrating to accidentally hit Up/Down +    -- while editing a line. +    if old_pos == #request.history.list + 1 and line ~= '' and request.history.list[#request.history.list] ~= line then +        request.history.list[#request.history.list + 1] = line +    end + +    -- Now show the history line (or a blank line for #history + 1) +    if request.history.pos <= #request.history.list then +        line = request.history.list[request.history.pos] +    else +        line = '' +    end +    cursor = line:len() + 1 +    insert_mode = false +    update() +end + +-- Go to the specified relative position in the command history (Up, Down) +local function move_history(amount) +    go_history(request.history.pos + amount) +end + +-- Go to the first command in the command history (PgUp) +local function handle_pgup() +    go_history(1) +end + +-- Stop browsing history and start editing a blank line (PgDown) +local function handle_pgdown() +    go_history(#request.history.list + 1) +end + +-- Move to the start of the current word, or if already at the start, the start +-- of the previous word. (Ctrl+Left) +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. +    cursor = line:len() - select(2, line:reverse():find('%s*[^%s]*', line:len() - cursor + 2)) + 1 +    update() +end + +-- Move to the end of the current word, or if already at the end, the end of +-- the next word. (Ctrl+Right) +local function next_word() +    cursor = select(2, line:find('%s*[^%s]*', cursor)) + 1 +    update() +end + +-- Move the cursor to the beginning of the line (HOME) +local function go_home() +    cursor = 1 +    update() +end + +-- Move the cursor to the end of the line (END) +local function go_end() +    cursor = line:len() + 1 +    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 +    update() +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 +    update() +end + +-- Delete from the cursor to the end of the line (Ctrl+K) +local function del_to_eol() +    line = line:sub(1, cursor - 1) +    update() +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 +    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, +        }) +        if not res.error then +            return res.stdout +        end +    elseif platform == 'windows' then +        local res = utils.subprocess({ +            args = { 'powershell', '-NoProfile', '-Command', [[& { +                Trap { +                    Write-Error -ErrorRecord $_ +                    Exit 1 +                } + +                $clip = "" +                if (Get-Command "Get-Clipboard" -errorAction SilentlyContinue) { +                    $clip = Get-Clipboard -Raw -Format Text -TextFormatType UnicodeText +                } else { +                    Add-Type -AssemblyName PresentationCore +                    $clip = [Windows.Clipboard]::GetText() +                } + +                $clip = $clip -Replace "`r","" +                $u8clip = [System.Text.Encoding]::UTF8.GetBytes($clip) +                [Console]::OpenStandardOutput().Write($u8clip, 0, $u8clip.Length) +            }]] }, +            playback_only = false, +        }) +        if not res.error then +            return res.stdout +        end +    elseif platform == 'macos' then +        local res = utils.subprocess({ +            args = { 'pbpaste' }, +            playback_only = false, +        }) +        if not res.error then +            return res.stdout +        end +    end +    return '' +end + +-- Paste text from the window-system's clipboard. 'clip' determines whether the +-- clipboard or the primary selection buffer is used (on X11 and Wayland only.) +local function paste(clip) +    local text = get_clipboard(clip) +    local before_cur = line:sub(1, cursor - 1) +    local after_cur = line:sub(cursor) +    line = before_cur .. text .. after_cur +    cursor = cursor + text:len() +    update() +end + +-- List of input bindings. This is a weird mashup between common GUI text-input +-- bindings and readline bindings. +local function get_bindings() +    local bindings = { +        { 'esc',         handle_esc                             }, +        { 'enter',       handle_enter                           }, +        { 'kp_enter',    handle_enter                           }, +        { 'shift+enter', function() handle_char_input('\n') end }, +        { 'ctrl+j',      handle_enter                           }, +        { 'ctrl+m',      handle_enter                           }, +        { 'bs',          handle_backspace                       }, +        { 'shift+bs',    handle_backspace                       }, +        { 'ctrl+h',      handle_backspace                       }, +        { 'del',         handle_del                             }, +        { 'shift+del',   handle_del                             }, +        { 'ins',         handle_ins                             }, +        { 'shift+ins',   function() paste(false) end            }, +        { 'mbtn_mid',    function() paste(false) end            }, +        { 'left',        function() prev_char() end             }, +        { 'ctrl+b',      function() prev_char() end             }, +        { 'right',       function() next_char() end             }, +        { 'ctrl+f',      function() next_char() end             }, +        { 'up',          function() move_history(-1) end        }, +        { 'ctrl+p',      function() move_history(-1) end        }, +        { 'wheel_up',    function() move_history(-1) end        }, +        { 'down',        function() move_history(1) end         }, +        { 'ctrl+n',      function() move_history(1) end         }, +        { 'wheel_down',  function() move_history(1) end         }, +        { 'wheel_left',  function() end                         }, +        { 'wheel_right', function() end                         }, +        { 'ctrl+left',   prev_word                              }, +        { 'alt+b',       prev_word                              }, +        { 'ctrl+right',  next_word                              }, +        { 'alt+f',       next_word                              }, +        { 'ctrl+a',      go_home                                }, +        { 'home',        go_home                                }, +        { 'ctrl+e',      go_end                                 }, +        { 'end',         go_end                                 }, +        { 'pgup',        handle_pgup                            }, +        { 'pgdwn',       handle_pgdown                          }, +        { 'ctrl+c',      clear                                  }, +        { 'ctrl+d',      maybe_exit                             }, +        { 'ctrl+k',      del_to_eol                             }, +        { 'ctrl+u',      del_to_start                           }, +        { 'ctrl+v',      function() paste(true) end             }, +        { 'meta+v',      function() paste(true) end             }, +        { 'ctrl+bs',     del_word                               }, +        { 'ctrl+w',      del_word                               }, +        { 'ctrl+del',    del_next_word                          }, +        { 'alt+d',       del_next_word                          }, +        { 'kp_dec',      function() handle_char_input('.') end  }, +    } + +    for i = 0, 9 do +        bindings[#bindings + 1] = +            {'kp' .. i, function() handle_char_input('' .. i) end} +    end + +    return bindings +end + +local function text_input(info) +    if info.key_text and (info.event == "press" or info.event == "down" +                          or info.event == "repeat") +    then +        handle_char_input(info.key_text) +    end +end + +local function define_key_bindings() +    if #key_bindings > 0 then +        return +    end +    for _, bind in ipairs(get_bindings()) do +        -- Generate arbitrary name for removing the bindings later. +        local name = "_userinput_" .. bind[1] +        key_bindings[#key_bindings + 1] = name +        mp.add_forced_key_binding(bind[1], name, bind[2], {repeatable = true}) +    end +    mp.add_forced_key_binding("any_unicode", "_userinput_text", text_input, +        {repeatable = true, complex = true}) +    key_bindings[#key_bindings + 1] = "_userinput_text" +end + +local function undefine_key_bindings() +    for _, name in ipairs(key_bindings) do +        mp.remove_key_binding(name) +    end +    key_bindings = {} +end + +-- Set the REPL visibility ("enable", Esc) +local function set_active(active) +    if active == repl_active then return end +    if active then +        repl_active = true +        insert_mode = false +        define_key_bindings() +    else +        clear() +        repl_active = false +        undefine_key_bindings() +        collectgarbage() +    end +    update() +end + + +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) + +-- 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) + +---------------------------------------------------------------------------------------- +---------------------------------------------------------------------------------------- +---------------------------------------------------------------------------------------- +-------------------------------END ORIGINAL MPV CODE------------------------------------ + +--[[ +    sends a response to the original script in the form of a json string +    it is expected that all requests get a response, if the input is nil then err should say why +    current error codes are: +        exited          the user closed the input instead of pressing Enter +        already_queued  a request with the specified id was already in the queue +        cancelled       a script cancelled the request +        replace         replaced by another request +]] +local function send_response(res) +    if res.source then +        mp.commandv("script-message-to", res.source, res.response, (utils.format_json(res))) +    else +        mp.commandv("script-message", res.response, (utils.format_json(res))) +    end +end + +-- push new request onto the queue +-- if a request with the same id already exists and the queueable flag is not enabled then +-- a nil result will be returned to the function +function push_request(req) +    if active_ids[req.id] then +        if req.replace then +            for i, q_req in ipairs(queue) do +                if q_req.id == req.id then +                    send_response{ err = "replaced", response = q_req.response, source = q_req.source } +                    queue[i] = req +                    if i == 1 then request = req end +                end +            end +            update() +            return +        end + +        if not req.queueable then +            send_response{ err = "already_queued", response = req.response, source = req.source } +            return +        end +    end + +    table.insert(queue, req) +    active_ids[req.id] = (active_ids[req.id] or 0) + 1 +    if #queue == 1 then coroutine.resume(co) end +    update() +end + +-- safely removes an item from the queue and updates the set of active requests +function remove_request(index) +    local req = table.remove(queue, index) +    active_ids[req.id] = active_ids[req.id] - 1 + +    if active_ids[req.id] == 0 then active_ids[req.id] = nil end +    return req +end + +--an infinite loop that moves through the request queue +--uses a coroutine to handle asynchronous operations +local function driver() +    while (true) do +        while queue[1] do +            request = queue[1] +            line = request.default_input +            cursor = request.cursor_pos + +            if repl_active then update() +            else set_active(true) end + +            res = coroutine.yield() +            if res then +                res.source, res.response = request.source, request.response +                send_response(res) +                remove_request(1) +            end +        end + +        set_active(false) +        coroutine.yield() +    end +end + +co = coroutine.create(driver) + +--cancels any input request that returns true for the given predicate function +local function cancel_input_request(pred) +    for i = #queue, 1, -1 do +        if pred(i) then +            req = remove_request(i) +            send_response{ err = "cancelled", response = req.response, source = req.source } + +            --if we're removing the first item then that means the coroutine is waiting for a response +            --we will need to tell the coroutine to resume, upon which it will move to the next request +            --if there is something in the buffer then save it to the history before erasing it +            if i == 1 then +                local old_line = line +                if old_line ~= "" then table.insert(histories[req.id].list, old_line) end +                clear() +                coroutine.resume(co) +            end +        end +    end +end + +mp.register_script_message("cancel-user-input/uid", function(uid) +    cancel_input_request(function(i) return queue[i].response == uid end) +end) + +-- removes all requests with the specified id from the queue +mp.register_script_message("cancel-user-input/id", function(id) +    cancel_input_request(function(i) return queue[i].id == id end) +end) + +-- ensures a request has the correct fields and is correctly formatted +local function format_request_fields(req) +    assert(req.version, "input requests require an API version string") +    if not string.find(req.version, API_MAJOR_MINOR, 1, true) then +        error(("input request has invalid version: expected %s.x, got %s"):format(API_MAJOR_MINOR, req.version)) +    end + +    assert(req.response, "input requests require a response string") +    assert(req.id, "input requests require an id string") + +    req.text = ass_escape(req.request_text or "") +    req.default_input = req.default_input or "" +    req.cursor_pos = tonumber(req.cursor_pos) or 1 +    req.id = req.id or "mpv" + +    if req.cursor_pos ~= 1 then +        if req.cursor_pos  < 1 then req.cursor_pos  = 1 +        elseif req.cursor_pos  > #req.default_input then req.cursor_pos  = #req.default_input + 1 end +    end + +    if not histories[req.id] then histories[req.id] = {pos = 1, list = {}} end +    req.history = histories[req.id] +    return req +end + +-- updates the fields of a specific request +mp.register_script_message("update-user-input/uid", function(uid, req_opts) +    req_opts = utils.parse_json(req_opts) +    req_opts.response = uid +    for i, req in ipairs(queue) do +        if req.response == uid then +            local success, result = pcall(format_request_fields, req_opts) +            if not success then return msg.error(result) end + +            queue[i] = result +            if i == 1 then request = queue[1] end +            update() +            return +        end +    end +end) + +--the function that parses the input requests +local function input_request(req) +    req = format_request_fields(req) +    push_request(req) +end + +-- script message to recieve input requests, get-user-input.lua acts as an interface to call this script message +mp.register_script_message("request-user-input", function(req) +    msg.debug(req) +    req = utils.parse_json(req) +    local success, err = pcall(input_request, req) +    if not success then +        send_response{ err = err, response = req.response, source = req.source} +        msg.error(err) +    end +end) + | 
