summaryrefslogtreecommitdiff
path: root/mac/.config/mpv/scripts/navigator.lua
diff options
context:
space:
mode:
Diffstat (limited to 'mac/.config/mpv/scripts/navigator.lua')
-rw-r--r--mac/.config/mpv/scripts/navigator.lua605
1 files changed, 605 insertions, 0 deletions
diff --git a/mac/.config/mpv/scripts/navigator.lua b/mac/.config/mpv/scripts/navigator.lua
new file mode 100644
index 0000000..4533f60
--- /dev/null
+++ b/mac/.config/mpv/scripts/navigator.lua
@@ -0,0 +1,605 @@
+local utils = require("mp.utils")
+local mpopts = require("mp.options")
+local assdraw = require("mp.assdraw")
+local user = os.getenv("USER") or "si"
+local home = os.getenv("HOME") or ("/home/" .. user)
+
+ON_WINDOWS = (package.config:sub(1, 1) ~= "/")
+WINDOWS_ROOTDIR = false
+WINDOWS_ROOT_DESC = "Select drive"
+SEPARATOR_WINDOWS = "\\"
+
+SEPARATOR = "/"
+
+local windows_desktop = ON_WINDOWS
+ and utils.join_path(os.getenv("USERPROFILE"), "Desktop"):gsub(SEPARATOR, SEPARATOR_WINDOWS) .. SEPARATOR_WINDOWS
+ or nil
+
+local settings = {
+ --navigation keybinds override arrowkeys and enter when activating navigation menu, false means keys are always actíve
+ dynamic_binds = true,
+ navigator_mainkey = "shift+v", --the key to bring up navigator's menu, can be bound on input.conf aswell
+
+ --dynamic binds, should not be bound in input.conf unless dynamic binds is false
+ key_navfavorites = "ctrl+f",
+ key_navup = "k",
+ key_navdown = "j",
+ key_navback = "h",
+ key_navforward = "l",
+ key_navopen = "ENTER",
+ key_navclose = "q",
+
+ --fallback if no file is open, should be a string that points to a path in your system
+ defaultpath = windows_desktop or os.getenv("HOME") or "/",
+ forcedefault = false, --force navigation to start from defaultpath instead of currently playing file
+ --favorites in format { 'Path to directory, notice trailing /' }
+ --on windows use double backslash c:\\my\\directory\\
+ favorites = {
+ "/media/" .. user,
+ "/mnt/second/videos",
+ home .. "/Downloads",
+ home .. "/Torrents/complete",
+ home .. "/Videos",
+ home .. "/.config/mpv/playlists",
+ },
+ --list of paths to ignore. the value is anything that returns true for if-statement.
+ --directory ignore entries must end with a trailing slash,
+ --but files and all symlinks (even to dirs) must be without slash!
+ --to help you with the format, simply run "ls -1p <parent folder>" in a terminal,
+ --and you will see if the file/folder to ignore is listed as "file" or "folder/" (trailing slash).
+ --you can ignore children without ignoring their parent.
+ ignorePaths = {
+ --general linux system paths (some are used by macOS too):
+ ["/bin/"] = "1",
+ ["/boot/"] = "1",
+ ["/cdrom/"] = "1",
+ ["/dev/"] = "1",
+ ["/etc/"] = "1",
+ ["/lib/"] = "1",
+ ["/lib32/"] = "1",
+ ["/lib64/"] = "1",
+ ["/tmp/"] = "1",
+ ["/srv/"] = "1",
+ ["/sys/"] = "1",
+ ["/snap/"] = "1",
+ ["/root/"] = "1",
+ ["/sbin/"] = "1",
+ ["/proc/"] = "1",
+ ["/opt/"] = "1",
+ ["/usr/"] = "1",
+ ["/run/"] = "1",
+ --useless macOS system paths (some of these standard folders are actually files (symlinks) into /private/ subpaths, hence some repetition):
+ ["/cores/"] = "1",
+ ["/etc"] = "1",
+ ["/installer.failurerequests"] = "1",
+ ["/net/"] = "1",
+ ["/private/"] = "1",
+ ["/tmp"] = "1",
+ ["/var"] = "1",
+ },
+ --ignore folders and files that match patterns regardless of where they exist on disk.
+ --make sure you use ^ (start of string) and $ (end of string) to catch the whole str instead of risking partial false positives.
+ --read about patterns at https://www.lua.org/pil/20.2.html or http://lua-users.org/wiki/PatternsTutorial
+ ignorePatterns = {
+ "^initrd%..*/?$", --hide files and folders folders starting with "initrd.<something>"
+ "^vmlinuz.*/?$", --hide files and folders starting with "vmlinuz<something>"
+ "^lost%+found/?$", --hide files and folders named "lost+found"
+ "^%$.*$", --ignore files starting with $
+ "^.*%.ico$",
+ "^.*%.txt$",
+ "^.*%.ahk$",
+ "^.*%.reg$",
+ "^.*%.exe$",
+ "^.*%.bin$",
+ "^.*%.mpq$",
+ "^.*%.inf$",
+ "^.*%.pdf$",
+ "^.*%.docx$",
+ "^.*%.xlsx$",
+ "^.*%.pptx$",
+ "^.*%.zip$",
+ "^.*%.rar$",
+ "^.*%.tar$",
+ "^.*%.gz$",
+ "^.*%.bz2$",
+ "^.*%.7z$",
+ "^.*%.pkg$",
+ "^.*%.deb$",
+ "^.*%.rpm$",
+ "^.*%.dll$",
+ "^.*%.sys$",
+ "^.*%.cfg$",
+ "^.*%.ini$",
+ "^.*%.dat$",
+ "^.*%.bat$",
+ "^.*%.cmd$",
+ "^.*%.js$",
+ "^.*%.html$",
+ "^.*%.htm$",
+ "^.*%.css$",
+ "^.*%.xml$",
+ "^.*%.json$",
+ "^.*%.csv$",
+ "^.*%.md$",
+ "^.*%.yml$",
+ "^.*%.yaml$",
+ "^.*%.sql$",
+ "^.*%.py$",
+ "^.*%.java$",
+ "^.*%.c$",
+ "^.*%.cpp$",
+ "^.*%.h$",
+ "^.*%.rb$",
+ "^.*%.pl$",
+ "^.*%.php$",
+ "^.*%.asp$",
+ "^.*%.aspx$",
+ "^.*%.jsp$",
+ "^.*%.sh$",
+ "^.*%.vbs$",
+ "^.*%.ps1$",
+ "^.*%.log$",
+ "^.*%.msg$",
+ "^.*%.eml$",
+ "^.*%.ics$",
+ "^.*%.vcard$",
+ "^.*%.vcf$",
+ "^.*%.mdf$",
+ "^.*%.ldf$",
+ "^.*%.nfo$",
+ "^.*%.tmp$",
+ "^.*%.bak$",
+ "^.*%.hax$",
+ },
+
+ subtitleformats = {
+ "srt",
+ "smi",
+ "ass",
+ "lrc",
+ "ssa",
+ "ttml",
+ "sbv",
+ "vtt",
+ "txt",
+ },
+
+ navigator_menu_favkey = "F", --this key will always be bound when the menu is open, and is the key you use to cycle your favorites list!
+ menu_timeout = false, --menu timeouts and closes itself after navigator_duration seconds, else will be toggled by keybind
+ navigator_duration = 13, --osd duration before the navigator closes, if timeout is set to true
+ visible_item_count = 20, --how many menu items to show per screen
+
+ --font size scales by window, if false requires larger font and padding sizes
+ scale_by_window = true,
+ --paddings from top left corner
+ text_padding_x = 10,
+ text_padding_y = 20,
+ -- --ass style overrides inside curly brackets, \keyvalue is one field, extra \ for escape in lua
+ -- --example {\\fnUbuntu\\fs10\\b0\\bord1} equals: font=Ubuntu, size=10, bold=no, border=1
+ -- --read http://docs.aegisub.org/3.2/ASS_Tags/ for reference of tags
+ -- --undeclared tags will use default osd settings
+ -- --these styles will be used for the whole navigator
+ -- style_ass_tags = "{}",
+ -- --you can also use the ass tags mentioned above. For example:
+ -- --selection_prefix="{\\c&HFF00FF&}● " - to add a color for selected file. However, if you
+ -- --use ass tags you need to set them for both name and selection prefix (see https://github.com/jonniek/mpv-playlistmanager/issues/20)
+ -- name_prefix = "○ ",
+ -- selection_prefix = "● ",
+
+ -- For white color:
+ style_ass_tags = "{\\q2\\an7\\fnUbuntu\\fs8\\b0\\bord1\\c&HFFFFFF&}",
+ name_prefix = "{\\c&HFFFFFF&}○ ",
+
+ -- For orange selection:
+ selection_prefix = "{\\c&H0080FF&}● ",
+}
+
+mpopts.read_options(settings)
+
+--escape a file or directory path for use in shell arguments
+function escapepath(dir, escapechar)
+ return string.gsub(dir, escapechar, "\\" .. escapechar)
+end
+
+local sub_lookup = {}
+for _, ext in ipairs(settings.subtitleformats) do
+ sub_lookup[ext] = true
+end
+
+--ensures directories never accidentally end in "//" due to our added slash
+function stripdoubleslash(dir)
+ if string.sub(dir, -2) == "//" then
+ return string.sub(dir, 1, -2) --negative 2 removes the last character
+ else
+ return dir
+ end
+end
+
+function os.capture(cmd, raw)
+ local f = assert(io.popen(cmd, "r"))
+ local s = assert(f:read("*a"))
+ f:close()
+ return string.sub(s, 0, -2)
+end
+
+dir = nil
+path = nil
+cursor = 0
+length = 0
+--osd handler that displays your navigation and information
+function handler()
+ add_keybinds()
+ timer:kill()
+ local ass = assdraw.ass_new()
+ ass:new_event()
+ ass:pos(settings.text_padding_x, settings.text_padding_y)
+ ass:append(settings.style_ass_tags)
+
+ if not path then
+ if mp.get_property("path") and not settings.forcedefault then
+ --determine path from currently playing file...
+ local workingdir = mp.get_property("working-directory")
+ local playfilename = mp.get_property("filename") --just the filename, without path
+ local playpath = mp.get_property("path") --can be relative or absolute depending on what args mpv was given
+ local firstchar = string.sub(playpath, 1, 1)
+ --first we need to remove the filename (may give us empty path if mpv was started in same dir as file)
+ path = string.sub(playpath, 1, string.len(playpath) - string.len(playfilename))
+ if firstchar ~= "/" and not ON_WINDOWS then --the path of the playing file wasn't absolute, so we need to add mpv's working dir to it
+ path = workingdir .. "/" .. path
+ end
+ --now resolve that path (to resolve things like "/home/anon/Movies/../Movies/foo.mkv")
+ path = resolvedir(path)
+ --lastly, check if the folder exists, and if not then fall back to the current mpv working dir
+ if not isfolder(path) then
+ if ON_WINDOWS then
+ path = workingdir .. SEPARATOR_WINDOWS
+ else
+ path = workingdir
+ end
+ end
+ else
+ path = settings.defaultpath
+ end
+ dir, length = scandirectory(path)
+ end
+ ass:append(path .. "\\N\\N")
+ local b = cursor - math.floor(settings.visible_item_count / 2)
+ if b > 0 then
+ ass:append("...\\N")
+ end
+ if b < 0 then
+ b = 0
+ end
+ for a = b, (b + settings.visible_item_count), 1 do
+ if a == length then
+ break
+ end
+ local prefix = (a == cursor and settings.selection_prefix or settings.name_prefix)
+ ass:append(prefix .. dir[a] .. "\\N")
+ if a == (b + settings.visible_item_count) then
+ ass:append("...")
+ end
+ end
+ local w, h = mp.get_osd_size()
+ if settings.scale_by_window then
+ w, h = 0, 0
+ end
+ mp.set_osd_ass(w, h, ass.text)
+ if settings.menu_timeout then
+ timer:resume()
+ end
+end
+
+function navdown()
+ if cursor ~= length - 1 then
+ cursor = cursor + 1
+ else
+ cursor = 0
+ end
+ handler()
+end
+
+function navup()
+ if cursor ~= 0 then
+ cursor = cursor - 1
+ else
+ cursor = length - 1
+ end
+ handler()
+end
+
+--moves into selected directory, or appends to playlist incase of file
+function childdir()
+ local item = dir[cursor]
+
+ -- windows only
+ if ON_WINDOWS then
+ if WINDOWS_ROOTDIR then
+ WINDOWS_ROOTDIR = false
+ end
+ if item then
+ local newdir = utils.join_path(path, item):gsub(SEPARATOR, SEPARATOR_WINDOWS) .. SEPARATOR_WINDOWS
+ local info, error = utils.file_info(newdir)
+
+ if info and info.is_dir then
+ changepath(newdir)
+ else
+ if issubtitle(item) then
+ loadsubs(utils.join_path(path, item))
+ else
+ mp.commandv("loadfile", utils.join_path(path, item), "append-play")
+ mp.osd_message("Appended file to playlist: " .. item)
+ end
+ handler()
+ end
+ end
+
+ return
+ end
+
+ if item then
+ if isfolder(utils.join_path(path, item)) then
+ local newdir = stripdoubleslash(utils.join_path(path, dir[cursor] .. "/"))
+ changepath(newdir)
+ else
+ if issubtitle(item) then
+ loadsubs(utils.join_path(path, item))
+ else
+ mp.commandv("loadfile", utils.join_path(path, item), "append-play")
+ mp.osd_message("Appended file to playlist: " .. item)
+ end
+ handler()
+ end
+ end
+end
+
+function issubtitle(file)
+ local ext = file:match("^.+%.(.+)$")
+ return ext and sub_lookup[ext:lower()]
+end
+
+function loadsubs(file)
+ mp.commandv("sub_add", file)
+ mp.osd_message("Loaded subtitle: " .. file)
+end
+
+--replace current playlist with directory or file
+--if directory, mpv will recursively queue all items found in the directory and its subfolders
+function opendir()
+ local item = dir[cursor]
+
+ if item then
+ remove_keybinds()
+
+ local filepath = utils.join_path(path, item)
+ if ON_WINDOWS then
+ filepath = filepath:gsub(SEPARATOR, SEPARATOR_WINDOWS)
+ end
+
+ if issubtitle(item) then
+ return loadsubs(filepath)
+ end
+
+ mp.commandv("loadfile", filepath, "replace")
+ end
+end
+
+--changes the directory to the path in argument
+function changepath(args)
+ path = args
+ if WINDOWS_ROOTDIR then
+ path = WINDOWS_ROOT_DESC
+ end
+ dir, length = scandirectory(path)
+ cursor = 0
+ handler()
+end
+
+--move up to the parent directory
+function parentdir()
+ -- windows only
+ if ON_WINDOWS then
+ if path:sub(-1) == SEPARATOR_WINDOWS then
+ path = path:sub(1, -2)
+ end
+ local parent = utils.split_path(path)
+ if path == parent then
+ WINDOWS_ROOTDIR = true
+ end
+ changepath(parent)
+ return
+ end
+
+ --if path doesn't exist or can't be entered, this returns "/" (root of the drive) as the parent
+ local parent = stripdoubleslash(
+ os.capture('cd "' .. escapepath(path, '"') .. '" 2>/dev/null && cd .. 2>/dev/null && pwd') .. "/"
+ )
+
+ changepath(parent)
+end
+
+--resolves relative paths such as "/home/foo/../foo/Music" (to "/home/foo/Music") if the folder exists!
+function resolvedir(dir)
+ local safedir = escapepath(dir, '"')
+
+ -- windows only
+ if ON_WINDOWS then
+ local resolved = stripdoubleslash(os.capture('cd /d "' .. safedir .. '" && cd'))
+ return resolved .. SEPARATOR_WINDOWS
+ end
+
+ --if dir doesn't exist or can't be entered, this returns "/" (root of the drive) as the resolved path
+ local resolved = stripdoubleslash(os.capture('cd "' .. safedir .. '" 2>/dev/null && pwd') .. "/")
+ return resolved
+end
+
+--true if path exists and is a folder, otherwise false
+function isfolder(dir)
+ -- windows only
+ if ON_WINDOWS then
+ local info, error = utils.file_info(dir)
+ return info and info.is_dir or nil
+ end
+
+ local lua51returncode, _, lua52returncode = os.execute('test -d "' .. escapepath(dir, '"') .. '"')
+ return lua51returncode == 0 or lua52returncode == 0
+end
+
+function scandirectory(searchdir)
+ local directory = {}
+ --list all files, using universal utilities and flags available on both Linux and macOS
+ -- ls: -1 = list one file per line, -p = append "/" indicator to the end of directory names, -v = display in natural order
+ -- stderr messages are ignored by sending them to /dev/null
+ -- hidden files ("." prefix) are skipped, since they exist everywhere and never contain media
+ -- if we cannot list the contents (due to no permissions, etc), this returns an empty list
+
+ -- windows only
+ if ON_WINDOWS then
+ -- handle drive letters
+ if WINDOWS_ROOTDIR then
+ local popen, err = io.popen("wmic logicaldisk get caption")
+ local i = 0
+ if popen then
+ for direntry in popen:lines() do
+ -- only single letter followed by colon (:) are valid
+ if string.find(direntry, "^%a:") then
+ direntry = string.sub(direntry, 1, 2)
+ local matchedignore = false
+ for k, pattern in pairs(settings.ignorePatterns) do
+ if direntry:find(pattern) then
+ matchedignore = true
+ break --don't waste time scanning further patterns
+ end
+ end
+ if not matchedignore and not settings.ignorePaths[path .. direntry] then
+ directory[i] = direntry
+ i = i + 1
+ end
+ end
+ end
+ popen:close()
+ else
+ mp.msg.error("Could not scan for files :" .. (err or ""))
+ end
+
+ return directory, i
+ end
+
+ local i = 0
+ local files = utils.readdir(searchdir)
+
+ if not files then
+ mp.msg.error("Could not scan for files :" .. (err or ""))
+ return directory, i
+ end
+
+ for _, direntry in ipairs(files) do
+ local matchedignore = false
+ for k, pattern in pairs(settings.ignorePatterns) do
+ if direntry:find(pattern) then
+ matchedignore = true
+ break --don't waste time scanning further patterns
+ end
+ end
+ if not matchedignore and not settings.ignorePaths[path .. direntry] then
+ directory[i] = direntry
+ i = i + 1
+ end
+ end
+
+ return directory, i
+ end
+
+ local popen, err = io.popen('ls -1vp "' .. escapepath(searchdir, '"') .. '" 2>/dev/null')
+ local i = 0
+ if popen then
+ for direntry in popen:lines() do
+ local matchedignore = false
+ for k, pattern in pairs(settings.ignorePatterns) do
+ if direntry:find(pattern) then
+ matchedignore = true
+ break --don't waste time scanning further patterns
+ end
+ end
+ if not matchedignore and not settings.ignorePaths[path .. direntry] then
+ directory[i] = direntry
+ i = i + 1
+ end
+ end
+ popen:close()
+ else
+ mp.msg.error("Could not scan for files :" .. (err or ""))
+ end
+ return directory, i
+end
+
+favcursor = 1
+function cyclefavorite()
+ local firstpath = settings.favorites[1]
+ if not firstpath then
+ return
+ end
+ local favpath = nil
+ local favlen = 0
+ for key, fav in pairs(settings.favorites) do
+ favlen = favlen + 1
+ if key == favcursor then
+ favpath = fav
+ end
+ end
+ if favpath then
+ changepath(favpath)
+ favcursor = favcursor + 1
+ else
+ changepath(firstpath)
+ favcursor = 2
+ end
+end
+
+function add_keybinds()
+ mp.add_forced_key_binding(settings.key_navdown, "navdown", navdown, "repeatable")
+ mp.add_forced_key_binding(settings.key_navup, "navup", navup, "repeatable")
+ mp.add_forced_key_binding(settings.key_navopen, "navopen", opendir)
+ mp.add_forced_key_binding(settings.key_navforward, "navforward", childdir)
+ mp.add_forced_key_binding(settings.key_navback, "navback", parentdir)
+ mp.add_forced_key_binding(settings.key_navfavorites, "navfavorites", cyclefavorite)
+ mp.add_forced_key_binding(settings.key_navclose, "navclose", remove_keybinds)
+end
+
+function remove_keybinds()
+ timer:kill()
+ mp.set_osd_ass(0, 0, "")
+ if settings.dynamic_binds then
+ mp.remove_key_binding("navdown")
+ mp.remove_key_binding("navup")
+ mp.remove_key_binding("navopen")
+ mp.remove_key_binding("navforward")
+ mp.remove_key_binding("navback")
+ mp.remove_key_binding("navfavorites")
+ mp.remove_key_binding("navclose")
+ end
+end
+
+timer = mp.add_periodic_timer(settings.navigator_duration, remove_keybinds)
+timer:kill()
+
+if not settings.dynamic_binds then
+ add_keybinds()
+end
+
+active = false
+function activate()
+ if settings.menu_timeout then
+ handler()
+ else
+ if active then
+ remove_keybinds()
+ active = false
+ else
+ handler()
+ active = true
+ end
+ end
+end
+
+mp.add_key_binding(settings.navigator_mainkey, "navigator", activate)