diff options
Diffstat (limited to 'mpv/scripts/file-browser/modules/addons/windir.lua')
| -rw-r--r-- | mpv/scripts/file-browser/modules/addons/windir.lua | 218 |
1 files changed, 218 insertions, 0 deletions
diff --git a/mpv/scripts/file-browser/modules/addons/windir.lua b/mpv/scripts/file-browser/modules/addons/windir.lua new file mode 100644 index 0000000..436fe33 --- /dev/null +++ b/mpv/scripts/file-browser/modules/addons/windir.lua @@ -0,0 +1,218 @@ +--[[ + An addon for mpv-file-browser which uses the Windows dir command to parse native directories + This behaves near identically to the native parser, but IO is done asynchronously. + + Available at: https://github.com/CogentRedTester/mpv-file-browser/tree/master/addons +]]-- + +local mp = require "mp" +local msg = require "mp.msg" +local fb = require "file-browser" + +local PLATFORM = fb.get_platform() + +---@param bytes string +---@return fun(): number, number +local function byte_iterator(bytes) + ---@async + ---@return number? + local function iter() + for i = 1, #bytes do + coroutine.yield(bytes:byte(i), i) + end + error('malformed utf16le string - expected byte but found end of string') + end + + return coroutine.wrap(iter) +end + +---@param bits number +---@param by number +---@return number +local function lshift(bits, by) + return bits * 2^by +end + +---@param bits number +---@param by number +---@return integer +local function rshift(bits, by) + return math.floor(bits / 2^by) +end + +---@param bits number +---@param i number +---@return number +local function bits_below(bits, i) + return bits % 2^i +end + +---@param bits number +---@param i number exclusive +---@param j number inclusive +---@return integer +local function bits_between(bits, i, j) + return rshift(bits_below(bits, j), i) +end + +---@param bytes string +---@return number[] +local function utf16le_to_unicode(bytes) + msg.trace('converting from utf16-le to unicode codepoints') + + ---@type number[] + local codepoints = {} + + local get_byte = byte_iterator(bytes) + + while true do + -- start of a char + local success, little, i = pcall(get_byte) + if not success then break end + + local big = get_byte() + local codepoint = little + lshift(big, 8) + + if codepoint < 0xd800 or codepoint > 0xdfff then + table.insert(codepoints, codepoint) + else + -- handling surrogate pairs + -- grab the next two bytes to grab the low surrogate + local high_pair = codepoint + local low_pair = get_byte() + lshift(get_byte(), 8) + + if high_pair >= 0xdc00 then + error(('malformed utf16le string at byte #%d (0x%04X) - high surrogate pair should be < 0xDC00'):format(i, high_pair)) + elseif low_pair < 0xdc00 then + error(('malformed utf16le string at byte #%d (0x%04X) - low surrogate pair should be >= 0xDC00'):format(i+2, low_pair)) + end + + -- The last 10 bits of each surrogate are the two halves of the codepoint + -- https://en.wikipedia.org/wiki/UTF-16#Code_points_from_U+010000_to_U+10FFFF + local high_bits = bits_below(high_pair, 10) + local low_bits = bits_below(low_pair, 10) + local surrogate_par = (low_bits + lshift(high_bits, 10)) + 0x10000 + + table.insert(codepoints, surrogate_par) + end + end + + return codepoints +end + +---@param codepoints number[] +---@return string +local function unicode_to_utf8(codepoints) + ---@type number[] + local bytes = {} + + -- https://en.wikipedia.org/wiki/UTF-8#Description + for i, codepoint in ipairs(codepoints) do + if codepoint >= 0xd800 and codepoint <= 0xdfff then + error(('codepoint %d (U+%05X) is within the reserved surrogate pair range (U+D800-U+DFFF)'):format(i, codepoint)) + elseif codepoint <= 0x7f then + table.insert(bytes, codepoint) + elseif codepoint <= 0x7ff then + table.insert(bytes, 0xC0 + rshift(codepoint, 6)) + table.insert(bytes, 0x80 + bits_below(codepoint, 6)) + elseif codepoint <= 0xffff then + table.insert(bytes, 0xE0 + rshift(codepoint, 12)) + table.insert(bytes, 0x80 + bits_between(codepoint, 6, 12)) + table.insert(bytes, 0x80 + bits_below(codepoint, 6)) + elseif codepoint <= 0x10ffff then + table.insert(bytes, 0xF0 + rshift(codepoint, 18)) + table.insert(bytes, 0x80 + bits_between(codepoint, 12, 18)) + table.insert(bytes, 0x80 + bits_between(codepoint, 6, 12)) + table.insert(bytes, 0x80 + bits_below(codepoint, 6)) + else + error(('codepoint %d (U+%05X) is larger than U+10FFFF'):format(i, codepoint)) + end + end + + return string.char(table.unpack(bytes)) +end + +local function utf8(text) + return unicode_to_utf8(utf16le_to_unicode(text)) +end + +---@type ParserConfig +local dir = { + priority = 109, + api_version = "1.9.0", + name = "cmd-dir", + keybind_name = "file" +} + +---@async +---@param args string[] +---@param parse_state ParseState +---@return string|nil +local function command(args, parse_state) + local async = mp.command_native_async({ + name = "subprocess", + playback_only = false, + capture_stdout = true, + capture_stderr = true, + args = args, + }, fb.coroutine.callback(30) ) + + ---@type boolean, boolean, MPVSubprocessResult + local completed, _, cmd = parse_state:yield() + if not completed then + msg.warn('read timed out for:', table.unpack(args)) + mp.abort_async_command(async) + return nil + end + + local success = xpcall(function() + cmd.stdout = utf8(cmd.stdout) or '' + cmd.stderr = utf8(cmd.stderr) or '' + end, fb.traceback) + + if not success then return msg.error('failed to convert utf16-le string to utf8') end + + --dir returns this exact error message if the directory is empty + if cmd.status == 1 and cmd.stderr == "File Not Found\r\n" then cmd.status = 0 end + if cmd.status ~= 0 then return msg.error(cmd.stderr) end + + return cmd.status == 0 and cmd.stdout or nil +end + +function dir:can_parse(directory) + if not fb.get_opt('windir_parser') then return false end + return PLATFORM == 'windows' and directory ~= '' and not fb.get_protocol(directory) +end + +---@async +function dir:parse(directory, parse_state) + local list = {} + + -- the dir command expects backslashes for our paths + directory = string.gsub(directory, "/", "\\") + + local dirs = command({ "cmd", "/U", "/c", "dir", "/b", "/ad", directory }, parse_state) + if not dirs then return end + + local files = command({ "cmd", "/U", "/c", "dir", "/b", "/a-d", directory }, parse_state) + if not files then return end + + for name in dirs:gmatch("[^\n\r]+") do + name = name.."/" + if fb.valid_dir(name) then + table.insert(list, { name = name, type = "dir" }) + msg.trace(name) + end + end + + for name in files:gmatch("[^\n\r]+") do + if fb.valid_file(name) then + table.insert(list, { name = name, type = "file" }) + msg.trace(name) + end + end + + return list, { filtered = true } +end + +return dir |
