nirish

config files for niri window manager
Log | Files | Refs | README | LICENSE

windir.lua (6795B)


      1 --[[
      2     An addon for mpv-file-browser which uses the Windows dir command to parse native directories
      3     This behaves near identically to the native parser, but IO is done asynchronously.
      4 
      5     Available at: https://github.com/CogentRedTester/mpv-file-browser/tree/master/addons
      6 ]]--
      7 
      8 local mp = require "mp"
      9 local msg = require "mp.msg"
     10 local fb = require "file-browser"
     11 
     12 local PLATFORM = fb.get_platform()
     13 
     14 ---@param bytes string
     15 ---@return fun(): number, number
     16 local function byte_iterator(bytes)
     17     ---@async
     18     ---@return number?
     19     local function iter()
     20         for i = 1, #bytes do
     21             coroutine.yield(bytes:byte(i), i)
     22         end
     23         error('malformed utf16le string - expected byte but found end of string')
     24     end
     25 
     26     return coroutine.wrap(iter)
     27 end
     28 
     29 ---@param bits number
     30 ---@param by number
     31 ---@return number
     32 local function lshift(bits, by)
     33     return bits * 2^by
     34 end
     35 
     36 ---@param bits number
     37 ---@param by number
     38 ---@return integer
     39 local function rshift(bits, by)
     40     return math.floor(bits / 2^by)
     41 end
     42 
     43 ---@param bits number
     44 ---@param i number
     45 ---@return number
     46 local function bits_below(bits, i)
     47     return bits % 2^i
     48 end
     49 
     50 ---@param bits number
     51 ---@param i number exclusive
     52 ---@param j number inclusive
     53 ---@return integer
     54 local function bits_between(bits, i, j)
     55     return rshift(bits_below(bits, j), i)
     56 end
     57 
     58 ---@param bytes string
     59 ---@return number[]
     60 local function utf16le_to_unicode(bytes)
     61     msg.trace('converting from utf16-le to unicode codepoints')
     62 
     63     ---@type number[]
     64     local codepoints = {}
     65 
     66     local get_byte = byte_iterator(bytes)
     67 
     68     while true do
     69         -- start of a char
     70         local success, little, i = pcall(get_byte)
     71         if not success then break end
     72 
     73         local big = get_byte()
     74         local codepoint = little + lshift(big, 8)
     75 
     76         if codepoint < 0xd800 or codepoint > 0xdfff then
     77             table.insert(codepoints, codepoint)
     78         else
     79             -- handling surrogate pairs
     80             -- grab the next two bytes to grab the low surrogate
     81             local high_pair = codepoint
     82             local low_pair = get_byte() + lshift(get_byte(), 8)
     83 
     84             if high_pair >= 0xdc00 then
     85                 error(('malformed utf16le string at byte #%d (0x%04X) - high surrogate pair should be < 0xDC00'):format(i, high_pair))
     86             elseif low_pair < 0xdc00 then
     87                 error(('malformed utf16le string at byte #%d (0x%04X) - low surrogate pair should be >= 0xDC00'):format(i+2, low_pair))
     88             end
     89 
     90             -- The last 10 bits of each surrogate are the two halves of the codepoint
     91             -- https://en.wikipedia.org/wiki/UTF-16#Code_points_from_U+010000_to_U+10FFFF
     92             local high_bits = bits_below(high_pair, 10)
     93             local low_bits = bits_below(low_pair, 10)
     94             local surrogate_par = (low_bits + lshift(high_bits, 10)) + 0x10000
     95 
     96             table.insert(codepoints, surrogate_par)
     97         end
     98     end
     99 
    100     return codepoints
    101 end
    102 
    103 ---@param codepoints number[]
    104 ---@return string
    105 local function unicode_to_utf8(codepoints)
    106     ---@type number[]
    107     local bytes = {}
    108 
    109     -- https://en.wikipedia.org/wiki/UTF-8#Description
    110     for i, codepoint in ipairs(codepoints) do
    111         if codepoint >= 0xd800 and codepoint <= 0xdfff then
    112             error(('codepoint %d (U+%05X) is within the reserved surrogate pair range (U+D800-U+DFFF)'):format(i, codepoint))
    113         elseif codepoint <= 0x7f then
    114             table.insert(bytes, codepoint)
    115         elseif codepoint <= 0x7ff then
    116             table.insert(bytes, 0xC0 + rshift(codepoint, 6))
    117             table.insert(bytes, 0x80 + bits_below(codepoint, 6))
    118         elseif codepoint <= 0xffff then
    119             table.insert(bytes, 0xE0 + rshift(codepoint, 12))
    120             table.insert(bytes, 0x80 + bits_between(codepoint, 6, 12))
    121             table.insert(bytes, 0x80 + bits_below(codepoint, 6))
    122         elseif codepoint <= 0x10ffff then
    123             table.insert(bytes, 0xF0 + rshift(codepoint, 18))
    124             table.insert(bytes, 0x80 + bits_between(codepoint, 12, 18))
    125             table.insert(bytes, 0x80 + bits_between(codepoint, 6, 12))
    126             table.insert(bytes, 0x80 + bits_below(codepoint, 6))
    127         else
    128             error(('codepoint %d (U+%05X) is larger than U+10FFFF'):format(i, codepoint))
    129         end
    130     end
    131 
    132     return string.char(table.unpack(bytes))
    133 end
    134 
    135 local function utf8(text)
    136     return unicode_to_utf8(utf16le_to_unicode(text))
    137 end
    138 
    139 ---@type ParserConfig
    140 local dir = {
    141     priority = 109,
    142     api_version = "1.9.0",
    143     name = "cmd-dir",
    144     keybind_name = "file"
    145 }
    146 
    147 ---@async
    148 ---@param args string[]
    149 ---@param parse_state ParseState
    150 ---@return string|nil
    151 local function command(args, parse_state)
    152     local async = mp.command_native_async({
    153             name = "subprocess",
    154             playback_only = false,
    155             capture_stdout = true,
    156             capture_stderr = true,
    157             args = args,
    158         }, fb.coroutine.callback(30) )
    159 
    160     ---@type boolean, boolean, MPVSubprocessResult
    161     local completed, _, cmd = parse_state:yield()
    162     if not completed then
    163         msg.warn('read timed out for:', table.unpack(args))
    164         mp.abort_async_command(async)
    165         return nil
    166     end
    167 
    168     local success = xpcall(function()
    169         cmd.stdout = utf8(cmd.stdout) or ''
    170         cmd.stderr = utf8(cmd.stderr) or ''
    171     end, fb.traceback)
    172 
    173     if not success then return msg.error('failed to convert utf16-le string to utf8') end
    174 
    175     --dir returns this exact error message if the directory is empty
    176     if cmd.status == 1 and cmd.stderr == "File Not Found\r\n" then cmd.status = 0 end
    177     if cmd.status ~= 0 then return msg.error(cmd.stderr) end
    178 
    179     return cmd.status == 0 and cmd.stdout or nil
    180 end
    181 
    182 function dir:can_parse(directory)
    183     if not fb.get_opt('windir_parser') then return false end
    184     return PLATFORM == 'windows' and directory ~= '' and not fb.get_protocol(directory)
    185 end
    186 
    187 ---@async
    188 function dir:parse(directory, parse_state)
    189     local list = {}
    190 
    191     -- the dir command expects backslashes for our paths
    192     directory = string.gsub(directory, "/", "\\")
    193 
    194     local dirs = command({ "cmd", "/U", "/c", "dir", "/b", "/ad", directory }, parse_state)
    195     if not dirs then return end
    196 
    197     local files = command({ "cmd", "/U", "/c", "dir", "/b", "/a-d", directory }, parse_state)
    198     if not files then return end
    199 
    200     for name in dirs:gmatch("[^\n\r]+") do
    201         name = name.."/"
    202         if fb.valid_dir(name) then
    203             table.insert(list, { name = name, type = "dir" })
    204             msg.trace(name)
    205         end
    206     end
    207 
    208     for name in files:gmatch("[^\n\r]+") do
    209         if fb.valid_file(name) then
    210             table.insert(list, { name = name, type = "file" })
    211             msg.trace(name)
    212         end
    213     end
    214 
    215     return list, { filtered = true }
    216 end
    217 
    218 return dir