summaryrefslogtreecommitdiff
path: root/mpv/scripts/file-browser/modules/addons
diff options
context:
space:
mode:
Diffstat (limited to 'mpv/scripts/file-browser/modules/addons')
-rw-r--r--mpv/scripts/file-browser/modules/addons/cache.lua146
-rw-r--r--mpv/scripts/file-browser/modules/addons/file.lua46
-rw-r--r--mpv/scripts/file-browser/modules/addons/find.lua124
-rw-r--r--mpv/scripts/file-browser/modules/addons/home-label.lua31
-rw-r--r--mpv/scripts/file-browser/modules/addons/last-opened-directory.lua62
-rw-r--r--mpv/scripts/file-browser/modules/addons/ls.lua68
-rw-r--r--mpv/scripts/file-browser/modules/addons/root.lua26
-rw-r--r--mpv/scripts/file-browser/modules/addons/windir.lua218
-rw-r--r--mpv/scripts/file-browser/modules/addons/winroot.lua62
9 files changed, 783 insertions, 0 deletions
diff --git a/mpv/scripts/file-browser/modules/addons/cache.lua b/mpv/scripts/file-browser/modules/addons/cache.lua
new file mode 100644
index 0000000..b1011fd
--- /dev/null
+++ b/mpv/scripts/file-browser/modules/addons/cache.lua
@@ -0,0 +1,146 @@
+--[[
+ This file is an internal file-browser addon.
+ It should not be imported like a normal module.
+
+ Maintains a cache of the accessed directories to improve
+ parsing speed. Disabled by default.
+]]
+
+local mp = require 'mp'
+local msg = require 'mp.msg'
+local utils = require 'mp.utils'
+
+local fb = require 'file-browser'
+
+---@type ParserConfig
+local cacheParser = {
+ name = 'cache',
+ priority = 0,
+ api_version = '1.9',
+}
+
+---@class CacheEntry
+---@field list List
+---@field opts Opts?
+---@field timeout MPTimer
+
+---@type table<string,CacheEntry>
+local cache = {}
+
+---@type table<string,(async fun(list: List?, opts: Opts?))[]>
+local pending_parses = {}
+
+---@param directories? string[]
+local function clear_cache(directories)
+ if directories then
+ msg.debug('clearing cache for', #directories, 'directorie(s)')
+ for _, dir in ipairs(directories) do
+ if cache[dir] then
+ msg.trace('clearing cache for', dir)
+ cache[dir].timeout:kill()
+ cache[dir] = nil
+ end
+ end
+ else
+ msg.debug('clearing cache')
+ for _, entry in pairs(cache) do
+ entry.timeout:kill()
+ end
+ cache = {}
+ end
+end
+
+---@type string
+local prev_directory = ''
+
+function cacheParser:can_parse(directory, parse_state)
+ -- allows the cache to be forcibly used or bypassed with the
+ -- cache/use parse property.
+ if parse_state.properties.cache and parse_state.properties.cache.use ~= nil then
+ if parse_state.source == 'browser' then prev_directory = directory end
+ return parse_state.properties.cache.use
+ end
+
+ -- the script message is guaranteed to always bypass the cache
+ if parse_state.source == 'script-message' then return false end
+ if not fb.get_opt('cache') or directory == '' then return false end
+
+ -- clear the cache if reloading the current directory in the browser
+ -- this means that fb.rescan() should maintain expected behaviour
+ if parse_state.source == 'browser' then
+ if prev_directory == directory then clear_cache({directory}) end
+ prev_directory = directory
+ end
+
+ return true
+end
+
+---@async
+function cacheParser:parse(directory)
+ if cache[directory] then
+ msg.verbose('fetching', directory, 'contents from cache')
+ cache[directory].timeout:kill()
+ cache[directory].timeout:resume()
+ return cache[directory].list, cache[directory].opts
+ end
+
+ ---@type List?, Opts?
+ local list, opts
+
+ -- if another parse is already running on the same directory, then wait and use the same result
+ if not pending_parses[directory] then
+ pending_parses[directory] = {}
+ list, opts = self:defer(directory)
+ else
+ msg.debug('parse for', directory, 'already running - waiting for other parse to finish...')
+ table.insert(pending_parses[directory], fb.coroutine.callback(30))
+ list, opts = coroutine.yield()
+ end
+
+ local pending = pending_parses[directory]
+ -- need to clear the pending parses before resuming them or they will also attempt to resume the parses
+ pending_parses[directory] = nil
+ if pending and #pending > 0 then
+ msg.debug('resuming', #pending, 'pending parses for', directory)
+ for _, cb in ipairs(pending) do
+ cb(list, opts)
+ end
+ end
+
+ if not list then return end
+
+ -- pending will be truthy for the original parse and falsy for any parses that were pending
+ if pending then
+ msg.debug('storing', directory, 'contents in cache')
+ cache[directory] = {
+ list = list,
+ opts = opts,
+ timeout = mp.add_timeout(120, function() cache[directory] = nil end),
+ }
+ end
+
+ return list, opts
+end
+
+cacheParser.keybinds = {
+ {
+ key = 'Ctrl+Shift+r',
+ name = 'clear',
+ command = function() clear_cache() ; fb.rescan() end,
+ }
+}
+
+-- provide method of clearing the cache through script messages
+mp.register_script_message('cache/clear', function(dirs)
+ if not dirs then
+ return clear_cache()
+ end
+
+ ---@type string[]?
+ local directories = utils.parse_json(dirs)
+ if not directories then msg.error('unable to parse', dirs) end
+
+ clear_cache(directories)
+end)
+
+return cacheParser
diff --git a/mpv/scripts/file-browser/modules/addons/file.lua b/mpv/scripts/file-browser/modules/addons/file.lua
new file mode 100644
index 0000000..a57573c
--- /dev/null
+++ b/mpv/scripts/file-browser/modules/addons/file.lua
@@ -0,0 +1,46 @@
+-- This file is an internal file-browser addon.
+-- It should not be imported like a normal module.
+
+local msg = require 'mp.msg'
+local utils = require 'mp.utils'
+
+---Parser for native filesystems
+---@type ParserConfig
+local file_parser = {
+ name = "file",
+ priority = 110,
+ api_version = '1.0.0',
+}
+
+--try to parse any directory except for the root
+function file_parser:can_parse(directory)
+ return directory ~= ''
+end
+
+--scans the given directory using the mp.utils.readdir function
+function file_parser:parse(directory)
+ local new_list = {}
+ local list1 = utils.readdir(directory, 'dirs')
+ if list1 == nil then return nil end
+
+ --sorts folders and formats them into the list of directories
+ for i=1, #list1 do
+ local item = list1[i]
+
+ msg.trace(item..'/')
+ table.insert(new_list, {name = item..'/', type = 'dir'})
+ end
+
+ --appends files to the list of directory items
+ local list2 = utils.readdir(directory, 'files')
+ if list2 == nil then return nil end
+ for i=1, #list2 do
+ local item = list2[i]
+
+ msg.trace(item)
+ table.insert(new_list, {name = item, type = 'file'})
+ end
+ return new_list
+end
+
+return file_parser
diff --git a/mpv/scripts/file-browser/modules/addons/find.lua b/mpv/scripts/file-browser/modules/addons/find.lua
new file mode 100644
index 0000000..0b64d92
--- /dev/null
+++ b/mpv/scripts/file-browser/modules/addons/find.lua
@@ -0,0 +1,124 @@
+--[[
+ This file is an internal file-browser addon.
+ It should not be imported like a normal module.
+
+ Allows searching the current directory.
+]]--
+
+local msg = require "mp.msg"
+local fb = require "file-browser"
+local input_loaded, input = pcall(require, "mp.input")
+local user_input_loaded, user_input = pcall(require, "user-input-module")
+
+---@type ParserConfig
+local find = {
+ api_version = "1.3.0"
+}
+
+---@type thread|nil
+local latest_coroutine = nil
+
+---@type State
+local global_fb_state = getmetatable(fb.get_state()).__original
+
+---@param name string
+---@param query string
+---@return boolean
+local function compare(name, query)
+ if name:find(query) then return true end
+ if name:lower():find(query) then return true end
+ if name:upper():find(query) then return true end
+
+ return false
+end
+
+---@async
+---@param key Keybind
+---@param state State
+---@param co thread
+---@return boolean?
+local function main(key, state, co)
+ if not state.list then return false end
+
+ ---@type string
+ local text
+ if key.name == "find/find" then text = "Find: enter search string"
+ else text = "Find: enter advanced search string" end
+
+ if input_loaded then
+ input.get({
+ prompt = text .. "\n>",
+ id = "file-browser/find",
+ submit = fb.coroutine.callback(),
+ })
+ elseif user_input_loaded then
+ user_input.get_user_input( fb.coroutine.callback(), { text = text, id = "find", replace = true } )
+ end
+
+ local query, error = coroutine.yield()
+ if input_loaded then input.terminate() end
+ if not query then return msg.debug(error) end
+
+ -- allow the directory to be changed before this point
+ local list = fb.get_list()
+ local parse_id = global_fb_state.co
+
+ if key.name == "find/find" then
+ query = fb.pattern_escape(query)
+ end
+
+ local results = {}
+
+ for index, item in ipairs(list) do
+ if compare(item.label or item.name, query) then
+ table.insert(results, index)
+ end
+ end
+
+ if (#results < 1) then
+ msg.warn("No matching items for '"..query.."'")
+ return
+ end
+
+ --keep cycling through the search results if any are found
+ --putting this into a separate coroutine removes any passthrough ambiguity
+ --the final return statement should return to `step_find` not any other function
+ ---@async
+ fb.coroutine.run(function()
+ latest_coroutine = coroutine.running()
+ ---@type number
+ local rindex = 1
+ while (true) do
+
+ if rindex == 0 then rindex = #results
+ elseif rindex == #results + 1 then rindex = 1 end
+
+ fb.set_selected_index(results[rindex])
+ local direction = coroutine.yield(true) --[[@as number]]
+ rindex = rindex + direction
+
+ if parse_id ~= global_fb_state.co then
+ latest_coroutine = nil
+ return
+ end
+ end
+ end)
+end
+
+local function step_find(key)
+ if not latest_coroutine then return false end
+ ---@type number
+ local direction = 0
+ if key.name == "find/next" then direction = 1
+ elseif key.name == "find/prev" then direction = -1 end
+ return fb.coroutine.resume_err(latest_coroutine, direction)
+end
+
+find.keybinds = {
+ {"Ctrl+f", "find", main, {}},
+ {"Ctrl+F", "find_advanced", main, {}},
+ {"n", "next", step_find, {}},
+ {"N", "prev", step_find, {}},
+}
+
+return find
diff --git a/mpv/scripts/file-browser/modules/addons/home-label.lua b/mpv/scripts/file-browser/modules/addons/home-label.lua
new file mode 100644
index 0000000..5e6cf8e
--- /dev/null
+++ b/mpv/scripts/file-browser/modules/addons/home-label.lua
@@ -0,0 +1,31 @@
+--[[
+ An addon for mpv-file-browser which displays ~/ for the home directory instead of the full path
+]]--
+
+local mp = require "mp"
+local fb = require "file-browser"
+
+local home = fb.fix_path(mp.command_native({"expand-path", "~/"}) --[[@as string]], true)
+
+---@type ParserConfig
+local home_label = {
+ priority = 100,
+ api_version = "1.0.0"
+}
+
+function home_label:can_parse(directory)
+ if not fb.get_opt('home_label') then return false end
+ return directory:sub(1, home:len()) == home
+end
+
+---@async
+function home_label:parse(directory)
+ local list, opts = self:defer(directory)
+ if not opts then opts = {} end
+ if (not opts.directory or opts.directory == directory) and not opts.directory_label then
+ opts.directory_label = "~/"..(directory:sub(home:len()+1) or "")
+ end
+ return list, opts
+end
+
+return home_label \ No newline at end of file
diff --git a/mpv/scripts/file-browser/modules/addons/last-opened-directory.lua b/mpv/scripts/file-browser/modules/addons/last-opened-directory.lua
new file mode 100644
index 0000000..c68f408
--- /dev/null
+++ b/mpv/scripts/file-browser/modules/addons/last-opened-directory.lua
@@ -0,0 +1,62 @@
+--[[
+ An addon for mpv-file-browser which stores the last opened directory and
+ sets it as the opened directory the next time mpv is opened.
+
+ 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 state_file = mp.command_native({'expand-path', fb.get_opt('last_opened_directory_file')}) --[[@as string]]
+msg.verbose('using', state_file)
+
+---@param directory? string
+---@return nil
+local function write_directory(directory)
+ if not fb.get_opt('save_last_opened_directory') then return end
+
+ local file = io.open(state_file, 'w+')
+
+ if not file then return msg.error('could not open', state_file, 'for writing') end
+
+ directory = directory or fb.get_directory() or ''
+ msg.verbose('writing', directory, 'to', state_file)
+ file:write(directory)
+ file:close()
+end
+
+---@type ParserConfig
+local addon = {
+ api_version = '1.7.0',
+ priority = 0,
+}
+
+function addon:setup()
+ if not fb.get_opt('default_to_last_opened_directory') then return end
+
+ local file = io.open(state_file, "r")
+ if not file then
+ return msg.info('failed to open', state_file, 'for reading (may be due to first load)')
+ end
+
+ local dir = file:read("*a")
+ msg.verbose('setting default directory to', dir)
+ fb.browse_directory(dir, false)
+ file:close()
+end
+
+function addon:can_parse(dir, parse_state)
+ if parse_state.source == 'browser' then write_directory(dir) end
+ return false
+end
+
+function addon:parse()
+ return nil
+end
+
+mp.register_event('shutdown', function() write_directory() end)
+
+return addon
diff --git a/mpv/scripts/file-browser/modules/addons/ls.lua b/mpv/scripts/file-browser/modules/addons/ls.lua
new file mode 100644
index 0000000..8c2e23c
--- /dev/null
+++ b/mpv/scripts/file-browser/modules/addons/ls.lua
@@ -0,0 +1,68 @@
+--[[
+ An addon for mpv-file-browser which uses the Linux ls 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()
+
+---@type ParserConfig
+local ls = {
+ priority = 109,
+ api_version = "1.9.0",
+ name = "ls",
+ 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
+
+ return cmd.status == 0 and cmd.stdout or nil
+end
+
+function ls:can_parse(directory)
+ if not fb.get_opt('ls_parser') then return false end
+ return PLATFORM ~= 'windows' and directory ~= '' and not fb.get_protocol(directory)
+end
+
+---@async
+function ls:parse(directory, parse_state)
+ local list = {}
+ local files = command({"ls", "-1", "-p", "-A", "-N", "--zero", "-L", directory}, parse_state)
+
+ if not files then return nil end
+
+ for str in files:gmatch("%Z+") do
+ local is_dir = str:sub(-1) == "/"
+ msg.trace(str)
+
+ table.insert(list, {name = str, type = is_dir and "dir" or "file"})
+ end
+
+ return list
+end
+
+return ls
diff --git a/mpv/scripts/file-browser/modules/addons/root.lua b/mpv/scripts/file-browser/modules/addons/root.lua
new file mode 100644
index 0000000..412a5ba
--- /dev/null
+++ b/mpv/scripts/file-browser/modules/addons/root.lua
@@ -0,0 +1,26 @@
+-- This file is an internal file-browser addon.
+-- It should not be imported like a normal module.
+
+local g = require 'modules.globals'
+
+---Parser for the root.
+---@type ParserConfig
+local root_parser = {
+ name = "root",
+ priority = math.huge,
+ api_version = '1.0.0',
+}
+
+function root_parser:can_parse(directory)
+ return directory == ''
+end
+
+--we return the root directory exactly as setup
+function root_parser:parse()
+ return g.root, {
+ sorted = true,
+ filtered = true,
+ }
+end
+
+return root_parser
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
diff --git a/mpv/scripts/file-browser/modules/addons/winroot.lua b/mpv/scripts/file-browser/modules/addons/winroot.lua
new file mode 100644
index 0000000..0b046e8
--- /dev/null
+++ b/mpv/scripts/file-browser/modules/addons/winroot.lua
@@ -0,0 +1,62 @@
+--[[
+ This file is an internal file-browser addon.
+ It should not be imported like a normal module.
+
+ Automatically populates the root with windows drives on startup.
+ Ctrl+r will add new drives mounted since startup.
+
+ Drives will only be added if they are not already present in the root.
+]]
+
+local mp = require 'mp'
+local msg = require 'mp.msg'
+local fb = require 'file-browser'
+
+local PLATFORM = fb.get_platform()
+
+---returns a list of windows drives
+---@return string[]?
+local function get_drives()
+ ---@type MPVSubprocessResult?, string?
+ local result, err = mp.command_native({
+ name = 'subprocess',
+ playback_only = false,
+ capture_stdout = true,
+ args = {'fsutil', 'fsinfo', 'drives'}
+ })
+ if not result then return msg.error(err) end
+ if result.status ~= 0 then return msg.error('could not read windows root') end
+
+ local root = {}
+ for drive in result.stdout:gmatch("(%a:)\\") do
+ table.insert(root, drive..'/')
+ end
+ return root
+end
+
+-- adds windows drives to the root if they are not already present
+local function import_drives()
+ if fb.get_opt('auto_detect_windows_drives') and PLATFORM ~= 'windows' then return end
+
+ local drives = get_drives()
+ if not drives then return end
+
+ for _, drive in ipairs(drives) do
+ fb.register_root_item(drive)
+ end
+end
+
+local keybind = {
+ key = 'Ctrl+r',
+ name = 'import_root_drives',
+ command = import_drives,
+ parser = 'root',
+ passthrough = true
+}
+
+---@type ParserConfig
+return {
+ api_version = '1.9.0',
+ setup = import_drives,
+ keybinds = { keybind }
+}