mirror of
https://github.com/Dichgrem/DCGOS.git
synced 2025-12-17 11:41:59 -05:00
1440 lines
48 KiB
Lua
Vendored
1440 lines
48 KiB
Lua
Vendored
-- memo.lua
|
||
--
|
||
-- A recent files menu for mpv
|
||
|
||
local options = {
|
||
-- File path gets expanded, leave empty for in-memory history
|
||
history_path = "~~/memo-history.log",
|
||
|
||
-- How many entries to display in menu
|
||
entries = 10,
|
||
|
||
-- Display navigation to older/newer entries
|
||
pagination = true,
|
||
|
||
-- Display files only once
|
||
hide_duplicates = true,
|
||
|
||
-- Check if files still exist
|
||
hide_deleted = true,
|
||
|
||
-- Display only the latest file from each directory
|
||
hide_same_dir = false,
|
||
|
||
-- Date format https://www.lua.org/pil/22.1.html
|
||
timestamp_format = "%Y-%m-%d %H:%M:%S",
|
||
|
||
-- Display titles instead of filenames when available
|
||
use_titles = true,
|
||
|
||
-- Truncate titles to n characters, 0 to disable
|
||
truncate_titles = 60,
|
||
|
||
-- Meant for use in auto profiles
|
||
enabled = true,
|
||
|
||
-- Keybinds for vanilla menu
|
||
up_binding = "UP WHEEL_UP",
|
||
down_binding = "DOWN WHEEL_DOWN",
|
||
select_binding = "RIGHT ENTER",
|
||
append_binding = "Shift+RIGHT Shift+ENTER",
|
||
close_binding = "LEFT ESC",
|
||
|
||
-- Path prefixes for the recent directory menu
|
||
-- This can be used to restrict the parent directory relative to which the
|
||
-- directories are shown.
|
||
-- Syntax
|
||
-- Prefixes are separated by | and can use Lua patterns by prefixing
|
||
-- them with "pattern:", otherwise they will be treated as plain text.
|
||
-- Pattern syntax can be found here https://www.lua.org/manual/5.1/manual.html#5.4.1
|
||
-- Example
|
||
-- "path_prefixes=My-Movies|pattern:TV Shows/.-/|Anime" will show directories
|
||
-- that are direct subdirectories of directories named "My-Movies" as well as
|
||
-- "Anime", while for TV Shows the shown directories are one level below that.
|
||
-- Opening the file "/data/TV Shows/Comedy/Curb Your Enthusiasm/S4/E06.mkv" will
|
||
-- lead to "Curb Your Enthusiasm" to be shown in the directory menu. Opening
|
||
-- of that entry will then open that file again.
|
||
path_prefixes = "pattern:.*"
|
||
}
|
||
|
||
function parse_path_prefixes(path_prefixes)
|
||
local patterns = {}
|
||
for prefix in path_prefixes:gmatch("([^|]+)") do
|
||
if prefix:find("pattern:", 1, true) == 1 then
|
||
patterns[#patterns + 1] = {pattern = prefix:sub(9)}
|
||
else
|
||
patterns[#patterns + 1] = {pattern = prefix, plain = true}
|
||
end
|
||
end
|
||
return patterns
|
||
end
|
||
|
||
local script_name = mp.get_script_name()
|
||
|
||
mp.utils = require "mp.utils"
|
||
mp.options = require "mp.options"
|
||
mp.options.read_options(options, "memo", function(list)
|
||
if list.path_prefixes then
|
||
options.path_prefixes = parse_path_prefixes(options.path_prefixes)
|
||
end
|
||
end)
|
||
options.path_prefixes = parse_path_prefixes(options.path_prefixes)
|
||
|
||
local assdraw = require "mp.assdraw"
|
||
|
||
local osd = mp.create_osd_overlay("ass-events")
|
||
osd.z = 2000
|
||
local osd_update = nil
|
||
local width, height = 0, 0
|
||
local margin_top, margin_bottom = 0, 0
|
||
local font_size = mp.get_property_number("osd-font-size") or 55
|
||
|
||
local fakeio = {data = "", cursor = 0, offset = 0, file = nil}
|
||
function fakeio:setvbuf(mode) end
|
||
function fakeio:flush()
|
||
self.cursor = self.offset + #self.data
|
||
end
|
||
function fakeio:read(format)
|
||
local out = ""
|
||
if self.cursor < self.offset then
|
||
self.file:seek("set", self.cursor)
|
||
out = self.file:read(format)
|
||
format = format - #out
|
||
self.cursor = self.cursor + #out
|
||
end
|
||
if format > 0 then
|
||
out = out .. self.data:sub(self.cursor - self.offset, self.cursor - self.offset + format)
|
||
self.cursor = self.cursor + format
|
||
end
|
||
return out
|
||
end
|
||
function fakeio:seek(whence, offset)
|
||
local base = 0
|
||
offset = offset or 0
|
||
if whence == "end" then
|
||
base = self.offset + #self.data
|
||
end
|
||
self.cursor = base + offset
|
||
return self.cursor
|
||
end
|
||
function fakeio:write(...)
|
||
local args = {...}
|
||
for i, v in ipairs(args) do
|
||
self.data = self.data .. v
|
||
end
|
||
end
|
||
|
||
local history, history_path
|
||
|
||
if options.history_path ~= "" then
|
||
history_path = mp.command_native({"expand-path", options.history_path})
|
||
history = io.open(history_path, "a+b")
|
||
end
|
||
if history == nil then
|
||
if history_path then
|
||
mp.msg.warn("cannot write to history file " .. options.history_path .. ", new entries will not be saved to disk")
|
||
history = io.open(history_path, "rb")
|
||
if history then
|
||
fakeio.offset = history:seek("end")
|
||
fakeio.file = history
|
||
end
|
||
end
|
||
history = fakeio
|
||
end
|
||
history:setvbuf("full")
|
||
|
||
local event_loop_exhausted = false
|
||
local uosc_available = false
|
||
local dyn_menu = nil
|
||
local menu_shown = false
|
||
local last_state = nil
|
||
local menu_data = nil
|
||
local palette = false
|
||
local search_words = nil
|
||
local search_query = nil
|
||
local dir_menu = false
|
||
local dir_menu_prefixes = nil
|
||
local new_loadfile = nil
|
||
local normalize_path = nil
|
||
|
||
local data_protocols = {
|
||
edl = true,
|
||
data = true,
|
||
null = true,
|
||
memory = true,
|
||
hex = true,
|
||
fd = true,
|
||
fdclose = true,
|
||
mf = true
|
||
}
|
||
|
||
local stacked_protocols = {
|
||
ffmpeg = true,
|
||
lavf = true,
|
||
appending = true,
|
||
file = true,
|
||
archive = true,
|
||
slice = true
|
||
}
|
||
|
||
local device_protocols = {
|
||
bd = true,
|
||
br = true,
|
||
bluray = true,
|
||
cdda = true,
|
||
dvb = true,
|
||
dvd = true,
|
||
dvdnav = true
|
||
}
|
||
|
||
function utf8_char_bytes(str, i)
|
||
local char_byte = str:byte(i)
|
||
local max_bytes = #str - i + 1
|
||
if char_byte < 0xC0 then
|
||
return math.min(max_bytes, 1)
|
||
elseif char_byte < 0xE0 then
|
||
return math.min(max_bytes, 2)
|
||
elseif char_byte < 0xF0 then
|
||
return math.min(max_bytes, 3)
|
||
elseif char_byte < 0xF8 then
|
||
return math.min(max_bytes, 4)
|
||
else
|
||
return math.min(max_bytes, 1)
|
||
end
|
||
end
|
||
|
||
function utf8_iter(str)
|
||
local byte_start = 1
|
||
return function()
|
||
local start = byte_start
|
||
if #str < start then return nil end
|
||
local byte_count = utf8_char_bytes(str, start)
|
||
byte_start = start + byte_count
|
||
return start, str:sub(start, byte_start - 1)
|
||
end
|
||
end
|
||
|
||
function utf8_table(str)
|
||
local t = {}
|
||
local width = 0
|
||
for _, char in utf8_iter(str) do
|
||
width = width + (#char > 2 and 2 or 1)
|
||
table.insert(t, char)
|
||
end
|
||
return t, width
|
||
end
|
||
|
||
function utf8_subwidth(t, start_index, end_index)
|
||
local index = 1
|
||
local substr = ""
|
||
for _, char in ipairs(t) do
|
||
if start_index <= index and index <= end_index then
|
||
local width = #char > 2 and 2 or 1
|
||
index = index + width
|
||
substr = substr .. char
|
||
end
|
||
end
|
||
return substr, index
|
||
end
|
||
|
||
function utf8_subwidth_back(t, num_chars)
|
||
local index = 0
|
||
local substr = ""
|
||
for i = #t, 1, -1 do
|
||
if num_chars > index then
|
||
local width = #t[i] > 2 and 2 or 1
|
||
index = index + width
|
||
substr = t[i] .. substr
|
||
end
|
||
end
|
||
return substr
|
||
end
|
||
|
||
function utf8_to_unicode(str, i)
|
||
local byte_count = utf8_char_bytes(str, i)
|
||
local char_byte = str:byte(i)
|
||
local unicode = char_byte
|
||
if byte_count ~= 1 then
|
||
local shift = 2 ^ (8 - byte_count)
|
||
char_byte = char_byte - math.floor(0xFF / shift) * shift
|
||
unicode = char_byte * (2 ^ 6) ^ (byte_count - 1)
|
||
end
|
||
for j = 2, byte_count do
|
||
char_byte = str:byte(i + j - 1) - 0x80
|
||
unicode = unicode + char_byte * (2 ^ 6) ^ (byte_count - j)
|
||
end
|
||
return math.floor(unicode + 0.5)
|
||
end
|
||
|
||
function ass_clean(str)
|
||
str = str:gsub("\\", "\\\239\187\191")
|
||
str = str:gsub("{", "\\{")
|
||
str = str:gsub("}", "\\}")
|
||
return str
|
||
end
|
||
|
||
-- Extended from https://stackoverflow.com/a/73283799 with zero-width handling from uosc
|
||
function unaccent(str)
|
||
local unimask = "[%z\1-\127\194-\244][\128-\191]*"
|
||
|
||
-- "Basic Latin".."Latin-1 Supplement".."Latin Extended-A".."Latin Extended-B"
|
||
local charmap =
|
||
"AÀÁÂÃÄÅĀĂĄǍǞǠǺȀȂȦȺAEÆǢǼ"..
|
||
"BßƁƂƄɃ"..
|
||
"CÇĆĈĊČƆƇȻ"..
|
||
"DÐĎĐƉƊDZƻDŽDZDzDžDz"..
|
||
"EÈÉÊËĒĔĖĘĚƎƏƐȄȆȨɆ"..
|
||
"FƑ"..
|
||
"GĜĞĠĢƓǤǦǴ"..
|
||
"HĤĦȞHuǶ"..
|
||
"IÌÍÎÏĨĪĬĮİƖƗǏȈȊIJIJ"..
|
||
"JĴɈ"..
|
||
"KĶƘǨ"..
|
||
"LĹĻĽĿŁȽLJLJLjLj"..
|
||
"NÑŃŅŇŊƝǸȠNJNJNjNj"..
|
||
"OÒÓÔÕÖØŌŎŐƟƠǑǪǬǾȌȎȪȬȮȰOEŒOIƢOUȢ"..
|
||
"PÞƤǷ"..
|
||
"QɊ"..
|
||
"RŔŖŘȐȒɌ"..
|
||
"SŚŜŞŠƧƩƪƼȘ"..
|
||
"TŢŤŦƬƮȚȾ"..
|
||
"UÙÚÛÜŨŪŬŮŰŲƯƱƲȔȖɄǓǕǗǙǛ"..
|
||
"VɅ"..
|
||
"WŴƜ"..
|
||
"YÝŶŸƳȜȲɎ"..
|
||
"ZŹŻŽƵƷƸǮȤ"..
|
||
"aàáâãäåāăąǎǟǡǻȁȃȧaeæǣǽ"..
|
||
"bƀƃƅ"..
|
||
"cçćĉċčƈȼ"..
|
||
"dðƌƋƍȡďđdbȸdzdždz"..
|
||
"eèéêëēĕėęěǝȅȇȩɇ"..
|
||
"fƒ"..
|
||
"gĝğġģƔǥǧǵ"..
|
||
"hĥħȟhvƕ"..
|
||
"iìíîïĩīĭįıǐȉȋijij"..
|
||
"jĵǰȷɉ"..
|
||
"kķĸƙǩ"..
|
||
"lĺļľŀłƚƛȴljlj"..
|
||
"nñńņňʼnŋƞǹȵnjnj"..
|
||
"oòóôõöøōŏőơǒǫǭǿȍȏȫȭȯȱoeœoiƣouȣ"..
|
||
"pþƥƿ"..
|
||
"qɋqpȹ"..
|
||
"rŕŗřƦȑȓɍ"..
|
||
"sśŝşšſƨƽșȿ"..
|
||
"tţťŧƫƭțȶtsƾ"..
|
||
"uùúûüũūŭůűųưǔǖǘǚǜȕȗ"..
|
||
"wŵ"..
|
||
"yýÿŷƴȝȳɏ"..
|
||
"zźżžƶƹƺǯȥɀ"
|
||
|
||
local zero_width_blocks = {
|
||
{0x0000, 0x001F}, -- C0
|
||
{0x007F, 0x009F}, -- Delete + C1
|
||
{0x034F, 0x034F}, -- combining grapheme joiner
|
||
{0x061C, 0x061C}, -- Arabic Letter Strong
|
||
{0x200B, 0x200F}, -- {zero-width space, zero-width non-joiner, zero-width joiner, left-to-right mark, right-to-left mark}
|
||
{0x2028, 0x202E}, -- {line separator, paragraph separator, Left-to-Right Embedding, Right-to-Left Embedding, Pop Directional Format, Left-to-Right Override, Right-to-Left Override}
|
||
{0x2060, 0x2060}, -- word joiner
|
||
{0x2066, 0x2069}, -- {Left-to-Right Isolate, Right-to-Left Isolate, First Strong Isolate, Pop Directional Isolate}
|
||
{0xFEFF, 0xFEFF}, -- zero-width non-breaking space
|
||
-- Some other characters can also be combined https://en.wikipedia.org/wiki/Combining_character
|
||
{0x0300, 0x036F}, -- Combining Diacritical Marks 0 BMP Inherited
|
||
{0x1AB0, 0x1AFF}, -- Combining Diacritical Marks Extended 0 BMP Inherited
|
||
{0x1DC0, 0x1DFF}, -- Combining Diacritical Marks Supplement 0 BMP Inherited
|
||
{0x20D0, 0x20FF}, -- Combining Diacritical Marks for Symbols 0 BMP Inherited
|
||
{0xFE20, 0xFE2F}, -- Combining Half Marks 0 BMP Cyrillic (2 characters), Inherited (14 characters)
|
||
-- Egyptian Hieroglyph Format Controls and Shorthand format Controls
|
||
{0x13430, 0x1345F}, -- Egyptian Hieroglyph Format Controls 1 SMP Egyptian Hieroglyphs
|
||
{0x1BCA0, 0x1BCAF}, -- Shorthand Format Controls 1 SMP Common
|
||
-- not sure how to deal with those https://en.wikipedia.org/wiki/Spacing_Modifier_Letters
|
||
{0x02B0, 0x02FF}, -- Spacing Modifier Letters 0 BMP Bopomofo (2 characters), Latin (14 characters), Common (64 characters)
|
||
}
|
||
|
||
return str:gsub(unimask, function(unichar)
|
||
local unicode = utf8_to_unicode(unichar, 1)
|
||
for _, block in ipairs(zero_width_blocks) do
|
||
if unicode >= block[1] and unicode <= block[2] then
|
||
return ""
|
||
end
|
||
end
|
||
|
||
return unichar:match("%a") or charmap:match("(%a+)[^%a]-"..(unichar:gsub("[%(%)%.%%%+%-%*%?%[%^%$]", "%%%1")))
|
||
end)
|
||
end
|
||
|
||
function shallow_copy(t)
|
||
local t2 = {}
|
||
for k,v in pairs(t) do
|
||
t2[k] = v
|
||
end
|
||
return t2
|
||
end
|
||
|
||
function has_protocol(path)
|
||
return path:find("^%a[%w.+-]-://") or path:find("^%a[%w.+-]-:%?")
|
||
end
|
||
|
||
function normalize(path)
|
||
if normalize_path ~= nil then
|
||
if normalize_path then
|
||
-- don't normalize magnet-style paths
|
||
local protocol_start, protocol_end, protocol = path:find("^(%a[%w.+-]-):%?")
|
||
if not protocol_end then
|
||
path = mp.command_native({"normalize-path", path})
|
||
end
|
||
else
|
||
-- TODO: implement the basics of path normalization ourselves for mpv 0.38.0 and under
|
||
local directory = mp.get_property("working-directory", "")
|
||
if not has_protocol(path) then
|
||
path = mp.utils.join_path(directory, path)
|
||
end
|
||
end
|
||
return path
|
||
end
|
||
|
||
normalize_path = false
|
||
|
||
local commands = mp.get_property_native("command-list", {})
|
||
for _, command in ipairs(commands) do
|
||
if command.name == "loadfile" then
|
||
for _, arg in ipairs(command.args) do
|
||
if arg.name == "index" then
|
||
new_loadfile = true
|
||
break
|
||
end
|
||
end
|
||
end
|
||
if command.name == "normalize-path" then
|
||
normalize_path = true
|
||
break
|
||
end
|
||
end
|
||
return normalize(path)
|
||
end
|
||
|
||
function loadfile_compat(path)
|
||
if new_loadfile ~= nil then
|
||
if new_loadfile then
|
||
return {"-1", path}
|
||
end
|
||
return {path}
|
||
end
|
||
|
||
new_loadfile = false
|
||
|
||
local commands = mp.get_property_native("command-list", {})
|
||
for _, command in ipairs(commands) do
|
||
if command.name == "loadfile" then
|
||
for _, arg in ipairs(command.args) do
|
||
if arg.name == "index" then
|
||
new_loadfile = true
|
||
return {"-1", path}
|
||
end
|
||
end
|
||
return {path}
|
||
end
|
||
end
|
||
return {path}
|
||
end
|
||
|
||
function menu_json(menu_items, page)
|
||
local title = (search_query or (dir_menu and "Directories" or "History")) .. ""
|
||
if options.pagination or page ~= 1 then
|
||
title = title .. " - Page " .. page
|
||
end
|
||
|
||
local menu = {
|
||
type = "memo-history",
|
||
title = title,
|
||
items = menu_items,
|
||
on_search = {"script-message-to", script_name, "memo-search-uosc:"},
|
||
on_close = {"script-message-to", script_name, "memo-clear"},
|
||
palette = palette, -- TODO: remove on next uosc release
|
||
search_style = palette and "palette" or nil
|
||
}
|
||
|
||
return menu
|
||
end
|
||
|
||
function uosc_update()
|
||
local json = mp.utils.format_json(menu_data) or "{}"
|
||
mp.commandv("script-message-to", "uosc", menu_shown and "update-menu" or "open-menu", json)
|
||
end
|
||
|
||
function update_dimensions()
|
||
width, height = mp.get_osd_size()
|
||
osd.res_x = width
|
||
osd.res_y = height
|
||
draw_menu()
|
||
end
|
||
|
||
if mp.utils.shared_script_property_set then
|
||
function update_margins()
|
||
local shared_props = mp.get_property_native("shared-script-properties")
|
||
local val = shared_props["osc-margins"]
|
||
if val then
|
||
-- formatted as "%f,%f,%f,%f" with left, right, top, bottom, each
|
||
-- value being the border size as ratio of the window size (0.0-1.0)
|
||
local vals = {}
|
||
for v in string.gmatch(val, "[^,]+") do
|
||
vals[#vals + 1] = tonumber(v)
|
||
end
|
||
margin_top = vals[3] -- top
|
||
margin_bottom = vals[4] -- bottom
|
||
else
|
||
margin_top = 0
|
||
margin_bottom = 0
|
||
end
|
||
draw_menu()
|
||
end
|
||
else
|
||
function update_margins()
|
||
local val = mp.get_property_native("user-data/osc/margins")
|
||
if val then
|
||
margin_top = val.t
|
||
margin_bottom = val.b
|
||
else
|
||
margin_top = 0
|
||
margin_bottom = 0
|
||
end
|
||
draw_menu()
|
||
end
|
||
end
|
||
|
||
function bind_keys(keys, name, func, opts)
|
||
if not keys then
|
||
mp.add_forced_key_binding(keys, name, func, opts)
|
||
return
|
||
end
|
||
local i = 1
|
||
for key in keys:gmatch("[^%s]+") do
|
||
local prefix = i == 1 and "" or i
|
||
mp.add_forced_key_binding(key, name .. prefix, func, opts)
|
||
i = i + 1
|
||
end
|
||
end
|
||
|
||
function unbind_keys(keys, name)
|
||
if not keys then
|
||
mp.remove_key_binding(name)
|
||
return
|
||
end
|
||
local i = 1
|
||
for key in keys:gmatch("[^%s]+") do
|
||
local prefix = i == 1 and "" or i
|
||
mp.remove_key_binding(name .. prefix)
|
||
i = i + 1
|
||
end
|
||
end
|
||
|
||
function close_menu()
|
||
mp.unobserve_property(update_dimensions)
|
||
mp.unobserve_property(update_margins)
|
||
unbind_keys(options.up_binding, "move_up")
|
||
unbind_keys(options.down_binding, "move_down")
|
||
unbind_keys(options.select_binding, "select")
|
||
unbind_keys(options.append_binding, "append")
|
||
unbind_keys(options.close_binding, "close")
|
||
last_state = nil
|
||
menu_data = nil
|
||
search_words = nil
|
||
search_query = nil
|
||
dir_menu = false
|
||
menu_shown = false
|
||
palette = false
|
||
osd:update()
|
||
osd.hidden = true
|
||
osd:update()
|
||
end
|
||
|
||
function open_menu()
|
||
menu_shown = true
|
||
|
||
update_dimensions()
|
||
mp.observe_property("osd-dimensions", "native", update_dimensions)
|
||
mp.observe_property("video-out-params", "native", update_dimensions)
|
||
local margin_prop = mp.utils.shared_script_property_set and "shared-script-properties" or "user-data/osc/margins"
|
||
mp.observe_property(margin_prop, "native", update_margins)
|
||
|
||
local function select_item(append)
|
||
local item = menu_data.items[last_state.selected_index]
|
||
if not item then return end
|
||
if not item.keep_open then
|
||
close_menu()
|
||
end
|
||
if append and item.value[1] == "loadfile" then
|
||
-- bail if file is already in playlist
|
||
local playlist = mp.get_property_native("playlist", {})
|
||
for i = 1, #playlist do
|
||
local playlist_file = playlist[i].filename
|
||
local display_path, save_path, effective_path, effective_protocol, is_remote, file_options = path_info(playlist_file)
|
||
if not is_remote then
|
||
playlist_file = normalize(save_path)
|
||
end
|
||
if item.value[2] == playlist_file then
|
||
return
|
||
end
|
||
end
|
||
item.value[3] = "append-play"
|
||
end
|
||
mp.commandv(unpack(item.value))
|
||
end
|
||
|
||
bind_keys(options.up_binding, "move_up", function()
|
||
last_state.selected_index = math.max(last_state.selected_index - 1, 1)
|
||
draw_menu()
|
||
end, { repeatable = true })
|
||
bind_keys(options.down_binding, "move_down", function()
|
||
last_state.selected_index = math.min(last_state.selected_index + 1, #menu_data.items)
|
||
draw_menu()
|
||
end, { repeatable = true })
|
||
bind_keys(options.select_binding, "select", select_item)
|
||
bind_keys(options.append_binding, "append", function()
|
||
select_item(true)
|
||
end)
|
||
bind_keys(options.close_binding, "close", close_menu)
|
||
osd.hidden = false
|
||
draw_menu()
|
||
end
|
||
|
||
function draw_menu()
|
||
if not menu_data then return end
|
||
if not menu_shown then
|
||
open_menu()
|
||
end
|
||
|
||
local num_options = #menu_data.items > 0 and #menu_data.items + 1 or 1
|
||
last_state.selected_index = math.min(last_state.selected_index, #menu_data.items)
|
||
|
||
local function get_scrolled_lines()
|
||
local output_height = height - margin_top * height - margin_bottom * height - 0.2 * font_size + 0.5
|
||
local screen_lines = math.max(math.floor(output_height / font_size), 1)
|
||
local max_scroll = math.max(num_options - screen_lines, 0)
|
||
return math.min(math.max(last_state.selected_index - math.ceil(screen_lines / 2), 0), max_scroll) - 1
|
||
end
|
||
|
||
local ass = assdraw.ass_new()
|
||
local curtain_opacity = 0.7
|
||
|
||
local alpha = 255 - math.ceil(255 * curtain_opacity)
|
||
ass.text = string.format("{\\pos(0,0)\\rDefault\\an7\\1c&H000000&\\alpha&H%X&}", alpha)
|
||
ass:draw_start()
|
||
ass:rect_cw(0, 0, width, height)
|
||
ass:draw_stop()
|
||
ass:new_event()
|
||
|
||
ass:append("{\\rDefault\\pos("..(0.3 * font_size).."," .. (margin_top * height + 0.1 * font_size) .. ")\\an7\\fs" .. font_size .. "\\bord2\\q2\\b1}" .. ass_clean(menu_data.title) .. "{\\b0}")
|
||
ass:new_event()
|
||
|
||
local scrolled_lines = get_scrolled_lines() - 1
|
||
local pos_y = margin_top * height - scrolled_lines * font_size + 0.2 * font_size + 0.5
|
||
local clip_top = math.floor(margin_top * height + font_size + 0.2 * font_size + 0.5)
|
||
local clip_bottom = math.floor((1 - margin_bottom) * height + 0.5)
|
||
local clipping_coordinates = "0," .. clip_top .. "," .. width .. "," .. clip_bottom
|
||
|
||
if #menu_data.items > 0 then
|
||
local menu_index = 0
|
||
for i = 1, #menu_data.items do
|
||
local item = menu_data.items[i]
|
||
if item.title then
|
||
local icon
|
||
local separator = last_state.selected_index == i and "{\\alpha&HFF&}●{\\alpha&H00&} - " or "{\\alpha&HFF&}●{\\alpha&H00&} - "
|
||
if item.icon == "spinner" then
|
||
separator = "⟳ "
|
||
elseif item.icon == "navigate_next" then
|
||
icon = last_state.selected_index == i and "▶" or "▷"
|
||
elseif item.icon == "navigate_before" then
|
||
icon = last_state.selected_index == i and "◀" or "◁"
|
||
else
|
||
icon = last_state.selected_index == i and "●" or "○"
|
||
end
|
||
ass:new_event()
|
||
ass:pos(0.3 * font_size, pos_y + menu_index * font_size)
|
||
ass:append("{\\rDefault\\fnmonospace\\an1\\fs" .. font_size .. "\\bord2\\q2\\clip(" .. clipping_coordinates .. ")}"..separator.."{\\rDefault\\an7\\fs" .. font_size .. "\\bord2\\q2}" .. ass_clean(item.title))
|
||
if icon then
|
||
ass:new_event()
|
||
ass:pos(0.6 * font_size, pos_y + menu_index * font_size)
|
||
ass:append("{\\rDefault\\fnmonospace\\an2\\fs" .. font_size .. "\\bord2\\q2\\clip(" .. clipping_coordinates .. ")}" .. icon)
|
||
end
|
||
menu_index = menu_index + 1
|
||
end
|
||
end
|
||
else
|
||
ass:pos(0.3 * font_size, pos_y)
|
||
ass:append("{\\rDefault\\an1\\fs" .. font_size .. "\\bord2\\q2\\clip(" .. clipping_coordinates .. ")}")
|
||
ass:append("No entries")
|
||
end
|
||
|
||
osd_update = nil
|
||
osd.data = ass.text
|
||
osd:update()
|
||
end
|
||
|
||
function get_full_path()
|
||
local path = mp.get_property("path")
|
||
if path == nil or path == "-" or path == "/dev/stdin" then return end
|
||
|
||
local display_path, save_path, effective_path, effective_protocol, is_remote, file_options = path_info(path)
|
||
|
||
if not is_remote then
|
||
path = normalize(save_path)
|
||
end
|
||
|
||
return path, display_path, save_path, effective_path, effective_protocol, is_remote, file_options
|
||
end
|
||
|
||
function path_info(full_path)
|
||
local function resolve(effective_path, save_path, display_path, last_protocol, is_remote)
|
||
local protocol_start, protocol_end, protocol = display_path:find("^(%a[%w.+-]-)://")
|
||
|
||
if protocol == "ytdl" then
|
||
-- for direct video access ytdl://videoID and ytsearch:
|
||
is_remote = true
|
||
elseif protocol and not stacked_protocols[protocol] then
|
||
local input_path, file_options
|
||
if device_protocols[protocol] then
|
||
input_path, file_options = display_path:match("(.-) %-%-opt=(.+)")
|
||
effective_path = file_options and file_options:match(".+=(.*)")
|
||
if protocol == "dvb" then
|
||
is_remote = true
|
||
if not effective_path then
|
||
effective_path = display_path
|
||
input_path = display_path:sub(protocol_end + 1)
|
||
end
|
||
end
|
||
display_path = input_path or display_path
|
||
else
|
||
is_remote = true
|
||
display_path = display_path:sub(protocol_end + 1)
|
||
end
|
||
return display_path, save_path, effective_path, protocol, is_remote, file_options
|
||
end
|
||
|
||
if not protocol_end then
|
||
if last_protocol == "ytdl" then
|
||
display_path = "ytdl://" .. display_path
|
||
end
|
||
return display_path, save_path, effective_path, last_protocol, is_remote, nil
|
||
end
|
||
|
||
display_path = display_path:sub(protocol_end + 1)
|
||
|
||
if protocol == "archive" then
|
||
local main_path, archive_path, filename = display_path:gsub("%%7C", "|"):match("(.-)(|.-[\\/])(.+)")
|
||
if not main_path then
|
||
local main_path = display_path:match("(.-)|")
|
||
effective_path = normalize(main_path or display_path)
|
||
_, save_path, effective_path, protocol, is_remote, file_options = resolve(effective_path, save_path, display_path, protocol, is_remote)
|
||
effective_path = normalize(effective_path)
|
||
save_path = "archive://" .. (save_path or effective_path)
|
||
if main_path then
|
||
save_path = save_path .. display_path:match("|(.-)")
|
||
end
|
||
else
|
||
display_path, save_path, _, protocol, is_remote, file_options = resolve(main_path, save_path, main_path, protocol, is_remote)
|
||
effective_path = normalize(display_path)
|
||
save_path = save_path or effective_path
|
||
save_path = "archive://" .. save_path .. (save_path:find("archive://") and archive_path:gsub("|", "%%7C") or archive_path) .. filename
|
||
_, main_path = mp.utils.split_path(main_path)
|
||
_, filename = mp.utils.split_path(filename)
|
||
display_path = main_path .. ": " .. filename
|
||
end
|
||
elseif protocol == "slice" then
|
||
if effective_path then
|
||
effective_path = effective_path:match(".-@(.*)") or effective_path
|
||
end
|
||
display_path = display_path:match(".-@(.*)") or display_path
|
||
end
|
||
|
||
return resolve(effective_path, save_path, display_path, protocol, is_remote)
|
||
end
|
||
|
||
-- don't resolve magnet-style paths
|
||
local protocol_start, protocol_end, protocol = full_path:find("^(%a[%w.+-]-):%?")
|
||
if protocol_end then
|
||
return full_path, full_path, protocol, true, nil
|
||
end
|
||
|
||
local display_path, save_path, effective_path, effective_protocol, is_remote, file_options = resolve(nil, nil, full_path, nil, false)
|
||
effective_path = effective_path or display_path
|
||
save_path = save_path or effective_path
|
||
if is_remote and not file_options then
|
||
display_path = display_path:gsub("%%(%x%x)", function(hex)
|
||
return string.char(tonumber(hex, 16))
|
||
end)
|
||
end
|
||
|
||
return display_path, save_path, effective_path, effective_protocol, is_remote, file_options
|
||
end
|
||
|
||
function write_history(display)
|
||
local full_path, display_path, save_path, effective_path, effective_protocol, is_remote, file_options = get_full_path()
|
||
if full_path == nil then
|
||
mp.msg.debug("cannot get full path to file")
|
||
if display then
|
||
mp.osd_message("[memo] cannot get full path to file")
|
||
end
|
||
return
|
||
end
|
||
|
||
if data_protocols[effective_protocol] then
|
||
mp.msg.debug("not logging file with " .. effective_protocol .. " protocol")
|
||
if display then
|
||
mp.osd_message("[memo] not logging file with " .. effective_protocol .. " protocol")
|
||
end
|
||
return
|
||
end
|
||
|
||
if effective_protocol == "bd" or effective_protocol == "br" or effective_protocol == "bluray" then
|
||
full_path = full_path .. " --opt=bluray-device=" .. mp.get_property("bluray-device", "")
|
||
elseif effective_protocol == "cdda" then
|
||
full_path = full_path .. " --opt=cdrom-device=" .. mp.get_property("cdrom-device", "")
|
||
elseif effective_protocol == "dvb" then
|
||
local dvb_program = mp.get_property("dvbin-prog", "")
|
||
if dvb_program ~= "" then
|
||
full_path = full_path .. " --opt=dvbin-prog=" .. dvb_program
|
||
end
|
||
elseif effective_protocol == "dvd" or effective_protocol == "dvdnav" then
|
||
full_path = full_path .. " --opt=dvd-angle=" .. mp.get_property("dvd-angle", "1") .. ",dvd-device=" .. mp.get_property("dvd-device", "")
|
||
end
|
||
|
||
mp.msg.debug("logging file " .. full_path)
|
||
if display then
|
||
mp.osd_message("[memo] logging file " .. full_path)
|
||
end
|
||
|
||
local playlist_pos = mp.get_property_number("playlist-pos") or -1
|
||
local title = playlist_pos > -1 and mp.get_property("playlist/"..playlist_pos.."/title") or ""
|
||
local title_length = #title
|
||
local timestamp = os.time()
|
||
|
||
-- format: <timestamp>,<title length>,<title>,<path>,<entry length>
|
||
local entry = timestamp .. "," .. (title_length > 0 and title_length or "") .. "," .. title .. "," .. full_path
|
||
local entry_length = #entry
|
||
|
||
history:seek("end")
|
||
history:write(entry .. "," .. entry_length, "\n")
|
||
history:flush()
|
||
|
||
if dyn_menu then
|
||
dyn_menu_update()
|
||
end
|
||
end
|
||
|
||
function show_history(entries, next_page, prev_page, update, return_items)
|
||
if event_loop_exhausted then return end
|
||
event_loop_exhausted = true
|
||
|
||
local should_close = menu_shown and not prev_page and not next_page and not update
|
||
if should_close then
|
||
memo_close()
|
||
if not return_items then
|
||
return
|
||
end
|
||
end
|
||
|
||
local max_digits_length = 4 + 2
|
||
local retry_offset = 512
|
||
local menu_items = {}
|
||
local state = (prev_page or next_page) and last_state or {
|
||
known_dirs = {},
|
||
known_files = {},
|
||
existing_files = {},
|
||
cursor = history:seek("end"),
|
||
retry = 0,
|
||
pages = {},
|
||
current_page = 1,
|
||
selected_index = 1
|
||
}
|
||
|
||
if update then
|
||
state.pages = {}
|
||
end
|
||
|
||
if last_state then
|
||
if prev_page then
|
||
if state.current_page == 1 then return end
|
||
state.current_page = state.current_page - 1
|
||
elseif next_page then
|
||
if state.cursor == 0 and not state.pages[state.current_page + 1] then return end
|
||
if options.entries < 1 then return end
|
||
state.current_page = state.current_page + 1
|
||
end
|
||
end
|
||
|
||
last_state = state
|
||
|
||
if state.pages[state.current_page] then
|
||
menu_data = menu_json(state.pages[state.current_page], state.current_page)
|
||
|
||
if uosc_available then
|
||
uosc_update()
|
||
else
|
||
draw_menu()
|
||
end
|
||
return
|
||
end
|
||
|
||
local function find_path_prefix(path, path_prefixes)
|
||
for _, prefix in ipairs(path_prefixes) do
|
||
local start, stop = path:find(prefix.pattern, 1, prefix.plain)
|
||
if start then
|
||
return start, stop
|
||
end
|
||
end
|
||
end
|
||
|
||
-- all of these error cases can only happen if the user messes with the history file externally
|
||
local function read_line()
|
||
history:seek("set", state.cursor - max_digits_length)
|
||
local tail = history:read(max_digits_length)
|
||
if not tail then
|
||
mp.msg.debug("error could not read entry length @ " .. state.cursor - max_digits_length)
|
||
return
|
||
end
|
||
|
||
local entry_length_str, whitespace = tail:match("(%d+)(%s*)$")
|
||
if not entry_length_str then
|
||
mp.msg.debug("invalid entry length @ " .. state.cursor)
|
||
state.cursor = math.max(state.cursor - retry_offset, 0)
|
||
history:seek("set", state.cursor)
|
||
local retry = history:read(retry_offset)
|
||
if not retry then
|
||
mp.msg.debug("retry failed @ " .. state.cursor)
|
||
state.cursor = 0
|
||
return
|
||
end
|
||
local last_valid = string.match(retry, ".*(%d+\n.*)")
|
||
local offset = last_valid and #last_valid or retry_offset
|
||
state.cursor = state.cursor + retry_offset - offset + 1
|
||
if state.cursor == state.retry then
|
||
mp.msg.debug("bailing")
|
||
state.cursor = 0
|
||
return
|
||
end
|
||
state.retry = state.cursor
|
||
mp.msg.debug("retrying @ " .. state.cursor)
|
||
return
|
||
end
|
||
|
||
local entry_length = tonumber(entry_length_str)
|
||
state.cursor = state.cursor - entry_length - #entry_length_str - #whitespace - 1
|
||
history:seek("set", state.cursor)
|
||
|
||
local entry = history:read(entry_length)
|
||
if not entry then
|
||
mp.msg.debug("unreadable entry data @ " .. state.cursor)
|
||
return
|
||
end
|
||
local timestamp_str, title_length_str, file_info = entry:match("([^,]*),(%d*),(.*)")
|
||
if not timestamp_str then
|
||
mp.msg.debug("invalid entry data @ " .. state.cursor)
|
||
return
|
||
end
|
||
|
||
local timestamp = tonumber(timestamp_str)
|
||
timestamp = timestamp and os.date(options.timestamp_format, timestamp) or timestamp_str
|
||
|
||
local title_length = title_length_str ~= "" and tonumber(title_length_str) or 0
|
||
local full_path = file_info:sub(title_length + 2)
|
||
|
||
local display_path, save_path, effective_path, effective_protocol, is_remote, file_options = path_info(full_path)
|
||
local cache_key = effective_path .. display_path .. (file_options or "")
|
||
|
||
if options.hide_duplicates and state.known_files[cache_key] then
|
||
return
|
||
end
|
||
|
||
if dir_menu and is_remote then
|
||
return
|
||
end
|
||
|
||
if search_words and not options.use_titles then
|
||
for _, word in ipairs(search_words) do
|
||
if unaccent(display_path):lower():find(word, 1, true) == nil then
|
||
return
|
||
end
|
||
end
|
||
end
|
||
|
||
local dirname, basename
|
||
|
||
if is_remote then
|
||
state.existing_files[cache_key] = true
|
||
state.known_files[cache_key] = true
|
||
elseif options.hide_same_dir or dir_menu then
|
||
dirname, basename = mp.utils.split_path(display_path)
|
||
if dir_menu then
|
||
if dirname == "." then return end
|
||
local unix_dirname = dirname:gsub("\\", "/")
|
||
local parent, _ = mp.utils.split_path(unix_dirname:sub(1, -2))
|
||
local start, stop = find_path_prefix(parent, dir_menu_prefixes)
|
||
if not start then
|
||
return
|
||
end
|
||
basename = unix_dirname:match("/(.-)/", stop)
|
||
if basename == nil then return end
|
||
start, stop = dirname:find(basename, stop, true)
|
||
dirname = dirname:sub(1, stop + 1)
|
||
end
|
||
if state.known_dirs[dirname] then
|
||
return
|
||
end
|
||
if dirname ~= "." then
|
||
state.known_dirs[dirname] = true
|
||
end
|
||
end
|
||
|
||
if options.hide_deleted and not (search_words and options.use_titles) then
|
||
if state.known_files[cache_key] and not state.existing_files[cache_key] then
|
||
return
|
||
end
|
||
if not state.known_files[cache_key] then
|
||
local stat = mp.utils.file_info(effective_path)
|
||
if stat then
|
||
state.existing_files[cache_key] = true
|
||
elseif dir_menu then
|
||
state.known_files[cache_key] = true
|
||
local dir = mp.utils.split_path(effective_path)
|
||
if dir == "." then
|
||
return
|
||
end
|
||
stat = mp.utils.readdir(dir, "files")
|
||
if stat and next(stat) ~= nil then
|
||
full_path = dir
|
||
else
|
||
return
|
||
end
|
||
else
|
||
state.known_files[cache_key] = true
|
||
return
|
||
end
|
||
end
|
||
end
|
||
|
||
local title = file_info:sub(1, title_length)
|
||
if not options.use_titles then
|
||
title = ""
|
||
end
|
||
|
||
if dir_menu then
|
||
title = basename
|
||
elseif title == "" then
|
||
if is_remote then
|
||
title = display_path
|
||
else
|
||
local effective_display_path = display_path
|
||
if file_options then
|
||
effective_display_path = file_options
|
||
end
|
||
if not dirname then
|
||
dirname, basename = mp.utils.split_path(effective_display_path)
|
||
end
|
||
title = basename ~= "" and basename or display_path
|
||
if file_options then
|
||
title = display_path .. " " .. title
|
||
end
|
||
end
|
||
end
|
||
|
||
title = title:gsub("\n", " ")
|
||
|
||
if search_words and options.use_titles then
|
||
for _, word in ipairs(search_words) do
|
||
if unaccent(title):lower():find(word, 1, true) == nil then
|
||
return
|
||
end
|
||
end
|
||
end
|
||
|
||
if options.hide_deleted and (search_words and options.use_titles) then
|
||
if state.known_files[cache_key] and not state.existing_files[cache_key] then
|
||
return
|
||
end
|
||
if not state.known_files[cache_key] then
|
||
local stat = mp.utils.file_info(effective_path)
|
||
if stat then
|
||
state.existing_files[cache_key] = true
|
||
elseif dir_menu then
|
||
state.known_files[cache_key] = true
|
||
local dir = mp.utils.split_path(effective_path)
|
||
if dir == "." then
|
||
return
|
||
end
|
||
stat = mp.utils.readdir(dir, "files")
|
||
if stat and next(stat) ~= nil then
|
||
full_path = dir
|
||
else
|
||
return
|
||
end
|
||
else
|
||
state.known_files[cache_key] = true
|
||
return
|
||
end
|
||
end
|
||
end
|
||
|
||
if options.truncate_titles > 0 then
|
||
local title_chars, title_width = utf8_table(title)
|
||
if title_width > options.truncate_titles then
|
||
local extension = string.match(title, "%.([^.][^.][^.]?[^.]?)$") or ""
|
||
local extra = #extension + 4
|
||
local title_sub, end_index = utf8_subwidth(title_chars, 1, options.truncate_titles - 3 - extra)
|
||
local title_trim = title_sub:gsub("[] ._'()?![]+$", "")
|
||
local around_extension = ""
|
||
if title_trim == "" then
|
||
title_trim = utf8_subwidth(title_chars, 1, options.truncate_titles - 3)
|
||
else
|
||
extra = extra + #title_sub - #title_trim
|
||
around_extension = utf8_subwidth_back(title_chars, extra)
|
||
end
|
||
if title_trim == "" then
|
||
title = utf8_subwidth(title_chars, 1, options.truncate_titles)
|
||
else
|
||
title = title_trim .. "..." .. around_extension
|
||
end
|
||
end
|
||
end
|
||
|
||
state.known_files[cache_key] = true
|
||
|
||
local command = {"loadfile", full_path, "replace"}
|
||
|
||
if file_options then
|
||
command[2] = display_path
|
||
for _, arg in ipairs(loadfile_compat(file_options)) do
|
||
table.insert(command, arg)
|
||
end
|
||
end
|
||
|
||
table.insert(menu_items, {title = title, hint = timestamp, value = command})
|
||
end
|
||
|
||
local item_count = -1
|
||
local attempts = 0
|
||
|
||
while #menu_items < entries do
|
||
if state.cursor - max_digits_length <= 0 then
|
||
break
|
||
end
|
||
|
||
if osd_update then
|
||
local time = mp.get_time()
|
||
if time > osd_update then
|
||
draw_menu()
|
||
end
|
||
end
|
||
|
||
if not return_items and (attempts > 0 or not (prev_page or next_page)) and attempts % options.entries == 0 and #menu_items ~= item_count then
|
||
item_count = #menu_items
|
||
local temp_items = {unpack(menu_items)}
|
||
for i = 1, options.entries - item_count do
|
||
table.insert(temp_items, {value = {"ignore"}, keep_open = true})
|
||
end
|
||
|
||
table.insert(temp_items, {title = "Loading...", value = {"ignore"}, italic = "true", muted = "true", icon = "spinner", keep_open = true})
|
||
|
||
if next_page and state.current_page ~= 1 then
|
||
table.insert(temp_items, {value = {"ignore"}, keep_open = true})
|
||
end
|
||
|
||
menu_data = menu_json(temp_items, state.current_page)
|
||
|
||
if uosc_available then
|
||
uosc_update()
|
||
menu_shown = true
|
||
else
|
||
osd_update = mp.get_time() + 0.1
|
||
end
|
||
end
|
||
|
||
read_line()
|
||
|
||
attempts = attempts + 1
|
||
end
|
||
|
||
if return_items then
|
||
return menu_items
|
||
end
|
||
|
||
if options.pagination then
|
||
if #menu_items > 0 and state.cursor - max_digits_length > 0 then
|
||
table.insert(menu_items, {title = "Older entries", value = {"script-binding", "memo-next"}, italic = "true", muted = "true", icon = "navigate_next", keep_open = true})
|
||
end
|
||
if state.current_page ~= 1 then
|
||
table.insert(menu_items, {title = "Newer entries", value = {"script-binding", "memo-prev"}, italic = "true", muted = "true", icon = "navigate_before", keep_open = true})
|
||
end
|
||
end
|
||
|
||
menu_data = menu_json(menu_items, state.current_page)
|
||
state.pages[state.current_page] = menu_items
|
||
last_state = state
|
||
|
||
if uosc_available then
|
||
uosc_update()
|
||
else
|
||
draw_menu()
|
||
end
|
||
|
||
menu_shown = true
|
||
end
|
||
|
||
function file_load()
|
||
if options.enabled then
|
||
write_history()
|
||
elseif dyn_menu then
|
||
dyn_menu_update()
|
||
end
|
||
|
||
if menu_shown and last_state and last_state.current_page == 1 then
|
||
show_history(options.entries, false, false, true)
|
||
end
|
||
end
|
||
|
||
function idle()
|
||
event_loop_exhausted = false
|
||
if osd_update then
|
||
osd_update = nil
|
||
osd:update()
|
||
end
|
||
end
|
||
|
||
mp.register_script_message("uosc-version", function(version)
|
||
local function semver_comp(v1, v2)
|
||
local v1_iterator = v1:gmatch("%d+")
|
||
local v2_iterator = v2:gmatch("%d+")
|
||
for v2_num_str in v2_iterator do
|
||
local v1_num_str = v1_iterator()
|
||
if not v1_num_str then return true end
|
||
local v1_num = tonumber(v1_num_str)
|
||
local v2_num = tonumber(v2_num_str)
|
||
if v1_num < v2_num then return true end
|
||
if v1_num > v2_num then return false end
|
||
end
|
||
return false
|
||
end
|
||
|
||
local min_version = "5.0.0"
|
||
uosc_available = not semver_comp(version, min_version)
|
||
end)
|
||
|
||
mp.register_script_message("menu-ready", function(client_name)
|
||
dyn_menu = client_name
|
||
dyn_menu_update()
|
||
end)
|
||
|
||
function memo_close()
|
||
menu_shown = false
|
||
palette = false
|
||
if uosc_available then
|
||
mp.commandv("script-message-to", "uosc", "close-menu", "memo-history")
|
||
else
|
||
close_menu()
|
||
end
|
||
end
|
||
|
||
function memo_clear()
|
||
last_state = nil
|
||
search_words = nil
|
||
search_query = nil
|
||
menu_shown = false
|
||
palette = false
|
||
dir_menu = false
|
||
end
|
||
|
||
function memo_prev()
|
||
show_history(options.entries, false, true)
|
||
end
|
||
|
||
function memo_next()
|
||
show_history(options.entries, true)
|
||
end
|
||
|
||
function memo_search(...)
|
||
-- close REPL
|
||
mp.commandv("keypress", "ESC")
|
||
|
||
local words = {...}
|
||
if #words > 0 then
|
||
query = table.concat(words, " ")
|
||
|
||
if query ~= "" then
|
||
for i, word in ipairs(words) do
|
||
words[i] = unaccent(word):lower()
|
||
end
|
||
search_query = query
|
||
search_words = words
|
||
else
|
||
search_query = nil
|
||
search_words = nil
|
||
end
|
||
end
|
||
|
||
show_history(options.entries, false)
|
||
end
|
||
|
||
function parse_query_parts(query)
|
||
local pos, len, parts = query:find("%S"), query:len(), {}
|
||
while pos and pos <= len do
|
||
local first_char, part, pos_end = query:sub(pos, pos)
|
||
if first_char == '"' or first_char == "'" then
|
||
pos_end = query:find(first_char, pos + 1, true)
|
||
if not pos_end or pos_end ~= len and not query:find("^%s", pos_end + 1) then
|
||
parts[#parts + 1] = query:sub(pos + 1)
|
||
return parts
|
||
end
|
||
part = query:sub(pos + 1, pos_end - 1)
|
||
else
|
||
pos_end = query:find("%S%s", pos) or len
|
||
part = query:sub(pos, pos_end)
|
||
end
|
||
parts[#parts + 1] = part
|
||
pos = query:find("%S", pos_end + 2)
|
||
end
|
||
return parts
|
||
end
|
||
|
||
function memo_search_uosc(query)
|
||
if query ~= "" then
|
||
search_query = query
|
||
search_words = parse_query_parts(unaccent(query):lower())
|
||
else
|
||
search_query = nil
|
||
search_words = nil
|
||
end
|
||
event_loop_exhausted = false
|
||
show_history(options.entries, false, false, menu_shown and last_state)
|
||
end
|
||
|
||
-- update menu in mpv-menu-plugin
|
||
function dyn_menu_update()
|
||
search_words = nil
|
||
event_loop_exhausted = false
|
||
local items = show_history(options.entries, false, false, false, true)
|
||
event_loop_exhausted = false
|
||
|
||
local menu = {
|
||
type = "submenu",
|
||
submenu = {}
|
||
}
|
||
|
||
if not options.enabled then
|
||
menu.submenu = {{title = "Add current file to memo", cmd = "script-binding memo-log"}, {type = "separator"}}
|
||
end
|
||
|
||
if items and #items > 0 then
|
||
local full_path, display_path, save_path, effective_path, effective_protocol, is_remote, file_options = get_full_path()
|
||
for _, item in ipairs(items) do
|
||
local cmd = string.format("%s \"%s\" %s %s %s",
|
||
item.value[1],
|
||
item.value[2]:gsub("\\", "\\\\"):gsub("\"", "\\\""),
|
||
item.value[3],
|
||
(item.value[4] or ""):gsub("\\", "\\\\"):gsub("\"", "\\\""):gsub("^(.+)$", "\"%1\""),
|
||
(item.value[5] or ""):gsub("\\", "\\\\"):gsub("\"", "\\\""):gsub("^(.+)$", "\"%1\"")
|
||
)
|
||
menu.submenu[#menu.submenu + 1] = {
|
||
title = item.title,
|
||
cmd = cmd,
|
||
shortcut = item.hint,
|
||
state = full_path == item.value[2] and {"checked"} or {}
|
||
}
|
||
end
|
||
if last_state.cursor > 0 then
|
||
menu.submenu[#menu.submenu + 1] = {title = "...", cmd = "script-binding memo-next"}
|
||
end
|
||
else
|
||
menu.submenu[#menu.submenu + 1] = {
|
||
title = "No entries",
|
||
state = {"disabled"}
|
||
}
|
||
end
|
||
|
||
mp.commandv("script-message-to", dyn_menu, "update", "memo", mp.utils.format_json(menu))
|
||
end
|
||
|
||
mp.register_script_message("memo-clear", memo_clear)
|
||
mp.register_script_message("memo-search:", memo_search)
|
||
mp.register_script_message("memo-search-uosc:", memo_search_uosc)
|
||
|
||
mp.add_key_binding(nil, "memo-next", memo_next)
|
||
mp.add_key_binding(nil, "memo-prev", memo_prev)
|
||
mp.add_key_binding(nil, "memo-log", function()
|
||
write_history(true)
|
||
|
||
if menu_shown and last_state and last_state.current_page == 1 then
|
||
show_history(options.entries, false, false, true)
|
||
end
|
||
end)
|
||
mp.add_key_binding(nil, "memo-last", function()
|
||
if event_loop_exhausted then return end
|
||
|
||
local items
|
||
if last_state and last_state.current_page == 1 and options.hide_duplicates and options.hide_deleted and options.entries >= 2 and not search_words and not dir_menu then
|
||
-- menu is open and we for sure have everything we need
|
||
items = last_state.pages[1]
|
||
last_state = nil
|
||
show_history(0, false, false, false, true)
|
||
else
|
||
-- menu is closed or we may not have everything
|
||
local options_bak = shallow_copy(options)
|
||
options.pagination = false
|
||
options.hide_duplicates = true
|
||
options.hide_deleted = true
|
||
last_state = nil
|
||
search_words = nil
|
||
dir_menu = false
|
||
items = show_history(2, false, false, false, true)
|
||
options = options_bak
|
||
end
|
||
if items then
|
||
local item
|
||
local full_path, display_path, save_path, effective_path, effective_protocol, is_remote, file_options = get_full_path()
|
||
if #items >= 1 and not items[1].keep_open then
|
||
if items[1].value[2] ~= full_path then
|
||
item = items[1]
|
||
elseif #items >= 2 and not items[2].keep_open and items[2].value[2] ~= full_path then
|
||
item = items[2]
|
||
end
|
||
end
|
||
|
||
if item then
|
||
mp.commandv(unpack(item.value))
|
||
return
|
||
end
|
||
end
|
||
mp.osd_message("[memo] no recent files to open")
|
||
end)
|
||
mp.add_key_binding(nil, "memo-search", function()
|
||
if uosc_available then
|
||
palette = true
|
||
show_history(options.entries, false, false, true)
|
||
return
|
||
end
|
||
if menu_shown then
|
||
memo_close()
|
||
end
|
||
mp.commandv("script-message-to", "console", "type", "script-message memo-search: ")
|
||
end)
|
||
mp.add_key_binding("h", "memo-history", function()
|
||
if event_loop_exhausted then return end
|
||
last_state = nil
|
||
search_words = nil
|
||
dir_menu = false
|
||
show_history(options.entries, false)
|
||
end)
|
||
mp.register_script_message("memo-dirs", function(path_prefixes)
|
||
if event_loop_exhausted then return end
|
||
last_state = nil
|
||
search_words = nil
|
||
dir_menu = true
|
||
if path_prefixes then
|
||
dir_menu_prefixes = parse_path_prefixes(path_prefixes)
|
||
else
|
||
dir_menu_prefixes = options.path_prefixes
|
||
end
|
||
show_history(options.entries, false)
|
||
end)
|
||
|
||
mp.register_event("file-loaded", file_load)
|
||
mp.register_idle(idle)
|