init
This commit is contained in:
@@ -0,0 +1,930 @@
|
||||
local mp = require 'mp'
|
||||
local utils = require 'mp.utils'
|
||||
local assdraw = require 'mp.assdraw'
|
||||
|
||||
-- create namespace with default values
|
||||
local em = {
|
||||
|
||||
-- customisable values ------------------------------------------------------
|
||||
|
||||
loop_when_navigating = false, -- Loop when navigating through list
|
||||
lines_to_show = 17, -- NOT including search line
|
||||
pause_on_open = true,
|
||||
resume_on_exit = "only-if-was-paused", -- another possible value is true
|
||||
|
||||
-- styles (earlyer it was a table, but required many more steps to pass def-s
|
||||
-- here from .conf file)
|
||||
font_size = 21,
|
||||
--font size scales by window
|
||||
scale_by_window = false,
|
||||
-- cursor 'width', useful to change if you have hidpi monitor
|
||||
cursor_x_border = 0.3,
|
||||
line_bottom_margin = 1, -- basically space between lines
|
||||
text_color = {
|
||||
default = 'ffffff',
|
||||
accent = 'd8a07b',
|
||||
current = 'aaaaaa',
|
||||
comment = '636363',
|
||||
},
|
||||
menu_x_padding = 5, -- this padding for now applies only to 'left', not x
|
||||
menu_y_padding = 2, -- but this one applies to both - top & bottom
|
||||
|
||||
|
||||
-- values that should be passed from main script ----------------------------
|
||||
|
||||
search_heading = 'Default search heading',
|
||||
-- 'full' is required from main script, 'current_i' is optional
|
||||
-- others are 'private'
|
||||
list = {
|
||||
full = {}, filtered = {}, current_i = nil, pointer_i = 1, show_from_to = {}
|
||||
},
|
||||
-- field to compare with when searching for 'current value' by 'current_i'
|
||||
index_field = 'index',
|
||||
-- fields to use when searching for string match / any other custom searching
|
||||
-- if value has 0 length, then search list item itself
|
||||
filter_by_fields = {},
|
||||
|
||||
|
||||
-- 'private' values that are not supposed to be changed from the outside ----
|
||||
|
||||
is_active = false,
|
||||
-- https://mpv.io/manual/master/#lua-scripting-mp-create-osd-overlay(format)
|
||||
ass = mp.create_osd_overlay("ass-events"),
|
||||
was_paused = false, -- flag that indicates that vid was paused by this script
|
||||
|
||||
line = '',
|
||||
-- if there was no cursor it wouldn't have been needed, but for now we need
|
||||
-- variable below only to compare it with 'line' and see if we need to filter
|
||||
prev_line = '',
|
||||
cursor = 1,
|
||||
history = {},
|
||||
history_pos = 1,
|
||||
key_bindings = {},
|
||||
insert_mode = false,
|
||||
|
||||
-- used only in 'update' func to get error text msgs
|
||||
error_codes = {
|
||||
no_match = 'Match required',
|
||||
no_submit_provided = 'No submit function provided'
|
||||
}
|
||||
}
|
||||
|
||||
-- PRIVATE METHODS ------------------------------------------------------------
|
||||
|
||||
local ime_active = mp.get_property_native("input-ime")
|
||||
|
||||
-- declare constructor function
|
||||
function em:new(o)
|
||||
o = o or {}
|
||||
setmetatable(o, self)
|
||||
self.__index = self
|
||||
|
||||
-- some options might be customised by user in .conf file and read as strings
|
||||
-- in that case parse those
|
||||
if type(o.filter_by_fields) == 'string' then
|
||||
o.filter_by_fields = utils.parse_json(o.filter_by_fields)
|
||||
end
|
||||
|
||||
if type(o.text_color) == 'string' then
|
||||
o.text_color = utils.parse_json(o.text_color)
|
||||
end
|
||||
|
||||
return o
|
||||
end
|
||||
|
||||
-- this func is just a getter of a current list depending on search line
|
||||
function em:current()
|
||||
return self.line == '' and self.list.full or self.list.filtered
|
||||
end
|
||||
|
||||
-- REVIEW: how to get rid of this wrapper and handle filter func sideeffects
|
||||
-- in a more elegant way?
|
||||
function em:filter_wrapper()
|
||||
-- handles sideeffect that are needed to be run on filtering list
|
||||
-- cuz the filter func may be redefined in main script and therefore needs
|
||||
-- to be straight forward - only doing filtering and returning the table
|
||||
|
||||
-- passing current query just in case, so ppl can use it in their custom funcs
|
||||
self.list.filtered = self:filter(self.line)
|
||||
|
||||
self.prev_line = self.line
|
||||
self.list.pointer_i = 1
|
||||
self:set_from_to(true)
|
||||
end
|
||||
|
||||
function em:set_from_to(reset_flag)
|
||||
-- additional variables just for shorter var name
|
||||
local i = self.list.pointer_i
|
||||
local to_show = self.lines_to_show
|
||||
local total = #self:current()
|
||||
|
||||
if reset_flag or to_show >= total then
|
||||
self.list.show_from_to = { 1, math.min(to_show, total) }
|
||||
return
|
||||
end
|
||||
|
||||
-- If menu is opened with something already selected we want this 'selected'
|
||||
-- to be displayed close to the middle of the menu. That's why 'show_from_to'
|
||||
-- is not initially set, so we can know - if show_from_to length is 0 - it is
|
||||
-- first call of this func in cur. init
|
||||
if #self.list.show_from_to == 0 then
|
||||
-- set show_from_to so chosen item will be displayed close to middle
|
||||
local half_list = math.ceil(to_show / 2)
|
||||
if i < half_list then
|
||||
self.list.show_from_to = { 1, to_show }
|
||||
elseif total - i < half_list then
|
||||
self.list.show_from_to = { total - to_show + 1, total }
|
||||
else
|
||||
self.list.show_from_to = { i - half_list + 1, i - half_list + to_show }
|
||||
end
|
||||
else
|
||||
table.unpack = table.unpack or unpack -- 5.1 compatibility
|
||||
local first, last = table.unpack(self.list.show_from_to)
|
||||
|
||||
-- handle cursor moving towards start / end bondary
|
||||
if first ~= 1 and i - first < 2 then
|
||||
self.list.show_from_to = { first - 1, last - 1 }
|
||||
end
|
||||
if last ~= total and last - i < 2 then
|
||||
self.list.show_from_to = { first + 1, last + 1 }
|
||||
end
|
||||
|
||||
-- handle index jumps from beginning to end and backwards
|
||||
if i > last then
|
||||
self.list.show_from_to = { i - to_show + 1, i }
|
||||
end
|
||||
if i < first then self.list.show_from_to = { 1, to_show } end
|
||||
end
|
||||
end
|
||||
|
||||
function em:change_selected_index(num)
|
||||
self.list.pointer_i = self.list.pointer_i + num
|
||||
if self.loop_when_navigating then
|
||||
if self.list.pointer_i < 1 then
|
||||
self.list.pointer_i = #self:current()
|
||||
elseif self.list.pointer_i > #self:current() then
|
||||
self.list.pointer_i = 1
|
||||
end
|
||||
else
|
||||
if self.list.pointer_i < 1 then
|
||||
self.list.pointer_i = 1
|
||||
elseif self.list.pointer_i > #self:current() then
|
||||
self.list.pointer_i = #self:current()
|
||||
end
|
||||
end
|
||||
self:set_from_to()
|
||||
self:update()
|
||||
end
|
||||
|
||||
-- Render the REPL and console as an ASS OSD
|
||||
function em:update(err_code)
|
||||
-- ASS tags documentation here - https://aegi.vmoe.info/docs/3.0/ASS_Tags/
|
||||
|
||||
-- do not bother if function was called to close the menu..
|
||||
if not self.is_active then
|
||||
em.ass:remove()
|
||||
return
|
||||
end
|
||||
|
||||
local line_height = self.font_size + self.line_bottom_margin
|
||||
local _, h, aspect = mp.get_osd_size()
|
||||
local wh = self.scale_by_window and 720 or h
|
||||
local ww = wh * aspect
|
||||
|
||||
-- '+ 1' below is a search string
|
||||
local menu_y_pos =
|
||||
wh - (line_height * (self.lines_to_show + 1) + self.menu_y_padding * 2)
|
||||
|
||||
-- didn't find better place to handle filtered list update
|
||||
if self.line ~= self.prev_line then self:filter_wrapper() end
|
||||
|
||||
local function get_background()
|
||||
local a = self:ass_new_wrapper()
|
||||
a:append('{\\1c&H1c1c1c\\1a&H19}') -- background color & opacity
|
||||
a:pos(0, 0)
|
||||
a:draw_start()
|
||||
a:rect_cw(0, menu_y_pos, ww, wh)
|
||||
a:draw_stop()
|
||||
return a.text
|
||||
end
|
||||
|
||||
local function get_search_header()
|
||||
local a = self:ass_new_wrapper()
|
||||
|
||||
a:pos(self.menu_x_padding, menu_y_pos + self.menu_y_padding)
|
||||
|
||||
local search_prefix = table.concat({
|
||||
self:get_font_color('accent'),
|
||||
(#self:current() ~= 0 and self.list.pointer_i or '!'),
|
||||
'/', #self:current(), '\\h\\h', self.search_heading, ':\\h'
|
||||
});
|
||||
|
||||
a:append(search_prefix)
|
||||
-- reset font color after search prefix
|
||||
a:append(self:get_font_color 'default')
|
||||
|
||||
-- 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 = self.font_size * 8
|
||||
-- TODO: maybe do it using draw_rect from ass?
|
||||
local cglyph = '{\\r' .. -- styles reset
|
||||
'\\1c&Hffffff&\\3c&Hffffff' .. -- font color and border color
|
||||
'\\xbord' .. self.cursor_x_border .. '\\p4\\pbo24}' .. -- xborder, scale x8 and baseline offset
|
||||
'm 0 0 l 0 ' .. cheight .. -- drawing just a line
|
||||
'{\\p0\\r}' -- finish drawing and reset styles
|
||||
local before_cur = self:ass_escape(self.line:sub(1, self.cursor - 1))
|
||||
local after_cur = self:ass_escape(self.line:sub(self.cursor))
|
||||
|
||||
a:append(table.concat({
|
||||
before_cur, cglyph, self:reset_styles(),
|
||||
self:get_font_color('default'), after_cur,
|
||||
(err_code and '\\h' .. self.error_codes[err_code] or "")
|
||||
}))
|
||||
|
||||
return a.text
|
||||
|
||||
-- NOTE: perhaps this commented code will some day help me in coding cursor
|
||||
-- like in M-x emacs menu:
|
||||
-- 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:append(style .. '{\\alpha&HFF&}> ' .. before_cur)
|
||||
-- ass:append(cglyph)
|
||||
-- ass:append(style .. '{\\alpha&HFF&}' .. after_cur)
|
||||
end
|
||||
|
||||
local function get_list()
|
||||
local a = assdraw.ass_new()
|
||||
|
||||
local function apply_highlighting(y)
|
||||
a:new_event()
|
||||
a:append(self:reset_styles())
|
||||
a:append('{\\1c&Hffffff\\1a&HE6}') -- background color & opacity
|
||||
a:pos(0, 0)
|
||||
a:draw_start()
|
||||
a:rect_cw(0, y, ww, y + self.font_size)
|
||||
a:draw_stop()
|
||||
end
|
||||
|
||||
-- REVIEW: maybe make another function 'get_line_str' and move there
|
||||
-- everything from this for loop?
|
||||
-- REVIEW: how to use something like table.unpack below?
|
||||
for i = self.list.show_from_to[1], self.list.show_from_to[2] do
|
||||
local value = assert(self:current()[i], 'no value with index ' .. i)
|
||||
local y_offset = menu_y_pos + self.menu_y_padding +
|
||||
(line_height * (i - self.list.show_from_to[1] + 1))
|
||||
|
||||
if i == self.list.pointer_i then apply_highlighting(y_offset) end
|
||||
|
||||
a:new_event()
|
||||
a:append(self:reset_styles())
|
||||
a:pos(self.menu_x_padding, y_offset)
|
||||
a:append(self:get_line(i, value))
|
||||
end
|
||||
|
||||
return a.text
|
||||
end
|
||||
|
||||
em.ass.res_x = ww
|
||||
em.ass.res_y = wh
|
||||
em.ass.data = table.concat({
|
||||
get_background(),
|
||||
get_search_header(),
|
||||
get_list()
|
||||
}, "\n")
|
||||
|
||||
em.ass:update()
|
||||
end
|
||||
|
||||
-- params:
|
||||
-- - data : {list: {}, [current_i] : num}
|
||||
function em:init(data)
|
||||
self.list.full = data.list or {}
|
||||
self.list.current_i = data.current_i or nil
|
||||
self.list.pointer_i = data.current_i or 1
|
||||
self:set_active(true)
|
||||
end
|
||||
|
||||
function em:exit()
|
||||
self:undefine_key_bindings()
|
||||
collectgarbage()
|
||||
end
|
||||
|
||||
-- TODO: write some idle func like this
|
||||
-- function idle()
|
||||
-- if pending_selection then
|
||||
-- gallery:set_selection(pending_selection)
|
||||
-- pending_selection = nil
|
||||
-- end
|
||||
-- if ass_changed or geometry_changed then
|
||||
-- local ww, wh = mp.get_osd_size()
|
||||
-- if geometry_changed then
|
||||
-- geometry_changed = false
|
||||
-- compute_geometry(ww, wh)
|
||||
-- end
|
||||
-- if ass_changed then
|
||||
-- ass_changed = false
|
||||
-- mp.set_osd_ass(ww, wh, ass)
|
||||
-- end
|
||||
-- end
|
||||
-- end
|
||||
-- ...
|
||||
-- and handle it as follows
|
||||
-- init():
|
||||
-- mp.register_idle(idle)
|
||||
-- idle()
|
||||
-- exit():
|
||||
-- mp.unregister_idle(idle)
|
||||
-- idle()
|
||||
-- And in these observers he is setting a flag, that's being checked in func above
|
||||
-- mp.observe_property("osd-width", "native", mark_geometry_stale)
|
||||
-- mp.observe_property("osd-height", "native", mark_geometry_stale)
|
||||
|
||||
-- PRIVATE METHODS END --------------------------------------------------------
|
||||
|
||||
-- PUBLIC METHODS -------------------------------------------------------------
|
||||
|
||||
function em:filter()
|
||||
-- default filter func, might be redefined in main script
|
||||
local result = {}
|
||||
|
||||
local function get_full_search_str(v)
|
||||
local str = ''
|
||||
for _, key in ipairs(self.filter_by_fields) do str = str .. (v[key] or '') end
|
||||
return str
|
||||
end
|
||||
|
||||
for _, v in ipairs(self.list.full) do
|
||||
-- if filter_by_fields has 0 length, then search list item itself
|
||||
if #self.filter_by_fields == 0 then
|
||||
if self:search_method(v) then table.insert(result, v) end
|
||||
else
|
||||
-- NOTE: we might use search_method on fiels separately like this:
|
||||
-- for _,key in ipairs(self.filter_by_fields) do
|
||||
-- if self:search_method(v[key]) then table.insert(result, v) end
|
||||
-- end
|
||||
-- But since im planning to implement fuzzy search in future i need full
|
||||
-- search string here
|
||||
if self:search_method(get_full_search_str(v)) then
|
||||
table.insert(result, v)
|
||||
end
|
||||
end
|
||||
end
|
||||
return result
|
||||
end
|
||||
|
||||
-- TODO: implement fuzzy search and maybe match highlights
|
||||
function em:search_method(str)
|
||||
-- also might be redefined by main script
|
||||
|
||||
-- convert to string just to make sure..
|
||||
return tostring(str):lower():find(self.line:lower(), 1, true)
|
||||
end
|
||||
|
||||
-- this module requires submit function to be defined in main script
|
||||
function em:submit() self:update('no_submit_provided') end
|
||||
|
||||
function em:update_list(list)
|
||||
-- for now this func doesn't handle cases when we have 'current_i' to update
|
||||
-- it
|
||||
self.list.full = list
|
||||
if self.line ~= self.prev_line then self:filter_wrapper() end
|
||||
end
|
||||
|
||||
-- PUBLIC METHODS END ---------------------------------------------------------
|
||||
|
||||
-- HELPER METHODS -------------------------------------------------------------
|
||||
|
||||
function em:get_line(_, v) -- [i]ndex, [v]alue
|
||||
-- this func might be redefined in main script to get a custom-formatted line
|
||||
-- default implementation of this func supposes that value.content field is a
|
||||
-- String
|
||||
local a = assdraw.ass_new()
|
||||
local style = (self.list.current_i == v[self.index_field])
|
||||
and 'current' or 'default'
|
||||
|
||||
a:append(self:reset_styles())
|
||||
a:append(self:get_font_color(style))
|
||||
-- content as default field, which is holding string
|
||||
-- no point in moving it to main object since content itself is being
|
||||
-- composed in THIS function, that might (and most likely, should) be
|
||||
-- redefined in main script
|
||||
a:append(v.content or 'Something is off in `get_line` func')
|
||||
return a.text
|
||||
end
|
||||
|
||||
-- REVIEW: for now i don't see normal way of mergin this func with below one
|
||||
-- but it's being used only once
|
||||
function em:reset_styles()
|
||||
local a = assdraw.ass_new()
|
||||
-- alignment top left, no word wrapping, border 0, shadow 0
|
||||
a:append('{\\an7\\q2\\bord0\\shad0}')
|
||||
a:append('{\\fs' .. self.font_size .. '}')
|
||||
return a.text
|
||||
end
|
||||
|
||||
-- function to get rid of some copypaste
|
||||
function em:ass_new_wrapper()
|
||||
local a = assdraw.ass_new()
|
||||
a:new_event()
|
||||
a:append(self:reset_styles())
|
||||
return a
|
||||
end
|
||||
|
||||
function em:get_font_color(style)
|
||||
return '{\\1c&H' .. self.text_color[style] .. '}'
|
||||
end
|
||||
|
||||
-- HELPER METHODS END ---------------------------------------------------------
|
||||
|
||||
|
||||
--[[
|
||||
The below code is a modified implementation of text input from mpv's console.lua:
|
||||
https://github.com/mpv-player/mpv/blob/87c9eefb2928252497f6141e847b74ad1158bc61/player/lua/console.lua
|
||||
|
||||
I was too lazy to list all modifications i've done to the script, but if u
|
||||
rly need to see those - do diff with the original code
|
||||
]]
|
||||
--
|
||||
|
||||
-------------------------------------------------------------------------------
|
||||
-- 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.
|
||||
|
||||
function em: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
|
||||
|
||||
-- Escape a string for verbatim display on the OSD
|
||||
function em: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
|
||||
|
||||
-- Set the REPL visibility ("enable", Esc)
|
||||
function em:set_active(active)
|
||||
if active == self.is_active then return end
|
||||
if active then
|
||||
if ime_active == false then
|
||||
mp.set_property_bool("input-ime", true)
|
||||
end
|
||||
self.is_active = true
|
||||
self.insert_mode = false
|
||||
mp.enable_messages('terminal-default')
|
||||
self:define_key_bindings()
|
||||
|
||||
-- set flag 'was_paused' only if vid wasn't paused before EM init
|
||||
if self.pause_on_open and not mp.get_property_bool("pause", false) then
|
||||
mp.set_property_bool("pause", true)
|
||||
self.was_paused = true
|
||||
end
|
||||
|
||||
self:set_from_to()
|
||||
self:update()
|
||||
else
|
||||
-- no need to call 'update' in this block cuz 'clear' method is calling it
|
||||
if ime_active == false then
|
||||
mp.set_property_bool("input-ime", false)
|
||||
end
|
||||
self.is_active = false
|
||||
self:undefine_key_bindings()
|
||||
|
||||
if self.resume_on_exit == true or
|
||||
(self.resume_on_exit == "only-if-was-paused" and self.was_paused) then
|
||||
mp.set_property_bool("pause", false)
|
||||
end
|
||||
|
||||
self:clear()
|
||||
collectgarbage()
|
||||
end
|
||||
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.
|
||||
function em: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'
|
||||
function em: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)
|
||||
function em:handle_char_input(c)
|
||||
if self.insert_mode then
|
||||
self.line = self.line:sub(1, self.cursor - 1) .. c .. self.line:sub(self:next_utf8(self.line, self.cursor))
|
||||
else
|
||||
self.line = self.line:sub(1, self.cursor - 1) .. c .. self.line:sub(self.cursor)
|
||||
end
|
||||
self.cursor = self.cursor + #c
|
||||
self:update()
|
||||
end
|
||||
|
||||
-- Remove the character behind the cursor (Backspace)
|
||||
function em:handle_backspace()
|
||||
if self.cursor <= 1 then return end
|
||||
local prev = self:prev_utf8(self.line, self.cursor)
|
||||
self.line = self.line:sub(1, prev - 1) .. self.line:sub(self.cursor)
|
||||
self.cursor = prev
|
||||
self:update()
|
||||
end
|
||||
|
||||
-- Remove the character in front of the cursor (Del)
|
||||
function em:handle_del()
|
||||
if self.cursor > self.line:len() then return end
|
||||
self.line = self.line:sub(1, self.cursor - 1) .. self.line:sub(self:next_utf8(self.line, self.cursor))
|
||||
self:update()
|
||||
end
|
||||
|
||||
-- Toggle insert mode (Ins)
|
||||
function em:handle_ins()
|
||||
self.insert_mode = not self.insert_mode
|
||||
end
|
||||
|
||||
-- Move the cursor to the next character (Right)
|
||||
function em:next_char()
|
||||
self.cursor = self:next_utf8(self.line, self.cursor)
|
||||
self:update()
|
||||
end
|
||||
|
||||
-- Move the cursor to the previous character (Left)
|
||||
function em:prev_char()
|
||||
self.cursor = self:prev_utf8(self.line, self.cursor)
|
||||
self:update()
|
||||
end
|
||||
|
||||
-- Clear the current line (Ctrl+C)
|
||||
function em:clear()
|
||||
self.line = ''
|
||||
self.prev_line = ''
|
||||
|
||||
self.list.current_i = nil
|
||||
self.list.pointer_i = 1
|
||||
self.list.filtered = {}
|
||||
self.list.show_from_to = {}
|
||||
|
||||
self.was_paused = false
|
||||
|
||||
self.cursor = 1
|
||||
self.insert_mode = false
|
||||
self.history_pos = #self.history + 1
|
||||
|
||||
self:update()
|
||||
end
|
||||
|
||||
-- Run the current command and clear the line (Enter)
|
||||
function em:handle_enter()
|
||||
if #self:current() == 0 then
|
||||
self:update('no_match')
|
||||
return
|
||||
end
|
||||
|
||||
if self.history[#self.history] ~= self.line then
|
||||
self.history[#self.history + 1] = self.line
|
||||
end
|
||||
|
||||
self:submit(self:current()[self.list.pointer_i])
|
||||
self:set_active(false)
|
||||
end
|
||||
|
||||
-- Go to the specified position in the command history
|
||||
function em:go_history(new_pos)
|
||||
local old_pos = self.history_pos
|
||||
self.history_pos = new_pos
|
||||
|
||||
-- Restrict the position to a legal value
|
||||
if self.history_pos > #self.history + 1 then
|
||||
self.history_pos = #self.history + 1
|
||||
elseif self.history_pos < 1 then
|
||||
self.history_pos = 1
|
||||
end
|
||||
|
||||
-- Do nothing if the history position didn't actually change
|
||||
if self.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 == #self.history + 1 and self.line ~= '' and self.history[#self.history] ~= self.line then
|
||||
self.history[#self.history + 1] = self.line
|
||||
end
|
||||
|
||||
-- Now show the history line (or a blank line for #history + 1)
|
||||
if self.history_pos <= #self.history then
|
||||
self.line = self.history[self.history_pos]
|
||||
else
|
||||
self.line = ''
|
||||
end
|
||||
self.cursor = self.line:len() + 1
|
||||
self.insert_mode = false
|
||||
self:update()
|
||||
end
|
||||
|
||||
-- Go to the specified relative position in the command history (Up, Down)
|
||||
function em:move_history(amount)
|
||||
self:go_history(self.history_pos + amount)
|
||||
end
|
||||
|
||||
-- Go to the first command in the command history (PgUp)
|
||||
function em:handle_pgup()
|
||||
-- Determine the number of items to move up (half a page)
|
||||
local half_page = math.ceil(self.lines_to_show / 2)
|
||||
|
||||
-- Move the history position up by half a page
|
||||
self:change_selected_index(-half_page)
|
||||
end
|
||||
|
||||
-- Stop browsing history and start editing a blank line (PgDown)
|
||||
function em:handle_pgdown()
|
||||
-- Determine the number of items to move down (half a page)
|
||||
local half_page = math.ceil(self.lines_to_show / 2)
|
||||
|
||||
-- Move the history position down by half a page
|
||||
self:change_selected_index(half_page)
|
||||
end
|
||||
|
||||
-- Move to the start of the current word, or if already at the start, the start
|
||||
-- of the previous word. (Ctrl+Left)
|
||||
function em: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.
|
||||
self.cursor = self.line:len() - select(2, self.line:reverse():find('%s*[^%s]*', self.line:len() - self.cursor + 2)) + 1
|
||||
self:update()
|
||||
end
|
||||
|
||||
-- Move to the end of the current word, or if already at the end, the end of
|
||||
-- the next word. (Ctrl+Right)
|
||||
function em:next_word()
|
||||
self.cursor = select(2, self.line:find('%s*[^%s]*', self.cursor)) + 1
|
||||
self:update()
|
||||
end
|
||||
|
||||
-- Move the cursor to the beginning of the line (HOME)
|
||||
function em:go_home()
|
||||
self.cursor = 1
|
||||
self:update()
|
||||
end
|
||||
|
||||
-- Move the cursor to the end of the line (END)
|
||||
function em:go_end()
|
||||
self.cursor = self.line:len() + 1
|
||||
self:update()
|
||||
end
|
||||
|
||||
-- Delete from the cursor to the beginning of the word (Ctrl+Backspace)
|
||||
function em:del_word()
|
||||
local before_cur = self.line:sub(1, self.cursor - 1)
|
||||
local after_cur = self.line:sub(self.cursor)
|
||||
|
||||
before_cur = before_cur:gsub('[^%s]+%s*$', '', 1)
|
||||
self.line = before_cur .. after_cur
|
||||
self.cursor = before_cur:len() + 1
|
||||
self:update()
|
||||
end
|
||||
|
||||
-- Delete from the cursor to the end of the word (Ctrl+Del)
|
||||
function em:del_next_word()
|
||||
if self.cursor > self.line:len() then return end
|
||||
|
||||
local before_cur = self.line:sub(1, self.cursor - 1)
|
||||
local after_cur = self.line:sub(self.cursor)
|
||||
|
||||
after_cur = after_cur:gsub('^%s*[^%s]+', '', 1)
|
||||
self.line = before_cur .. after_cur
|
||||
self:update()
|
||||
end
|
||||
|
||||
-- Delete from the cursor to the end of the line (Ctrl+K)
|
||||
function em:del_to_eol()
|
||||
self.line = self.line:sub(1, self.cursor - 1)
|
||||
self:update()
|
||||
end
|
||||
|
||||
-- Delete from the cursor back to the start of the line (Ctrl+U)
|
||||
function em:del_to_start()
|
||||
self.line = self.line:sub(self.cursor)
|
||||
self.cursor = 1
|
||||
self:update()
|
||||
end
|
||||
|
||||
-- Returns a string of UTF-8 text from the clipboard (or the primary selection)
|
||||
function em:get_clipboard(clip)
|
||||
-- Pick a better default font for Windows and macOS
|
||||
local platform = self:detect_platform()
|
||||
|
||||
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.)
|
||||
function em:paste(clip)
|
||||
local text = self:get_clipboard(clip)
|
||||
local before_cur = self.line:sub(1, self.cursor - 1)
|
||||
local after_cur = self.line:sub(self.cursor)
|
||||
self.line = before_cur .. text .. after_cur
|
||||
self.cursor = self.cursor + text:len()
|
||||
self:update()
|
||||
end
|
||||
|
||||
-- List of input bindings. This is a weird mashup between common GUI text-input
|
||||
-- bindings and readline bindings.
|
||||
function em:get_bindings()
|
||||
local bindings = {
|
||||
{ 'ctrl+[', function() self:set_active(false) end },
|
||||
{ 'ctrl+g', function() self:set_active(false) end },
|
||||
{ 'esc', function() self:set_active(false) end },
|
||||
{ 'enter', function() self:handle_enter() end },
|
||||
{ 'kp_enter', function() self:handle_enter() end },
|
||||
{ 'ctrl+m', function() self:handle_enter() end },
|
||||
{ 'bs', function() self:handle_backspace() end },
|
||||
{ 'shift+bs', function() self:handle_backspace() end },
|
||||
{ 'ctrl+h', function() self:handle_backspace() end },
|
||||
{ 'del', function() self:handle_del() end },
|
||||
{ 'shift+del', function() self:handle_del() end },
|
||||
{ 'ins', function() self:handle_ins() end },
|
||||
{ 'shift+ins', function() self:paste(false) end },
|
||||
{ 'mbtn_mid', function() self:paste(false) end },
|
||||
{ 'left', function() self:prev_char() end },
|
||||
{ 'ctrl+b', function() self:prev_char() end },
|
||||
{ 'right', function() self:next_char() end },
|
||||
{ 'ctrl+f', function() self:next_char() end },
|
||||
{ 'ctrl+k', function() self:change_selected_index(-1) end },
|
||||
{ 'ctrl+p', function() self:change_selected_index(-1) end },
|
||||
{ 'ctrl+j', function() self:change_selected_index(1) end },
|
||||
{ 'ctrl+n', function() self:change_selected_index(1) end },
|
||||
{ 'up', function() self:move_history(-1) end },
|
||||
{ 'alt+p', function() self:move_history(-1) end },
|
||||
{ 'wheel_up', function() self:move_history(-1) end },
|
||||
{ 'down', function() self:move_history(1) end },
|
||||
{ 'alt+n', function() self:move_history(1) end },
|
||||
{ 'wheel_down', function() self:move_history(1) end },
|
||||
{ 'wheel_left', function() end },
|
||||
{ 'wheel_right', function() end },
|
||||
{ 'ctrl+left', function() self:prev_word() end },
|
||||
{ 'alt+b', function() self:prev_word() end },
|
||||
{ 'ctrl+right', function() self:next_word() end },
|
||||
{ 'alt+f', function() self:next_word() end },
|
||||
{ 'ctrl+a', function() self:go_home() end },
|
||||
{ 'home', function() self:go_home() end },
|
||||
{ 'ctrl+e', function() self:go_end() end },
|
||||
{ 'end', function() self:go_end() end },
|
||||
{ 'ctrl+shift+f',function() self:handle_pgdown() end },
|
||||
{ 'ctrl+shift+b',function() self:handle_pgup() end },
|
||||
{ 'pgdwn', function() self:handle_pgdown() end },
|
||||
{ 'pgup', function() self:handle_pgup() end },
|
||||
{ 'ctrl+c', function() self:clear() end },
|
||||
{ 'ctrl+d', function() self:handle_del() end },
|
||||
{ 'ctrl+u', function() self:del_to_start() end },
|
||||
{ 'ctrl+v', function() self:paste(true) end },
|
||||
{ 'meta+v', function() self:paste(true) end },
|
||||
{ 'ctrl+bs', function() self:del_word() end },
|
||||
{ 'ctrl+w', function() self:del_word() end },
|
||||
{ 'ctrl+del', function() self:del_next_word() end },
|
||||
{ 'alt+d', function() self:del_next_word() end },
|
||||
{ 'kp_dec', function() self:handle_char_input('.') end },
|
||||
}
|
||||
|
||||
for i = 0, 9 do
|
||||
bindings[#bindings + 1] =
|
||||
{ 'kp' .. i, function() self:handle_char_input('' .. i) end }
|
||||
end
|
||||
|
||||
return bindings
|
||||
end
|
||||
|
||||
function em:text_input(info)
|
||||
if info.key_text and (info.event == "press" or info.event == "down"
|
||||
or info.event == "repeat")
|
||||
then
|
||||
self:handle_char_input(info.key_text)
|
||||
end
|
||||
end
|
||||
|
||||
function em:define_key_bindings()
|
||||
if #self.key_bindings > 0 then
|
||||
return
|
||||
end
|
||||
for _, bind in ipairs(self:get_bindings()) do
|
||||
-- Generate arbitrary name for removing the bindings later.
|
||||
local name = "search_" .. (#self.key_bindings + 1)
|
||||
self.key_bindings[#self.key_bindings + 1] = name
|
||||
mp.add_forced_key_binding(bind[1], name, bind[2], { repeatable = true })
|
||||
end
|
||||
mp.add_forced_key_binding("any_unicode", "search_input", function(...)
|
||||
self:text_input(...)
|
||||
end, { repeatable = true, complex = true })
|
||||
self.key_bindings[#self.key_bindings + 1] = "search_input"
|
||||
end
|
||||
|
||||
function em:undefine_key_bindings()
|
||||
for _, name in ipairs(self.key_bindings) do
|
||||
mp.remove_key_binding(name)
|
||||
end
|
||||
self.key_bindings = {}
|
||||
end
|
||||
|
||||
-------------------------------------------------------------------------------
|
||||
-- END ORIGINAL MPV CODE --
|
||||
-------------------------------------------------------------------------------
|
||||
|
||||
return em
|
||||
@@ -0,0 +1,206 @@
|
||||
--[[
|
||||
An addon for mpv-file-browser which adds a Favourites path that can be loaded from the ROOT
|
||||
]]--
|
||||
|
||||
local mp = require "mp"
|
||||
local msg = require "mp.msg"
|
||||
|
||||
local fb = require 'file-browser'
|
||||
local save_path = mp.command_native({"expand-path", "~~/script-opts/file_browser_favourites.txt"}) --[[@as string]]
|
||||
do
|
||||
local file = io.open(save_path, "a+")
|
||||
if not file then
|
||||
msg.error("cannot access file", ("%q"):format(save_path), "make sure that the directory exists")
|
||||
return {}
|
||||
end
|
||||
file:close()
|
||||
end
|
||||
|
||||
---@type Item[]
|
||||
local favourites = {}
|
||||
local favourites_loaded = false
|
||||
|
||||
---@type ParserConfig
|
||||
local favs = {
|
||||
api_version = "1.8.0",
|
||||
priority = 30,
|
||||
cursor = 1
|
||||
}
|
||||
|
||||
local use_virtual_directory = true
|
||||
|
||||
---@type table<string,string>
|
||||
local full_paths = {}
|
||||
|
||||
---@param str string
|
||||
---@return Item
|
||||
local function create_favourite_object(str)
|
||||
local item = {
|
||||
type = str:sub(-1) == "/" and "dir" or "file",
|
||||
path = str,
|
||||
redirect = not use_virtual_directory,
|
||||
name = str:match("([^/]+/?)$")
|
||||
}
|
||||
full_paths[str:match("([^/]+)/?$")] = str
|
||||
return item
|
||||
end
|
||||
|
||||
---@param self Parser
|
||||
function favs:setup()
|
||||
self:register_root_item('Favourites/')
|
||||
end
|
||||
|
||||
local function update_favourites()
|
||||
local file = io.open(save_path, "r")
|
||||
if not file then return end
|
||||
|
||||
favourites = {}
|
||||
for str in file:lines() do
|
||||
table.insert(favourites, create_favourite_object(str))
|
||||
end
|
||||
file:close()
|
||||
favourites_loaded = true
|
||||
end
|
||||
|
||||
function favs:can_parse(directory)
|
||||
return directory:find("Favourites/") == 1
|
||||
end
|
||||
|
||||
---@async
|
||||
---@param self Parser
|
||||
---@param directory string
|
||||
---@return List?
|
||||
---@return Opts?
|
||||
function favs:parse(directory)
|
||||
if not favourites_loaded then update_favourites() end
|
||||
if directory == "Favourites/" then
|
||||
local opts = {
|
||||
filtered = true,
|
||||
sorted = true
|
||||
}
|
||||
return favourites, opts
|
||||
end
|
||||
|
||||
if use_virtual_directory then
|
||||
-- converts the relative favourite path into a full path
|
||||
local name = directory:match("Favourites/([^/]+)/?")
|
||||
|
||||
local _, finish = directory:find("Favourites/([^/]+/?)")
|
||||
local full_path = (full_paths[name] or "")..directory:sub(finish+1)
|
||||
local list, opts = self:defer(full_path or "")
|
||||
|
||||
if not list then return nil end
|
||||
opts = opts or {}
|
||||
opts.id = self:get_id()
|
||||
if opts.directory_label then
|
||||
opts.directory_label = opts.directory_label:gsub(full_paths[name], "Favourites/"..name..'/')
|
||||
if opts.directory_label:find("Favourites/") ~= 1 then opts.directory_label = nil end
|
||||
end
|
||||
|
||||
for _, item in ipairs(list) do
|
||||
if not item.path then item.redirect = false end
|
||||
item.path = item.path or full_path..item.name
|
||||
end
|
||||
|
||||
return list, opts
|
||||
end
|
||||
|
||||
local path = full_paths[ directory:match("([^/]+/?)$") or "" ]
|
||||
|
||||
local list, opts = self:defer(path)
|
||||
if not list then return nil end
|
||||
opts = opts or {}
|
||||
opts.directory = opts.directory or path
|
||||
return list, opts
|
||||
end
|
||||
|
||||
---@param path string
|
||||
---@return integer?
|
||||
---@return Item?
|
||||
local function get_favourite(path)
|
||||
for index, value in ipairs(favourites) do
|
||||
if value.path == path then return index, value end
|
||||
end
|
||||
end
|
||||
|
||||
--update the browser with new contents of the file
|
||||
---@async
|
||||
local function update_browser()
|
||||
if favs.get_directory():find("^[fF]avourites/$") then
|
||||
local cursor = favs.get_selected_index()
|
||||
fb.rescan_await()
|
||||
fb.set_selected_index(cursor)
|
||||
else
|
||||
fb.clear_cache({'favourites/', 'Favourites/'})
|
||||
end
|
||||
end
|
||||
|
||||
--write the contents of favourites to the file
|
||||
local function write_to_file()
|
||||
local file = io.open(save_path, "w+")
|
||||
if not file then return msg.error(file, "could not open favourites file") end
|
||||
for _, item in ipairs(favourites) do
|
||||
file:write(string.format("%s\n", item.path))
|
||||
end
|
||||
file:close()
|
||||
end
|
||||
|
||||
local function add_favourite(path)
|
||||
if get_favourite(path) then return end
|
||||
update_favourites()
|
||||
table.insert(favourites, create_favourite_object(path))
|
||||
write_to_file()
|
||||
end
|
||||
|
||||
local function remove_favourite(path)
|
||||
update_favourites()
|
||||
local index = get_favourite(path)
|
||||
if not index then return end
|
||||
table.remove(favourites, index)
|
||||
write_to_file()
|
||||
end
|
||||
|
||||
local function move_favourite(path, direction)
|
||||
update_favourites()
|
||||
local index, item = get_favourite(path)
|
||||
if not index or not favourites[index + direction] then return end
|
||||
|
||||
favourites[index] = favourites[index + direction]
|
||||
favourites[index + direction] = item
|
||||
write_to_file()
|
||||
end
|
||||
|
||||
---@async
|
||||
local function toggle_favourite(cmd, state, co)
|
||||
local path = fb.get_full_path(state.list[state.selected], state.directory)
|
||||
|
||||
if state.directory:find("[fF]avourites/$") then remove_favourite(path)
|
||||
else add_favourite(path) end
|
||||
update_browser()
|
||||
end
|
||||
|
||||
---@async
|
||||
local function move_key(cmd, state, co)
|
||||
if not state.directory:find("[fF]avourites/") then return false end
|
||||
local path = fb.get_full_path(state.list[state.selected], state.directory)
|
||||
|
||||
local cursor = fb.get_selected_index()
|
||||
if cmd.name == favs:get_id().."/move_up" then
|
||||
move_favourite(path, -1)
|
||||
fb.set_selected_index(cursor-1)
|
||||
else
|
||||
move_favourite(path, 1)
|
||||
fb.set_selected_index(cursor+1)
|
||||
end
|
||||
update_browser()
|
||||
end
|
||||
|
||||
update_favourites()
|
||||
|
||||
favs.keybinds = {
|
||||
{ "F", "toggle_favourite", toggle_favourite, {}, },
|
||||
{ "Ctrl+UP", "move_up", move_key, {repeatable = true} },
|
||||
{ "Ctrl+DOWN", "move_down", move_key, {repeatable = true} },
|
||||
}
|
||||
|
||||
return favs
|
||||
@@ -0,0 +1,39 @@
|
||||
--[[
|
||||
An addon for file-browser which decodes URLs so that they are more readable
|
||||
]]
|
||||
|
||||
---@type ParserConfig
|
||||
local urldecode = {
|
||||
priority = 5,
|
||||
api_version = "1.0.0"
|
||||
}
|
||||
|
||||
--decodes a URL address
|
||||
--this piece of code was taken from: https://stackoverflow.com/questions/20405985/lua-decodeuri-luvit/20406960#20406960
|
||||
---@type fun(s: string): string
|
||||
local decodeURI
|
||||
do
|
||||
local char, gsub, tonumber = string.char, string.gsub, tonumber
|
||||
local function _(hex) return char(tonumber(hex, 16)) end
|
||||
|
||||
function decodeURI(s)
|
||||
s = gsub(s, '%%(%x%x)', _)
|
||||
return s
|
||||
end
|
||||
end
|
||||
|
||||
function urldecode:can_parse(directory)
|
||||
return self.get_protocol(directory) ~= nil
|
||||
end
|
||||
|
||||
---@async
|
||||
function urldecode:parse(directory)
|
||||
local list, opts = self:defer(directory)
|
||||
opts = opts or {}
|
||||
if opts.directory and not self.get_protocol(opts.directory) then return list, opts end
|
||||
|
||||
opts.directory_label = decodeURI(opts.directory_label or (opts.directory or directory))
|
||||
return list, opts
|
||||
end
|
||||
|
||||
return urldecode
|
||||
@@ -0,0 +1,206 @@
|
||||
--[[
|
||||
An addon for mpv-file-browser which adds a Favourites path that can be loaded from the ROOT
|
||||
]]--
|
||||
|
||||
local mp = require "mp"
|
||||
local msg = require "mp.msg"
|
||||
|
||||
local fb = require 'file-browser'
|
||||
local save_path = mp.command_native({"expand-path", "~~/script-opts/file_browser_favourites.txt"}) --[[@as string]]
|
||||
do
|
||||
local file = io.open(save_path, "a+")
|
||||
if not file then
|
||||
msg.error("cannot access file", ("%q"):format(save_path), "make sure that the directory exists")
|
||||
return {}
|
||||
end
|
||||
file:close()
|
||||
end
|
||||
|
||||
---@type Item[]
|
||||
local favourites = {}
|
||||
local favourites_loaded = false
|
||||
|
||||
---@type ParserConfig
|
||||
local favs = {
|
||||
api_version = "1.8.0",
|
||||
priority = 30,
|
||||
cursor = 1
|
||||
}
|
||||
|
||||
local use_virtual_directory = true
|
||||
|
||||
---@type table<string,string>
|
||||
local full_paths = {}
|
||||
|
||||
---@param str string
|
||||
---@return Item
|
||||
local function create_favourite_object(str)
|
||||
local item = {
|
||||
type = str:sub(-1) == "/" and "dir" or "file",
|
||||
path = str,
|
||||
redirect = not use_virtual_directory,
|
||||
name = str:match("([^/]+/?)$")
|
||||
}
|
||||
full_paths[str:match("([^/]+)/?$")] = str
|
||||
return item
|
||||
end
|
||||
|
||||
---@param self Parser
|
||||
function favs:setup()
|
||||
self:register_root_item('Favourites/')
|
||||
end
|
||||
|
||||
local function update_favourites()
|
||||
local file = io.open(save_path, "r")
|
||||
if not file then return end
|
||||
|
||||
favourites = {}
|
||||
for str in file:lines() do
|
||||
table.insert(favourites, create_favourite_object(str))
|
||||
end
|
||||
file:close()
|
||||
favourites_loaded = true
|
||||
end
|
||||
|
||||
function favs:can_parse(directory)
|
||||
return directory:find("Favourites/") == 1
|
||||
end
|
||||
|
||||
---@async
|
||||
---@param self Parser
|
||||
---@param directory string
|
||||
---@return List?
|
||||
---@return Opts?
|
||||
function favs:parse(directory)
|
||||
if not favourites_loaded then update_favourites() end
|
||||
if directory == "Favourites/" then
|
||||
local opts = {
|
||||
filtered = true,
|
||||
sorted = true
|
||||
}
|
||||
return favourites, opts
|
||||
end
|
||||
|
||||
if use_virtual_directory then
|
||||
-- converts the relative favourite path into a full path
|
||||
local name = directory:match("Favourites/([^/]+)/?")
|
||||
|
||||
local _, finish = directory:find("Favourites/([^/]+/?)")
|
||||
local full_path = (full_paths[name] or "")..directory:sub(finish+1)
|
||||
local list, opts = self:defer(full_path or "")
|
||||
|
||||
if not list then return nil end
|
||||
opts = opts or {}
|
||||
opts.id = self:get_id()
|
||||
if opts.directory_label then
|
||||
opts.directory_label = opts.directory_label:gsub(full_paths[name], "Favourites/"..name..'/')
|
||||
if opts.directory_label:find("Favourites/") ~= 1 then opts.directory_label = nil end
|
||||
end
|
||||
|
||||
for _, item in ipairs(list) do
|
||||
if not item.path then item.redirect = false end
|
||||
item.path = item.path or full_path..item.name
|
||||
end
|
||||
|
||||
return list, opts
|
||||
end
|
||||
|
||||
local path = full_paths[ directory:match("([^/]+/?)$") or "" ]
|
||||
|
||||
local list, opts = self:defer(path)
|
||||
if not list then return nil end
|
||||
opts = opts or {}
|
||||
opts.directory = opts.directory or path
|
||||
return list, opts
|
||||
end
|
||||
|
||||
---@param path string
|
||||
---@return integer?
|
||||
---@return Item?
|
||||
local function get_favourite(path)
|
||||
for index, value in ipairs(favourites) do
|
||||
if value.path == path then return index, value end
|
||||
end
|
||||
end
|
||||
|
||||
--update the browser with new contents of the file
|
||||
---@async
|
||||
local function update_browser()
|
||||
if favs.get_directory():find("^[fF]avourites/$") then
|
||||
local cursor = favs.get_selected_index()
|
||||
fb.rescan_await()
|
||||
fb.set_selected_index(cursor)
|
||||
else
|
||||
fb.clear_cache({'favourites/', 'Favourites/'})
|
||||
end
|
||||
end
|
||||
|
||||
--write the contents of favourites to the file
|
||||
local function write_to_file()
|
||||
local file = io.open(save_path, "w+")
|
||||
if not file then return msg.error(file, "could not open favourites file") end
|
||||
for _, item in ipairs(favourites) do
|
||||
file:write(string.format("%s\n", item.path))
|
||||
end
|
||||
file:close()
|
||||
end
|
||||
|
||||
local function add_favourite(path)
|
||||
if get_favourite(path) then return end
|
||||
update_favourites()
|
||||
table.insert(favourites, create_favourite_object(path))
|
||||
write_to_file()
|
||||
end
|
||||
|
||||
local function remove_favourite(path)
|
||||
update_favourites()
|
||||
local index = get_favourite(path)
|
||||
if not index then return end
|
||||
table.remove(favourites, index)
|
||||
write_to_file()
|
||||
end
|
||||
|
||||
local function move_favourite(path, direction)
|
||||
update_favourites()
|
||||
local index, item = get_favourite(path)
|
||||
if not index or not favourites[index + direction] then return end
|
||||
|
||||
favourites[index] = favourites[index + direction]
|
||||
favourites[index + direction] = item
|
||||
write_to_file()
|
||||
end
|
||||
|
||||
---@async
|
||||
local function toggle_favourite(cmd, state, co)
|
||||
local path = fb.get_full_path(state.list[state.selected], state.directory)
|
||||
|
||||
if state.directory:find("[fF]avourites/$") then remove_favourite(path)
|
||||
else add_favourite(path) end
|
||||
update_browser()
|
||||
end
|
||||
|
||||
---@async
|
||||
local function move_key(cmd, state, co)
|
||||
if not state.directory:find("[fF]avourites/") then return false end
|
||||
local path = fb.get_full_path(state.list[state.selected], state.directory)
|
||||
|
||||
local cursor = fb.get_selected_index()
|
||||
if cmd.name == favs:get_id().."/move_up" then
|
||||
move_favourite(path, -1)
|
||||
fb.set_selected_index(cursor-1)
|
||||
else
|
||||
move_favourite(path, 1)
|
||||
fb.set_selected_index(cursor+1)
|
||||
end
|
||||
update_browser()
|
||||
end
|
||||
|
||||
update_favourites()
|
||||
|
||||
favs.keybinds = {
|
||||
{ "F", "toggle_favourite", toggle_favourite, {}, },
|
||||
{ "Ctrl+UP", "move_up", move_key, {repeatable = true} },
|
||||
{ "Ctrl+DOWN", "move_down", move_key, {repeatable = true} },
|
||||
}
|
||||
|
||||
return favs
|
||||
@@ -0,0 +1,45 @@
|
||||
local fb = require "file-browser"
|
||||
local opt = require "mp.options"
|
||||
|
||||
local o = {
|
||||
--list of absolute paths separated by the root separators
|
||||
paths = ""
|
||||
}
|
||||
|
||||
--config file stored in ~~/script-opts/file-browser/filter.conf
|
||||
opt.read_options(o, "file-browser/filter")
|
||||
|
||||
local parser = {
|
||||
priority = 10,
|
||||
api_version = "1.3.0"
|
||||
}
|
||||
|
||||
local paths = {}
|
||||
for str in fb.iterate_opt(o.paths) do
|
||||
paths[str] = true
|
||||
end
|
||||
|
||||
local function filter(path)
|
||||
return paths[path]
|
||||
end
|
||||
|
||||
function parser:can_parse()
|
||||
return true
|
||||
end
|
||||
|
||||
function parser:parse(directory)
|
||||
local list, opts = self:defer(directory)
|
||||
if not list then return list, opts end
|
||||
|
||||
directory = opts.directory or directory
|
||||
|
||||
for i=#list, 1, -1 do
|
||||
if filter( fb.get_full_path(list[i], directory) ) then
|
||||
table.remove(list, i)
|
||||
end
|
||||
end
|
||||
|
||||
return list, opts
|
||||
end
|
||||
|
||||
return parser
|
||||
@@ -0,0 +1,41 @@
|
||||
local mp = require 'mp'
|
||||
local msg = require 'mp.msg'
|
||||
local fb = require 'file-browser'
|
||||
|
||||
local isos = {
|
||||
name = 'iso-loader',
|
||||
priority = 20,
|
||||
api_version = '1.5'
|
||||
}
|
||||
|
||||
function isos:setup()
|
||||
fb.add_default_extension('iso')
|
||||
end
|
||||
|
||||
function isos:can_parse()
|
||||
return true
|
||||
end
|
||||
|
||||
function isos:parse(directory, parse_state)
|
||||
local list, opts = self:defer(directory, parse_state)
|
||||
if not list or #list == 0 then return list, opts end
|
||||
|
||||
for _, item in ipairs(list) do
|
||||
local path = fb.get_full_path(item, opts.directory or directory)
|
||||
if fb.get_extension(path) == 'iso' then
|
||||
item.mpv_options = { ['bluray-device'] = path, ['dvd-device'] = path }
|
||||
item.path = 'bd://'
|
||||
end
|
||||
end
|
||||
|
||||
return list, opts
|
||||
end
|
||||
|
||||
mp.add_hook('on_load_fail', 50, function()
|
||||
if mp.get_property('stream-open-filename') == 'bd://' then
|
||||
msg.info('failed to load bluray-device, attempting dvd-device')
|
||||
mp.set_property('stream-open-filename', 'dvd://')
|
||||
end
|
||||
end)
|
||||
|
||||
return isos
|
||||
@@ -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 }
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
local msg = require 'mp.msg'
|
||||
local utils = require 'mp.utils'
|
||||
local fb = require 'file-browser'
|
||||
|
||||
local parser = {
|
||||
priority = 105,
|
||||
api_version = '1.2.0'
|
||||
}
|
||||
|
||||
-- stores a table of the parsers loaded by file-browser
|
||||
-- we will use this to check if a parser is for a local file system
|
||||
local parsers
|
||||
|
||||
local sort_mode = 0
|
||||
|
||||
function parser:setup()
|
||||
parsers = fb.get_parsers()
|
||||
end
|
||||
|
||||
function parser:parse(directory)
|
||||
if sort_mode == 0 or fb.get_protocol(directory) then return end
|
||||
local list, opts = self:defer(directory)
|
||||
if not list then return list, opts end
|
||||
|
||||
-- Only run this on parsers that are for the local filesystem.
|
||||
-- We assume that custom addons for the local filesystem are setting the keybind_name field to 'file'
|
||||
-- for compatability.
|
||||
if parsers[opts.id] then
|
||||
if parsers[opts.id].keybind_name ~= 'file' and parsers[opts.id].name ~= 'file' then
|
||||
return list, opts
|
||||
end
|
||||
end
|
||||
|
||||
directory = opts.directory or directory
|
||||
local cache = {}
|
||||
|
||||
-- gets the file info of an item
|
||||
-- uses memoisation to speed things up
|
||||
function get_file_info(item)
|
||||
if cache[item] then return cache[item] end
|
||||
|
||||
local path = fb.get_full_path(item, directory)
|
||||
local file_info = utils.file_info(path)
|
||||
if not file_info then
|
||||
msg.warn('failed to read file info for', path)
|
||||
return {}
|
||||
end
|
||||
|
||||
cache[item] = file_info
|
||||
return file_info
|
||||
end
|
||||
|
||||
-- sorts the items based on the latest modification time
|
||||
-- if mtime is undefined due to a file read failure then use 0
|
||||
table.sort(list, function(a, b)
|
||||
-- `dir` will compare as less than `file`
|
||||
if a.type ~= b.type then return a.type < b.type end
|
||||
if sort_mode == 1 then
|
||||
return (get_file_info(a).mtime or 0) < (get_file_info(b).mtime or 0)
|
||||
elseif sort_mode == 2 then
|
||||
return (get_file_info(a).mtime or 0) > (get_file_info(b).mtime or 0)
|
||||
elseif sort_mode == 3 then
|
||||
return (get_file_info(a).size or 0) < (get_file_info(b).size or 0)
|
||||
elseif sort_mode == 4 then
|
||||
return (get_file_info(a).size or 0) > (get_file_info(b).size or 0)
|
||||
end
|
||||
end)
|
||||
|
||||
opts.sorted = true
|
||||
return list, opts
|
||||
end
|
||||
|
||||
-- adds the keybind to toggle sorting
|
||||
parser.keybinds = {
|
||||
{
|
||||
key = '^',
|
||||
name = 'toggle_sort',
|
||||
command = function()
|
||||
sort_mode = sort_mode + 1
|
||||
if sort_mode > 4 then sort_mode = 0 end
|
||||
fb.rescan()
|
||||
end
|
||||
}
|
||||
}
|
||||
|
||||
return parser
|
||||
@@ -0,0 +1,39 @@
|
||||
--[[
|
||||
An addon for file-browser which decodes URLs so that they are more readable
|
||||
]]
|
||||
|
||||
---@type ParserConfig
|
||||
local urldecode = {
|
||||
priority = 5,
|
||||
api_version = "1.0.0"
|
||||
}
|
||||
|
||||
--decodes a URL address
|
||||
--this piece of code was taken from: https://stackoverflow.com/questions/20405985/lua-decodeuri-luvit/20406960#20406960
|
||||
---@type fun(s: string): string
|
||||
local decodeURI
|
||||
do
|
||||
local char, gsub, tonumber = string.char, string.gsub, tonumber
|
||||
local function _(hex) return char(tonumber(hex, 16)) end
|
||||
|
||||
function decodeURI(s)
|
||||
s = gsub(s, '%%(%x%x)', _)
|
||||
return s
|
||||
end
|
||||
end
|
||||
|
||||
function urldecode:can_parse(directory)
|
||||
return self.get_protocol(directory) ~= nil
|
||||
end
|
||||
|
||||
---@async
|
||||
function urldecode:parse(directory)
|
||||
local list, opts = self:defer(directory)
|
||||
opts = opts or {}
|
||||
if opts.directory and not self.get_protocol(opts.directory) then return list, opts end
|
||||
|
||||
opts.directory_label = decodeURI(opts.directory_label or (opts.directory or directory))
|
||||
return list, opts
|
||||
end
|
||||
|
||||
return urldecode
|
||||
@@ -0,0 +1,51 @@
|
||||
local fb = require "file-browser"
|
||||
local fb_utils = require 'modules.utils'
|
||||
|
||||
local PLATFORM = fb.get_platform()
|
||||
|
||||
-- Only enable Windows-specific sorting on Windows platforms
|
||||
if PLATFORM == 'windows' then
|
||||
-- this code is based on https://github.com/mpvnet-player/mpv.net/issues/575#issuecomment-1817413401
|
||||
local ffi = require "ffi"
|
||||
local winapi = {
|
||||
ffi = ffi,
|
||||
C = ffi.C,
|
||||
CP_UTF8 = 65001,
|
||||
shlwapi = ffi.load("shlwapi"),
|
||||
}
|
||||
|
||||
-- ffi code from https://github.com/po5/thumbfast, Mozilla Public License Version 2.0
|
||||
ffi.cdef[[
|
||||
int __stdcall MultiByteToWideChar(unsigned int CodePage, unsigned long dwFlags, const char *lpMultiByteStr,
|
||||
int cbMultiByte, wchar_t *lpWideCharStr, int cchWideChar);
|
||||
int __stdcall StrCmpLogicalW(wchar_t *psz1, wchar_t *psz2);
|
||||
]]
|
||||
|
||||
winapi.utf8_to_wide = function(utf8_str)
|
||||
if utf8_str then
|
||||
local utf16_len = winapi.C.MultiByteToWideChar(winapi.CP_UTF8, 0, utf8_str, -1, nil, 0)
|
||||
|
||||
if utf16_len > 0 then
|
||||
local utf16_str = winapi.ffi.new("wchar_t[?]", utf16_len)
|
||||
|
||||
if winapi.C.MultiByteToWideChar(winapi.CP_UTF8, 0, utf8_str, -1, utf16_str, utf16_len) > 0 then
|
||||
return utf16_str
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return ""
|
||||
end
|
||||
|
||||
fb_utils.sort = function (t)
|
||||
table.sort(t, function(a, b)
|
||||
local a_wide = winapi.utf8_to_wide(a.type:sub(1, 1) .. (a.label or a.name))
|
||||
local b_wide = winapi.utf8_to_wide(b.type:sub(1, 1) .. (b.label or b.name))
|
||||
return winapi.shlwapi.StrCmpLogicalW(a_wide, b_wide) == -1
|
||||
end)
|
||||
|
||||
return t
|
||||
end
|
||||
end
|
||||
|
||||
return { api_version = '1.2.0' }
|
||||
Reference in New Issue
Block a user