init
This commit is contained in:
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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 }
|
||||
}
|
||||
Reference in New Issue
Block a user