commit e6e9f22100ae44bebcc5dcb80ccae7bb08245875 Author: dxrknesss Date: Sat Jan 17 22:13:40 2026 +0200 add dotfiles diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..2b9f4b0 --- /dev/null +++ b/.gitignore @@ -0,0 +1,12 @@ +/* +!.gitignore +!nvim +!mpv +!fish +!jj +!fastfetch +!ghostty +!git +!htop +!tmux +!zathura diff --git a/fastfetch/config.jsonc b/fastfetch/config.jsonc new file mode 100644 index 0000000..68a984d --- /dev/null +++ b/fastfetch/config.jsonc @@ -0,0 +1,31 @@ +{ + "$schema": "https://github.com/fastfetch-cli/fastfetch/raw/dev/doc/json_schema.json", + "modules": [ + "title", + "separator", + "os", + "host", + "kernel", + "uptime", + "packages", + "shell", + "display", + "de", + "wm", + "font", + "terminal", + "terminalfont", + "cpu", + "gpu", + "memory", + "disk", + "break", + "colors" + ], + "logo": { + "color": { + "1": "blue", + "2": "yellow" + } + } +} diff --git a/fish/completions/bun.fish b/fish/completions/bun.fish new file mode 100644 index 0000000..e262bb6 --- /dev/null +++ b/fish/completions/bun.fish @@ -0,0 +1,186 @@ +# This is terribly complicated +# It's because: +# 1. bun run has to have dynamic completions +# 2. there are global options +# 3. bun {install add remove} gets special options +# 4. I don't know how to write fish completions well +# Contributions very welcome!! + +function __fish__get_bun_bins + string split ' ' (bun getcompletes b) +end + +function __fish__get_bun_scripts + set -lx SHELL bash + set -lx MAX_DESCRIPTION_LEN 40 + string trim (string split '\n' (string split '\t' (bun getcompletes z))) +end + +function __fish__get_bun_packages + if test (commandline -ct) != "" + set -lx SHELL fish + string split ' ' (bun getcompletes a (commandline -ct)) + end +end + +function __history_completions + set -l tokens (commandline --current-process --tokenize) + history --prefix (commandline) | string replace -r \^$tokens[1]\\s\* "" | string replace -r \^$tokens[2]\\s\* "" | string split ' ' +end + +function __fish__get_bun_bun_js_files + string split ' ' (bun getcompletes j) +end + +set -l bun_install_boolean_flags yarn production optional development no-save dry-run force no-cache silent verbose global +set -l bun_install_boolean_flags_descriptions "Write a yarn.lock file (yarn v1)" "Don't install devDependencies" "Add dependency to optionalDependencies" "Add dependency to devDependencies" "Don't update package.json or save a lockfile" "Don't install anything" "Always request the latest versions from the registry & reinstall all dependencies" "Ignore manifest cache entirely" "Don't output anything" "Excessively verbose logging" "Use global folder" + +set -l bun_builtin_cmds_without_run dev create help bun upgrade discord install remove add init pm x +set -l bun_builtin_cmds_accepting_flags create help bun upgrade discord run init link unlink pm x + +function __bun_complete_bins_scripts --inherit-variable bun_builtin_cmds_without_run -d "Emit bun completions for bins and scripts" + # Do nothing if we already have a builtin subcommand, + # or any subcommand other than "run". + if __fish_seen_subcommand_from $bun_builtin_cmds_without_run + or not __fish_use_subcommand && not __fish_seen_subcommand_from run + return + end + # Do we already have a bin or script subcommand? + set -l bins (__fish__get_bun_bins) + if __fish_seen_subcommand_from $bins + return + end + # Scripts have descriptions appended with a tab separator. + # Strip off descriptions for the purposes of subcommand testing. + set -l scripts (__fish__get_bun_scripts) + if __fish_seen_subcommand_from (string split \t -f 1 -- $scripts) + return + end + # Emit scripts. + for script in $scripts + echo $script + end + # Emit binaries and JS files (but only if we're doing `bun run`). + if __fish_seen_subcommand_from run + for bin in $bins + echo "$bin"\t"package bin" + end + for file in (__fish__get_bun_bun_js_files) + echo "$file"\t"Bun.js" + end + end +end + + +# Clear existing completions +complete -e -c bun + +# Dynamically emit scripts and binaries +complete -c bun -f -a "(__bun_complete_bins_scripts)" + +# Complete flags if we have no subcommand or a flag-friendly one. +set -l flag_applies "__fish_use_subcommand; or __fish_seen_subcommand_from $bun_builtin_cmds_accepting_flags" +complete -c bun \ + -n $flag_applies --no-files -s 'u' -l 'origin' -r -d 'Server URL. Rewrites import paths' +complete -c bun \ + -n $flag_applies --no-files -s 'p' -l 'port' -r -d 'Port number to start server from' +complete -c bun \ + -n $flag_applies --no-files -s 'd' -l 'define' -r -d 'Substitute K:V while parsing, e.g. --define process.env.NODE_ENV:\"development\"' +complete -c bun \ + -n $flag_applies --no-files -s 'e' -l 'external' -r -d 'Exclude module from transpilation (can use * wildcards). ex: -e react' +complete -c bun \ + -n $flag_applies --no-files -l 'use' -r -d 'Use a framework (ex: next)' +complete -c bun \ + -n $flag_applies --no-files -l 'hot' -r -d 'Enable hot reloading in Bun\'s JavaScript runtime' + +# Complete dev and create as first subcommand. +complete -c bun \ + -n "__fish_use_subcommand" -a 'dev' -d 'Start dev server' +complete -c bun \ + -n "__fish_use_subcommand" -a 'create' -f -d 'Create a new project from a template' + +# Complete "next" and "react" if we've seen "create". +complete -c bun \ + -n "__fish_seen_subcommand_from create" -a 'next' -d 'new Next.js project' + +complete -c bun \ + -n "__fish_seen_subcommand_from create" -a 'react' -d 'new React project' + +# Complete "upgrade" as first subcommand. +complete -c bun \ + -n "__fish_use_subcommand" -a 'upgrade' -d 'Upgrade bun to the latest version' -x +# Complete "-h/--help" unconditionally. +complete -c bun \ + -s "h" -l "help" -d 'See all commands and flags' -x + +# Complete "-v/--version" if we have no subcommand. +complete -c bun \ + -n "not __fish_use_subcommand" -l "version" -s "v" -d 'Bun\'s version' -x + +# Complete additional subcommands. +complete -c bun \ + -n "__fish_use_subcommand" -a 'discord' -d 'Open bun\'s Discord server' -x + + +complete -c bun \ + -n "__fish_use_subcommand" -a 'bun' -d 'Generate a new bundle' + + +complete -c bun \ + -n "__fish_seen_subcommand_from bun" -F -d 'Bundle this' + +complete -c bun \ + -n "__fish_seen_subcommand_from create; and __fish_seen_subcommand_from react next" -F -d "Create in directory" + + +complete -c bun \ + -n "__fish_use_subcommand" -a 'init' -F -d 'Start an empty Bun project' + +complete -c bun \ + -n "__fish_use_subcommand" -a 'install' -f -d 'Install packages from package.json' + +complete -c bun \ + -n "__fish_use_subcommand" -a 'add' -F -d 'Add a package to package.json' + +complete -c bun \ + -n "__fish_use_subcommand" -a 'remove' -F -d 'Remove a package from package.json' + + +for i in (seq (count $bun_install_boolean_flags)) + complete -c bun \ + -n "__fish_seen_subcommand_from install add remove" -l "$bun_install_boolean_flags[$i]" -d "$bun_install_boolean_flags_descriptions[$i]" +end + +complete -c bun \ + -n "__fish_seen_subcommand_from install add remove" -l 'cwd' -d 'Change working directory' + +complete -c bun \ + -n "__fish_seen_subcommand_from install add remove" -l 'cache-dir' -d 'Choose a cache directory (default: $HOME/.bun/install/cache)' + +complete -c bun \ + -n "__fish_seen_subcommand_from add" -d 'Popular' -a '(__fish__get_bun_packages)' + +complete -c bun \ + -n "__fish_seen_subcommand_from add" -d 'History' -a '(__history_completions)' + +complete -c bun \ + -n "__fish_seen_subcommand_from pm; and not __fish_seen_subcommand_from (__fish__get_bun_bins) (__fish__get_bun_scripts) cache;" -a 'bin ls cache hash hash-print hash-string' -f + +complete -c bun \ + -n "__fish_seen_subcommand_from pm; and __fish_seen_subcommand_from cache; and not __fish_seen_subcommand_from (__fish__get_bun_bins) (__fish__get_bun_scripts);" -a 'rm' -f + +# Add built-in subcommands with descriptions. +complete -c bun -n "__fish_use_subcommand" -a "create" -f -d "Create a new project from a template" +complete -c bun -n "__fish_use_subcommand" -a "build bun" --require-parameter -F -d "Transpile and bundle one or more files" +complete -c bun -n "__fish_use_subcommand" -a "upgrade" -d "Upgrade Bun" +complete -c bun -n "__fish_use_subcommand" -a "run" -d "Run a script or package binary" +complete -c bun -n "__fish_use_subcommand" -a "install" -d "Install dependencies from package.json" -f +complete -c bun -n "__fish_use_subcommand" -a "remove" -d "Remove a dependency from package.json" -f +complete -c bun -n "__fish_use_subcommand" -a "add" -d "Add a dependency to package.json" -f +complete -c bun -n "__fish_use_subcommand" -a "init" -d "Initialize a Bun project in this directory" -f +complete -c bun -n "__fish_use_subcommand" -a "link" -d "Register or link a local npm package" -f +complete -c bun -n "__fish_use_subcommand" -a "unlink" -d "Unregister a local npm package" -f +complete -c bun -n "__fish_use_subcommand" -a "pm" -d "Additional package management utilities" -f +complete -c bun -n "__fish_use_subcommand" -a "x" -d "Execute a package binary, installing if needed" -f +complete -c bun -n "__fish_use_subcommand" -a "outdated" -d "Display the latest versions of outdated dependencies" -f +complete -c bun -n "__fish_use_subcommand" -a "publish" -d "Publish your package from local to npm" -f diff --git a/fish/completions/sdk.fish b/fish/completions/sdk.fish new file mode 100644 index 0000000..54693df --- /dev/null +++ b/fish/completions/sdk.fish @@ -0,0 +1,243 @@ +# Defines autocompletion for SDKMAN! + +# Copyright (c) 2018-2023 Raphael Reitzig +# MIT License (MIT) +# https://github.com/reitzig/sdkman-for-fish + +# Guard: SDKMAN! needs to be installed +if not test -f "$SDKMAN_DIR/bin/sdkman-init.sh" + exit 0 +end + +# # # # # # +# Completion trigger predicates +# # # # # # + +# Test if there is no command +function __fish_sdkman_no_command + set cmd (commandline -opc) + + if [ (count $cmd) -eq 1 ] + return 0 + end + return 1 +end + +# Test if the main command matches one of the parameters +function __fish_sdkman_using_command + set cmd (commandline -opc) + + if [ (count $cmd) -eq 2 ] + if contains $cmd[2] $argv + return 0 + end + end + return 1 +end + +function __fish_sdkman_specifying_candidate + set cmd (commandline -opc) + + if [ (count $cmd) -eq 3 ] # currently, sdk does not support multiple versions + if contains $cmd[2] $argv + return 0 + end + end + return 1 +end + +function __fish_sdkman_command_has_enough_parameters + set cmd (commandline -opc) + + if [ (count $cmd) -ge (math $argv[1] + 2) ]; and contains $cmd[2] $argv[2..-1] + return 0 + end + return 1 +end + +# # # # # # +# Data collectors +# # # # # # + +function __fish_sdkman_candidates + cat "$SDKMAN_DIR"/var/candidates | string replace -a -r ',' '\n' +end + +function __fish_sdkman_candidates_with_versions + set regexpHome (string replace -a '/' '\\/' "$HOME/") + + find "$SDKMAN_DIR"/candidates/ -mindepth 2 -maxdepth 2 -name '*current' \ + | awk -F '/' '{ print $(NF-1) }' \ + | sort -u +end + +function __fish_sdkman_installed_versions + set cmd (commandline -opc) + if [ -d "$SDKMAN_DIR"/candidates/$cmd[3]/current ] + ls -v1 "$SDKMAN_DIR"/candidates/$cmd[3] | grep -v current + end +end + +# # # # # # +# Completion specification +# # # # # # + +# install +complete -c sdk -f -n '__fish_sdkman_no_command' \ + -a 'i install' \ + -d 'Install new version' +complete -c sdk -f -n '__fish_sdkman_using_command i install' \ + -a "(__fish_sdkman_candidates)" +# TODO complete available versions --> issue #4 +complete -c sdk -f -n '__fish_sdkman_specifying_candidate i install' \ + -a 'a.b.c' \ + -d "version list unavailable" +complete -c sdk -f -n '__fish_sdkman_specifying_candidate i install' \ + -a 'x.y.z' \ + -d "Specify path to install custom version." +# Implicit: complete files as fourth parameter +complete -c sdk -f -n '__fish_sdkman_command_has_enough_parameters 3 i install' + # block + +# uninstall +complete -c sdk -f -n '__fish_sdkman_no_command' \ + -a 'rm uninstall' -d 'Uninstall version' +complete -c sdk -f -n '__fish_sdkman_using_command rm uninstall' \ + -a "(__fish_sdkman_candidates_with_versions)" +complete -c sdk -f -n '__fish_sdkman_specifying_candidate rm uninstall' \ + -a "(__fish_sdkman_installed_versions)" +complete -c sdk -f -n '__fish_sdkman_command_has_enough_parameters 2 rm uninstall' + # block + +# list +complete -c sdk -f -n '__fish_sdkman_no_command' \ + -a 'ls list' \ + -d 'List versions' +complete -c sdk -f -n '__fish_sdkman_using_command ls list' \ + -a "(__fish_sdkman_candidates)" +complete -c sdk -f -n '__fish_sdkman_command_has_enough_parameters 1 ls list' + # block + +# use +complete -c sdk -f -n '__fish_sdkman_no_command' \ + -a 'u use' \ + -d 'Use specific version' +complete -c sdk -f -n '__fish_sdkman_using_command u use' \ + -a "(__fish_sdkman_candidates_with_versions)" +complete -c sdk -f -n '__fish_sdkman_specifying_candidate u use' \ + -a "(__fish_sdkman_installed_versions)" +complete -c sdk -f -n '__fish_sdkman_command_has_enough_parameters 2 u use' + # block + +# default +complete -c sdk -f -n '__fish_sdkman_no_command' \ + -a 'd default' \ + -d 'Set default version' +complete -c sdk -f -n '__fish_sdkman_using_command d default' \ + -a "(__fish_sdkman_candidates_with_versions)" +complete -c sdk -f -n '__fish_sdkman_specifying_candidate d default' \ + -a "(__fish_sdkman_installed_versions)" +complete -c sdk -f -n '__fish_sdkman_command_has_enough_parameters 2 d default' + # block + +# current +complete -c sdk -f -n '__fish_sdkman_no_command' \ + -a 'c current' \ + -d 'Display current version' +complete -c sdk -f -n '__fish_sdkman_using_command c current' \ + -a "(__fish_sdkman_candidates)" +complete -c sdk -f -n '__fish_sdkman_command_has_enough_parameters 1 c current' + # block + +# upgrade +complete -c sdk -f -n '__fish_sdkman_no_command' \ + -a 'ug upgrade' \ + -d 'Display what is outdated' +complete -c sdk -f -n '__fish_sdkman_using_command ug upgrade' \ + -a "(__fish_sdkman_candidates_with_versions)" +complete -c sdk -f -n '__fish_sdkman_command_has_enough_parameters 1 ug upgrade' + # block + +# version +complete -c sdk -f -n '__fish_sdkman_no_command' \ + -a 'v version' \ + -d 'Display version' +complete -c sdk -f -n '__fish_sdkman_command_has_enough_parameters 0 v version' + # block + +# help +complete -c sdk -f -n '__fish_sdkman_no_command' \ + -a 'help' \ + -d 'Display help message' +complete -c sdk -f -n '__fish_sdkman_command_has_enough_parameters 0 h help' + # block + +# offline +complete -c sdk -f -n '__fish_sdkman_no_command' \ + -a 'offline' \ + -d 'Set offline status' +complete -c sdk -f -n '__fish_sdkman_using_command offline' \ + -a 'enable' \ + -d 'Make sdk work while offline' +complete -c sdk -f -n '__fish_sdkman_using_command offline' \ + -a 'disable' \ + -d 'Turn on all features' +complete -c sdk -f -n '__fish_sdkman_command_has_enough_parameters 1 offline' + # block + +# selfupdate +complete -c sdk -f -n '__fish_sdkman_no_command' \ + -a 'selfupdate' \ + -d 'Update sdk' +complete -c sdk -f -n '__fish_sdkman_using_command selfupdate' \ + -a 'force' \ + -d 'Force re-install of current version' +complete -c sdk -f -n '__fish_sdkman_command_has_enough_parameters 1 selfupdate' + # block + +# update +complete -c sdk -f -n '__fish_sdkman_no_command' \ + -a 'update' \ + -d 'Reload the candidate list' +complete -c sdk -f -n '__fish_sdkman_command_has_enough_parameters 0 update' + # block + +# flush +complete -c sdk -f -n '__fish_sdkman_no_command' \ + -a 'flush' \ + -d 'Clear out archives and temporary storage folders' +complete -c sdk -f -n '__fish_sdkman_using_command flush' \ + -a 'temp' \ + -d 'Clear out staging work folder' +complete -c sdk -f -n '__fish_sdkman_using_command flush' \ + -a 'version' \ + -d 'Flush version file' +complete -c sdk -f -n '__fish_sdkman_command_has_enough_parameters 1 flush' + # block + +# env +complete -c sdk -f -n '__fish_sdkman_no_command' \ + -a 'e env' \ + -d 'Load environment from .sdkmanrc file' +complete -c sdk -f -n '__fish_sdkman_using_command e env' \ + -a 'init' \ + -d 'Initialize .sdkmanrc file' +complete -c sdk -f -n '__fish_sdkman_using_command e env' \ + -a 'install' \ + -d 'Install all candidate versions listed in .sdkmanrc' +complete -c sdk -f -n '__fish_sdkman_using_command e env' \ + -a 'clear' \ + -d 'Unload currently loaded environment' +complete -c sdk -f -n '__fish_sdkman_command_has_enough_parameters 1 e env' + # block + +# home +complete -c sdk -f -n '__fish_sdkman_no_command' \ + -a 'h home' \ + -d 'Show installation folder of given candidate' +complete -c sdk -f -n '__fish_sdkman_using_command h home' \ + -a "(__fish_sdkman_candidates_with_versions)" +complete -c sdk -f -n '__fish_sdkman_specifying_candidate h home' \ + -a "(__fish_sdkman_installed_versions)" +complete -c sdk -f -n '__fish_sdkman_command_has_enough_parameters 2 h home' + # block diff --git a/fish/completions/uv.fish b/fish/completions/uv.fish new file mode 100644 index 0000000..ac24044 --- /dev/null +++ b/fish/completions/uv.fish @@ -0,0 +1 @@ +uv generate-shell-completion fish | source diff --git a/fish/conf.d/fish_frozen_key_bindings.fish b/fish/conf.d/fish_frozen_key_bindings.fish new file mode 100644 index 0000000..495aee9 --- /dev/null +++ b/fish/conf.d/fish_frozen_key_bindings.fish @@ -0,0 +1,14 @@ +# This file was created by fish when upgrading to version 4.3, to migrate +# the 'fish_key_bindings' variable from its old default scope (universal) +# to its new default scope (global). We recommend you delete this file +# and configure key bindings in ~/.config/fish/config.fish if needed. + +# set --global fish_key_bindings fish_default_key_bindings + +# Prior to version 4.3, fish shipped an event handler that runs +# `set --universal fish_key_bindings fish_default_key_bindings` +# whenever the fish_key_bindings variable is erased. +# This means that as long as any fish < 4.3 is still running on this system, +# we cannot complete the migration. +# As a workaround, erase the universal variable at every shell startup. +set --erase --universal fish_key_bindings diff --git a/fish/conf.d/fish_frozen_theme.fish b/fish/conf.d/fish_frozen_theme.fish new file mode 100644 index 0000000..7236ca1 --- /dev/null +++ b/fish/conf.d/fish_frozen_theme.fish @@ -0,0 +1,37 @@ +# This file was created by fish when upgrading to version 4.3, to migrate +# theme variables from universal to global scope. +# Don't edit this file, as it will be written by the web-config tool (`fish_config`). +# To customize your theme, delete this file and see +# help interactive#syntax-highlighting +# or +# man fish-interactive | less +/^SYNTAX.HIGHLIGHTING +# for appropriate commands to add to ~/.config/fish/config.fish instead. +# See also the release notes for fish 4.3.0 (run `help relnotes`). + +set --global fish_color_autosuggestion 555 brblack +set --global fish_color_cancel -r +set --global fish_color_command blue +set --global fish_color_comment red +set --global fish_color_cwd green +set --global fish_color_cwd_root red +set --global fish_color_end green +set --global fish_color_error brred +set --global fish_color_escape brcyan +set --global fish_color_history_current --bold +set --global fish_color_host normal +set --global fish_color_host_remote yellow +set --global fish_color_normal normal +set --global fish_color_operator brcyan +set --global fish_color_param cyan +set --global fish_color_quote yellow +set --global fish_color_redirection cyan --bold +set --global fish_color_search_match white --background=brblack +set --global fish_color_selection white --bold --background=brblack +set --global fish_color_status red +set --global fish_color_user brgreen +set --global fish_color_valid_path --underline +set --global fish_pager_color_completion normal +set --global fish_pager_color_description B3A06D yellow -i +set --global fish_pager_color_prefix normal --bold --underline +set --global fish_pager_color_progress brwhite --background=cyan +set --global fish_pager_color_selected_background -r diff --git a/fish/conf.d/sdk.fish b/fish/conf.d/sdk.fish new file mode 100644 index 0000000..420dc56 --- /dev/null +++ b/fish/conf.d/sdk.fish @@ -0,0 +1,111 @@ +#!/usr/bin/fish + +# Makes command and binaries from SDKMAN! available in fish. +# Delegates to bash for the `sdk` command. + +# Copyright (c) 2018-2023 Raphael Reitzig +# MIT License (MIT) +# https://github.com/reitzig/sdkman-for-fish + +# Account for custom install locations +if set -q __sdkman_custom_dir + set -gx SDKMAN_DIR "$__sdkman_custom_dir" +end +# Guard: SDKMAN! needs to be installed +if set -q SDKMAN_DIR; and not test -f "$SDKMAN_DIR/bin/sdkman-init.sh" + echo "WARNING: SDKMAN! installation path set to $SDKMAN_DIR, but no installation found there" + exit 0 +end + +# Unless overridden, use the default location: +if not set -q SDKMAN_DIR + set -gx SDKMAN_DIR "$HOME/.sdkman" +end + +set __fish_sdkman_init "$SDKMAN_DIR/bin/sdkman-init.sh" + +# Guard: SDKMAN! needs to be installed +if not test -f "$__fish_sdkman_init" + exit 0 +end + +# Copied from https://github.com/jorgebucaran/fisher/blob/main/functions/fisher.fish to be consistent: +set --query fisher_path || set --local fisher_path $__fish_config_dir +set __fish_sdkman_noexport_init "$fisher_path/functions/__sdkman-noexport-init.sh" + +# Hack for issue #19: +# Create version of sdkman-init that doesn't export any environment variables. +# Refresh if sdkman-init changed. +if begin not test -f "$__fish_sdkman_noexport_init"; + or env test "$__fish_sdkman_init" -nt "$__fish_sdkman_noexport_init" + end + mkdir -p (dirname $__fish_sdkman_noexport_init) + sed -E -e 's/^(\s*).*(export|to_path).*$/\1:/g' "$__fish_sdkman_init" \ + > "$__fish_sdkman_noexport_init" +end + +# Runs the given command in bash, capturing some side effects +# and repeating them on the current fish shell. +# Returns the same status code as the given command. +function __fish_sdkman_run_in_bash + # We need to leave stdin and stdout of sdk free for user interaction. + # So, pipe relevant environment variables (which might have changed) + # through a file. + # But since now getting the exit code of sdk itself is a hassle, + # pipe it as well. + # + # TODO: Can somebody get this to work without the overhead of a file? + set pipe (mktemp) + bash -c "$argv[1]; + echo -e \"\$?\" > $pipe; + env | grep -e '^SDKMAN_\|^PATH' >> $pipe; + env | grep -i -E \"^(`echo \${SDKMAN_CANDIDATES_CSV} | sed 's/,/|/g'`)_HOME\" >> $pipe; + echo \"SDKMAN_OFFLINE_MODE=\${SDKMAN_OFFLINE_MODE}\" >> $pipe; + echo \"SDKMAN_ENV=\${SDKMAN_ENV}\" >> $pipe" # it's not an environment variable! + set bashDump (cat $pipe; rm $pipe) + + set sdkStatus $bashDump[1] + set bashEnv $bashDump[2..-1] + + # If SDKMAN! succeeded, copy relevant environment variables + # to the current shell (they might have changed) + if [ $sdkStatus = 0 ] + for line in $bashEnv + set parts (string split "=" $line) + set var $parts[1] + set value (string join "=" $parts[2..-1]) + + switch "$var" + case "PATH" + # Special treatment: need fish list instead + # of colon-separated list. + set value (string split : "$value") + end + + if test -n value + set -gx $var $value + # Note: This makes SDKMAN_{OFFLINE_MODE,ENV} environment variables. + # That gives it the behaviour we _want_! + end + end + end + + return $sdkStatus +end + +# If this is a subshell of a(n initialized) fish owned by the same user, +# no initialization necessary. +# Otherwise: +if not set -q SDKMAN_CANDIDATES_DIR; or test (ls -ld "$SDKMAN_CANDIDATES_DIR" | awk '{print $3}') != (whoami) + __fish_sdkman_run_in_bash "source $__fish_sdkman_init" +end + +# Set up auto_env +if grep -q "^sdkman_auto_env=true" "$SDKMAN_DIR/etc/config" + function __fish_sdkman_autoenv --on-variable PWD + # Run the (modified) init script, which performs the checks and calls for us! + __fish_sdkman_run_in_bash "source \"$__fish_sdkman_noexport_init\"" + + set -x SDKMAN_OLD_PWD "$PWD" # needed by the Bash implementation + end +end diff --git a/fish/config.fish b/fish/config.fish new file mode 100644 index 0000000..8f7e31f --- /dev/null +++ b/fish/config.fish @@ -0,0 +1,39 @@ +if status is-interactive + # Commands to run in interactive sessions can go here + zoxide init fish | source + fzf --fish | source + + set -gx EDITOR nvim + set -gx QT_QPA_PLATFORM wayland + # set -gx GRAALVM_HOME /home/dxrkness/.sdkman/candidates/java/25-graal/ + set -gx JAVA_HOME /home/dxrkness/.sdkman/candidates/java/current/ + set -gx BUN_INSTALL $HOME/.bun + set -gx SDKMAN_DIR $HOME/.sdkman + set -gx RUSTBIN $HOME/.cargo/bin + set -gx RUSTUP_BIN $HOME/.rustup/toolchains/stable-x86_64-unknown-linux-gnu/bin + set -gx GOBIN $HOME/go/bin + set -gx LOCAL_BIN /home/dxrkness/.local/bin + set -gx PATH $PATH $GOBIN $JAVA_HOME/bin $BUN_INSTALL/bin $RUSTBIN $RUSTUP_BIN $LOCAL_BIN + set -gx TESTCONTAINERS_RYUK_DISABLED true + set -gx TESTCONTAINERS_DOCKER_SOCKET_OVERRIDE /var/run/docker.sock + set -gx CC clang + set -gx GTK_IM_MODULE ibus + set -gx QT_IM_MODULE ibus + set -gx XMODIFIERS @im=ibus + set -gx MANPAGER "nvim +Man!" + set -gx PROTON_ENABLE_WAYLAND 1 + + alias sudo doas + abbr ls "eza" + alias cp "uu-cp -v -g" + alias rm "uu-rm -v" + alias mv "uu-mv -v -g" + abbr cd z # zoxide + abbr find "fd" + abbr wlp wl-paste + + alias jcu 'journalctl --user' + alias jc journalctl + alias scu 'systemctl --user' + alias sc systemctl +end diff --git a/fish/fish_plugins b/fish/fish_plugins new file mode 100644 index 0000000..63d24b2 --- /dev/null +++ b/fish/fish_plugins @@ -0,0 +1 @@ +reitzig/sdkman-for-fish diff --git a/fish/fish_variables b/fish/fish_variables new file mode 100644 index 0000000..692fcee --- /dev/null +++ b/fish/fish_variables @@ -0,0 +1,7 @@ +# This file contains fish universal variable definitions. +# VERSION: 3.0 +SETUVAR __fish_initialized:4300 +SETUVAR _fisher_plugins:reitzig/sdkman\x2dfor\x2dfish +SETUVAR _fisher_reitzig_2F_sdkman_2D_for_2D_fish_files:\x7e/\x2econfig/fish/functions/sdk\x2efish\x1e\x7e/\x2econfig/fish/conf\x2ed/sdk\x2efish\x1e\x7e/\x2econfig/fish/completions/sdk\x2efish +SETUVAR _fisher_upgraded_to_4_4:\x1d +SETUVAR fish_user_paths:/usr/bin diff --git a/fish/functions/__sdkman-noexport-init.sh b/fish/functions/__sdkman-noexport-init.sh new file mode 100644 index 0000000..a86652c --- /dev/null +++ b/fish/functions/__sdkman-noexport-init.sh @@ -0,0 +1,175 @@ +#!/usr/bin/env bash + +# +# Copyright 2021 Marco Vermeulen +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +# set env vars if not set +if [ -z "$SDKMAN_CANDIDATES_API" ]; then + : +fi + +if [ -z "$SDKMAN_DIR" ]; then + : +fi + +# Load the sdkman config if it exists. +if [ -f "${SDKMAN_DIR}/etc/config" ]; then + source "${SDKMAN_DIR}/etc/config" +fi + +# Read the platform file +SDKMAN_PLATFORM="$(cat "${SDKMAN_DIR}/var/platform")" +: + +# OS specific support (must be 'true' or 'false'). +cygwin=false +darwin=false +solaris=false +freebsd=false +SDKMAN_KERNEL="$(uname -s)" +case "${SDKMAN_KERNEL}" in + CYGWIN*) + cygwin=true + ;; + Darwin*) + darwin=true + ;; + SunOS*) + solaris=true + ;; + FreeBSD*) + freebsd=true +esac + +# Determine shell +zsh_shell=false +bash_shell=false + +if [[ -n "$ZSH_VERSION" ]]; then + zsh_shell=true +elif [[ -n "$BASH_VERSION" ]]; then + bash_shell=true +fi + +# Source sdkman module scripts and extension files. +# +# Extension files are prefixed with 'sdkman-' and found in the ext/ folder. +# Use this if extensions are written with the functional approach and want +# to use functions in the main sdkman script. For more details, refer to +# . +OLD_IFS="$IFS" +IFS=$'\n' +scripts=($(find "${SDKMAN_DIR}/src" "${SDKMAN_DIR}/ext" -type f -name 'sdkman-*.sh')) +for f in "${scripts[@]}"; do + source "$f" +done +IFS="$OLD_IFS" +unset OLD_IFS scripts f + +# Create upgrade delay file if it doesn't exist +if [[ ! -f "${SDKMAN_DIR}/var/delay_upgrade" ]]; then + touch "${SDKMAN_DIR}/var/delay_upgrade" +fi + +# set curl connect-timeout and max-time +if [[ -z "$sdkman_curl_connect_timeout" ]]; then sdkman_curl_connect_timeout=7; fi +if [[ -z "$sdkman_curl_max_time" ]]; then sdkman_curl_max_time=10; fi + +# set curl retry +if [[ -z "${sdkman_curl_retry}" ]]; then sdkman_curl_retry=0; fi + +# set curl retry max time in seconds +if [[ -z "${sdkman_curl_retry_max_time}" ]]; then sdkman_curl_retry_max_time=60; fi + +# set curl to continue downloading automatically +if [[ -z "${sdkman_curl_continue}" ]]; then sdkman_curl_continue=true; fi + +# read list of candidates and set array +SDKMAN_CANDIDATES_CACHE="${SDKMAN_DIR}/var/candidates" +SDKMAN_CANDIDATES_CSV=$(<"$SDKMAN_CANDIDATES_CACHE") +__sdkman_echo_debug "Setting candidates csv: $SDKMAN_CANDIDATES_CSV" +if [[ "$zsh_shell" == 'true' ]]; then + SDKMAN_CANDIDATES=(${(s:,:)SDKMAN_CANDIDATES_CSV}) +else + IFS=',' read -a SDKMAN_CANDIDATES <<< "${SDKMAN_CANDIDATES_CSV}" +fi + +: + +for candidate_name in "${SDKMAN_CANDIDATES[@]}"; do + candidate_dir="${SDKMAN_CANDIDATES_DIR}/${candidate_name}/current" + if [[ -h "$candidate_dir" || -d "${candidate_dir}" ]]; then + : + : + fi +done +unset candidate_name candidate_dir +: + +# source completion scripts +if [[ "$sdkman_auto_complete" == 'true' ]]; then + if [[ "$zsh_shell" == 'true' ]]; then + # initialize zsh completions (if not already done) + if ! (( $+functions[compdef] )) ; then + autoload -Uz compinit + if [[ $ZSH_DISABLE_COMPFIX == 'true' ]]; then + compinit -u -C + else + compinit + fi + fi + autoload -U bashcompinit + bashcompinit + source "${SDKMAN_DIR}/contrib/completion/bash/sdk" + __sdkman_echo_debug "ZSH completion script loaded..." + elif [[ "$bash_shell" == 'true' ]]; then + source "${SDKMAN_DIR}/contrib/completion/bash/sdk" + __sdkman_echo_debug "Bash completion script loaded..." + else + __sdkman_echo_debug "No completion scripts found for $SHELL" + fi +fi + +if [[ "$sdkman_auto_env" == "true" ]]; then + if [[ "$zsh_shell" == "true" ]]; then + function sdkman_auto_env() { + if [[ -n $SDKMAN_ENV ]] && [[ ! $PWD =~ ^$SDKMAN_ENV ]]; then + sdk env clear + fi + if [[ -f .sdkmanrc ]]; then + sdk env + fi + } + + chpwd_functions+=(sdkman_auto_env) + else + function sdkman_auto_env() { + if [[ -n $SDKMAN_ENV ]] && [[ ! $PWD =~ ^$SDKMAN_ENV ]]; then + sdk env clear + fi + if [[ "$SDKMAN_OLD_PWD" != "$PWD" ]] && [[ -f ".sdkmanrc" ]]; then + sdk env + fi + + : + } + + trimmed_prompt_command="${PROMPT_COMMAND%"${PROMPT_COMMAND##*[![:space:]]}"}" + [[ -z "$trimmed_prompt_command" ]] && PROMPT_COMMAND="sdkman_auto_env" || PROMPT_COMMAND="${trimmed_prompt_command%\;};sdkman_auto_env" + fi + + sdkman_auto_env +fi diff --git a/fish/functions/html_to_md.fish b/fish/functions/html_to_md.fish new file mode 100644 index 0000000..3ed1092 --- /dev/null +++ b/fish/functions/html_to_md.fish @@ -0,0 +1,6 @@ +function html_to_md + set -l filename $argv[1] + set -l filename_md $(basename $filename .html).md + echo "transforming $filename => $filename_md..." + pandoc -f html -t markdown $filename -o $filename_md +end diff --git a/fish/functions/sdk.fish b/fish/functions/sdk.fish new file mode 100644 index 0000000..25def1f --- /dev/null +++ b/fish/functions/sdk.fish @@ -0,0 +1,42 @@ +# Wrapper function for SDKMAN! + +# Copyright (c) 2018-2023 Raphael Reitzig +# MIT License (MIT) +# https://github.com/reitzig/sdkman-for-fish + +function sdk -d "Manage SDKs" + # Guard: SDKMAN! needs to be installed + if not test -f "$__fish_sdkman_init" + # Propose to install SDKMAN! + + function read_confirm + while true + read -l -P "$argv[1] [y/N] " confirm + + switch $confirm + case Y y + return 0 + case '' N n + return 1 + end + end + end + + if read_confirm "You don't seem to have SDKMAN! installed. Install now?" + if not which curl > /dev/null + echo "curl required" + return 1 + end + if not which bash > /dev/null + echo "bash required" + return 1 + end + + curl -s "https://get.sdkman.io" | bash | sed '/All done!/q' + echo "Please open a new terminal/shell to load SDKMAN!" + end + else + # Declare the _actual_ sdk command for fish + __fish_sdkman_run_in_bash "source \"$__fish_sdkman_noexport_init\" && sdk $argv" + end +end diff --git a/fish/functions/source_env_file_vars.fish b/fish/functions/source_env_file_vars.fish new file mode 100644 index 0000000..bb630b1 --- /dev/null +++ b/fish/functions/source_env_file_vars.fish @@ -0,0 +1,5 @@ +function source_env_file_vars + for line in (cat $argv[1] | rg '^[^#\s]') + set -gx (string split '=' -m 1 -- $line) + end +end diff --git a/ghostty/config b/ghostty/config new file mode 100644 index 0000000..ba2be98 --- /dev/null +++ b/ghostty/config @@ -0,0 +1,8 @@ +font-size = 13 +theme = Farmhouse Dark +window-theme = ghostty +background-opacity = 0.8 +fullscreen = true +window-padding-y=0 +gtk-titlebar = false +gtk-single-instance = true diff --git a/git/config b/git/config new file mode 100644 index 0000000..035218b --- /dev/null +++ b/git/config @@ -0,0 +1,17 @@ +[user] + name = dxrknesss + email = imp0rt4ntduck@gmail.com + signingkey = imp0rt4ntduck@gmail.com +[core] + editor = nvim +[commit] + gpgsign = true +[init] + defaultObjectFormat = sha256 +[http] + version = HTTP/2 +[fetch] + prune = true + +[diff] + tool = nvimdiff diff --git a/git/gitk b/git/gitk new file mode 100644 index 0000000..0ec0153 --- /dev/null +++ b/git/gitk @@ -0,0 +1,64 @@ +set mainfont {sans 9} +set textfont {monospace 9} +set uifont {sans 9 bold} +set tabstop 8 +set findmergefiles 0 +set maxgraphpct 50 +set maxwidth 16 +set cmitmode patch +set wrapcomment none +set autoselect 1 +set autosellen 40 +set showneartags 1 +set maxrefs 20 +set visiblerefs {"master"} +set hideremotes 0 +set showlocalchanges 1 +set datetimeformat {%Y-%m-%d %H:%M:%S} +set limitdiffs 1 +set uicolor grey85 +set want_ttk 1 +set bgcolor white +set fgcolor black +set uifgcolor black +set uifgdisabledcolor #999 +set colors {"#00ff00" red blue magenta darkgrey brown orange} +set diffcolors {"#c30000" "#009800" blue} +set mergecolors {red blue "#00ff00" purple brown "#009090" magenta "#808000" "#009000" "#ff0080" cyan "#b07070" "#70b0f0" "#70f0b0" "#f0b070" "#ff70b0"} +set markbgcolor #e0e0ff +set diffcontext 3 +set selectbgcolor gray85 +set foundbgcolor yellow +set currentsearchhitbgcolor orange +set extdifftool meld +set perfile_attrs 0 +set headbgcolor #00ff00 +set headfgcolor black +set headoutlinecolor black +set remotebgcolor #ffddaa +set tagbgcolor yellow +set tagfgcolor black +set tagoutlinecolor black +set reflinecolor black +set filesepbgcolor #aaaaaa +set filesepfgcolor black +set linehoverbgcolor #ffff80 +set linehoverfgcolor black +set linehoveroutlinecolor black +set mainheadcirclecolor yellow +set workingfilescirclecolor red +set indexcirclecolor #00ff00 +set circlecolors {white blue gray blue blue} +set linkfgcolor blue +set circleoutlinecolor black +set diffbgcolors {"#fff3f3" "#f0fff0"} +set web_browser xdg-open +set geometry(main) 1273x795+482+140 +set geometry(state) normal +set geometry(topwidth) 1273 +set geometry(topheight) 255 +set geometry(pwsash0) "440 1" +set geometry(pwsash1) "780 1" +set geometry(botwidth) 550 +set geometry(botheight) 535 +set permviews {} diff --git a/htop/htoprc b/htop/htoprc new file mode 100644 index 0000000..8e075b3 --- /dev/null +++ b/htop/htoprc @@ -0,0 +1,64 @@ +# Beware! This file is rewritten by htop when settings are changed in the interface. +# The parser is also very primitive, and not human-friendly. +htop_version=3.4.1-3.4.1 +config_reader_min_version=3 +fields=0 48 17 18 38 39 40 130 2 46 47 49 1 +hide_kernel_threads=1 +hide_userland_threads=1 +hide_running_in_container=0 +shadow_other_users=0 +show_thread_names=0 +show_program_path=1 +highlight_base_name=0 +highlight_deleted_exe=1 +shadow_distribution_path_prefix=0 +highlight_megabytes=1 +highlight_threads=1 +highlight_changes=0 +highlight_changes_delay_secs=5 +find_comm_in_cmdline=1 +strip_exe_from_cmdline=1 +show_merged_command=0 +header_margin=1 +screen_tabs=1 +detailed_cpu_time=0 +cpu_count_from_one=0 +show_cpu_usage=1 +show_cpu_frequency=1 +show_cpu_temperature=1 +degree_fahrenheit=0 +show_cached_memory=1 +update_process_names=0 +account_guest_in_cpu_meter=0 +color_scheme=0 +enable_mouse=1 +delay=15 +hide_function_bar=0 +header_layout=two_50_50 +column_meters_0=LeftCPUs MemorySwap Uptime NetworkIO +column_meter_modes_0=1 1 2 2 +column_meters_1=RightCPUs Tasks LoadAverage System +column_meter_modes_1=1 2 2 2 +tree_view=1 +sort_key=47 +tree_sort_key=47 +sort_direction=-1 +tree_sort_direction=-1 +tree_view_always_by_pid=0 +all_branches_collapsed=0 +screen:Main=PID USER PRIORITY NICE M_VIRT M_RESIDENT M_SHARE M_PRIV STATE PERCENT_CPU PERCENT_MEM TIME Command +.sort_key=PERCENT_MEM +.tree_sort_key=PERCENT_MEM +.tree_view_always_by_pid=0 +.tree_view=1 +.sort_direction=-1 +.tree_sort_direction=-1 +.all_branches_collapsed=0 +screen:I/O=PID USER IO_PRIORITY IO_RATE IO_READ_RATE IO_WRITE_RATE PERCENT_SWAP_DELAY PERCENT_IO_DELAY Command +.sort_key=IO_RATE +.tree_sort_key=PID +.tree_view_always_by_pid=0 +.tree_view=0 +.sort_direction=-1 +.tree_sort_direction=1 +.all_branches_collapsed=1 diff --git a/jj/config.toml b/jj/config.toml new file mode 100644 index 0000000..90d8150 --- /dev/null +++ b/jj/config.toml @@ -0,0 +1,5 @@ +#:schema https://docs.jj-vcs.dev/latest/config-schema.json + +[user] +name = "dxrkness" +email = "dxrkness@linerds.us" diff --git a/mpv/input.conf b/mpv/input.conf new file mode 100755 index 0000000..74875e5 --- /dev/null +++ b/mpv/input.conf @@ -0,0 +1,9 @@ +UP add volume +5 +DOWN add volume -5 +WHEEL_UP add volume +5 +WHEEL_DOWN add volume -5 +s cycle sub +S cycle sub down +Alt+f script-binding quality_menu/video_formats_toggle +[ multiply speed 1/1.1 # decrease the playback speed +] multiply speed 1.1 # increase the playback speed \ No newline at end of file diff --git a/mpv/mpv.conf b/mpv/mpv.conf new file mode 100755 index 0000000..080491e --- /dev/null +++ b/mpv/mpv.conf @@ -0,0 +1,8 @@ +fullscreen=yes +alang=eng +slang=eng +title=${filename} +idle=yes +force-window=yes +hwdec=vaapi +vo=gpu-next diff --git a/mpv/scripts/copy-paste-URL.lua b/mpv/scripts/copy-paste-URL.lua new file mode 100755 index 0000000..d0ac3f1 --- /dev/null +++ b/mpv/scripts/copy-paste-URL.lua @@ -0,0 +1,53 @@ +function trim(s) + return (s:gsub("^%s*(%S+)%s*", "%1")) +end + +function openURL() + + subprocess = { + name = "subprocess", + args = { "powershell", "-Command", "Get-Clipboard", "-Raw" }, + playback_only = false, + capture_stdout = true, + capture_stderr = true + } + + mp.osd_message("Getting URL from clipboard...") + + r = mp.command_native(subprocess) + + --failed getting clipboard data for some reason + if r.status < 0 then + mp.osd_message("Failed getting clipboard data!") + print("Error(string): "..r.error_string) + print("Error(stderr): "..r.stderr) + end + + url = r.stdout + + if not url then + return + end + + --trim whitespace from string + url=trim(url) + + if not url then + mp.osd_message("clipboard empty") + return + end + + --immediately resume playback after loading URL + if mp.get_property_bool("core-idle") then + if not mp.get_property_bool("idle-active") then + mp.command("keypress space") + end + end + + --try opening url + --will fail if url is not valid + mp.osd_message("Try Opening URL:\n"..url) + mp.commandv("loadfile", url, "replace") +end + +mp.add_key_binding("ctrl+v", openURL) diff --git a/mpv/scripts/playlistmanager.lua b/mpv/scripts/playlistmanager.lua new file mode 100755 index 0000000..7b8dbbd --- /dev/null +++ b/mpv/scripts/playlistmanager.lua @@ -0,0 +1,1472 @@ +local settings = { + + -- #### FUNCTIONALITY SETTINGS + + --navigation keybindings force override only while playlist is visible + --if "no" then you can display the playlist by any of the navigation keys + dynamic_binds = true, + + -- to bind multiple keys separate them by a space + + -- main key to show playlist + key_showplaylist = "SHIFT+ENTER", + + -- display playlist while key is held down + key_peek_at_playlist = "", + + -- dynamic keys + key_moveup = "UP", + key_movedown = "DOWN", + key_movepageup = "PGUP", + key_movepagedown = "PGDWN", + key_movebegin = "HOME", + key_moveend = "END", + key_selectfile = "RIGHT LEFT", + key_unselectfile = "", + key_playfile = "ENTER", + key_removefile = "BS", + key_closeplaylist = "ESC SHIFT+ENTER", + + -- extra functionality keys + key_sortplaylist = "", + key_shuffleplaylist = "", + key_reverseplaylist = "", + key_loadfiles = "", + key_saveplaylist = "", + + --replaces matches on filenames based on extension, put as empty string to not replace anything + --replace rules are executed in provided order + --replace rule key is the pattern and value is the replace value + --uses :gsub('pattern', 'replace'), read more http://lua-users.org/wiki/StringLibraryTutorial + --'all' will match any extension or protocol if it has one + --uses json and parses it into a lua table to be able to support .conf file + + filename_replace = [[ + [ + { + "protocol": { "all": true }, + "rules": [ + { "%%(%x%x)": "hex_to_char" } + ] + } + ] + ]], + +--[=====[ START OF SAMPLE REPLACE - Remove this line to use it + --Sample replace: replaces underscore to space on all files + --for mp4 and webm; remove extension, remove brackets and surrounding whitespace, change dot between alphanumeric to space + filename_replace = [[ + [ + { + "ext": { "all": true}, + "rules": [ + { "_" : " " } + ] + },{ + "ext": { "mp4": true, "mkv": true }, + "rules": [ + { "^(.+)%..+$": "%1" }, + { "%s*[%[%(].-[%]%)]%s*": "" }, + { "(%w)%.(%w)": "%1 %2" } + ] + },{ + "protocol": { "http": true, "https": true }, + "rules": [ + { "^%a+://w*%.?": "" } + ] + } + ] + ]], +--END OF SAMPLE REPLACE ]=====] + + --json array of filetypes to search from directory + loadfiles_filetypes = [[ + [ + "jpg", "jpeg", "png", "tif", "tiff", "gif", "webp", "svg", "bmp", + "mp3", "wav", "ogm", "flac", "m4a", "wma", "ogg", "opus", + "mkv", "avi", "mp4", "ogv", "webm", "rmvb", "flv", "wmv", "mpeg", "mpg", "m4v", "3gp" + ] + ]], + + --loadfiles at startup if 1 or more items in playlist + loadfiles_on_start = false, + -- loadfiles from working directory on idle startup + loadfiles_on_idle_start = false, + --always put loaded files after currently playing file + loadfiles_always_append = false, + + --sort playlist when files are added to playlist + sortplaylist_on_file_add = false, + + --default sorting method, must be one of: "name-asc", "name-desc", "date-asc", "date-desc", "size-asc", "size-desc". + default_sort = "name-asc", + + --"linux | windows | auto" + system = "auto", + + --Use ~ for home directory. Leave as empty to use mpv/playlists + playlist_savepath = "", + + -- constant filename to save playlist as. Note that it will override existing playlist. Leave empty for generated name. + playlist_save_filename = "", + + --save playlist automatically after current file was unloaded + save_playlist_on_file_end = false, + + + --show playlist or filename every time a new file is loaded + --2 shows playlist, 1 shows current file(filename strip applied) as osd text, 0 shows nothing + --instead of using this you can also call script-message playlistmanager show playlist/filename + --ex. KEY playlist-next ; script-message playlistmanager show playlist + show_playlist_on_fileload = 0, + + --sync cursor when file is loaded from outside reasons(file-ending, playlist-next shortcut etc.) + --has the sideeffect of moving cursor if file happens to change when navigating + --good side is cursor always following current file when going back and forth files with playlist-next/prev + sync_cursor_on_load = true, + + --allow the playlist cursor to loop from end to start and vice versa + loop_cursor = true, + + --youtube-dl executable for title resolving if enabled, probably "youtube-dl" or "yt-dlp", can be absolute path + youtube_dl_executable = "youtube-dl", + + -- allow playlistmanager to write watch later config when navigating between files + allow_write_watch_later_config = true, + + -- reset cursor navigation when closing or opening playlist + reset_cursor_on_close = true, + reset_cursor_on_open = true, + + --#### VISUAL SETTINGS + + --prefer to display titles for following files: "all", "url", "none". Sorting still uses filename. + prefer_titles = "url", + + --call youtube-dl to resolve the titles of urls in the playlist + resolve_url_titles = false, + + --call ffprobe to resolve the titles of local files in the playlist (if they exist in the metadata) + resolve_local_titles = false, + + -- timeout in seconds for url title resolving + resolve_title_timeout = 15, + + -- how many url titles can be resolved at a time. Higher number might lead to stutters. + concurrent_title_resolve_limit = 10, + + --osd timeout on inactivity in seconds, use 0 for no timeout + playlist_display_timeout = 0, + + -- when peeking at playlist, show playlist at the very least for display timeout + peek_respect_display_timeout = false, + + -- the maximum amount of lines playlist will render. Optimal value depends on font/video size etc. + showamount = 9, + + --font size scales by window, if false requires larger font and padding sizes + scale_playlist_by_window=true, + --playlist 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 playlist + style_ass_tags = "{}", + --paddings from top left corner + text_padding_x = 10, + text_padding_y = 30, + + --screen dim when menu is open 0.0 - 1.0 (0 is no dim, 1 is black) + curtain_opacity=0, + + --set title of window with stripped name + set_title_stripped = false, + title_prefix = "", + title_suffix = " - mpv", + + --slice long filenames, and how many chars to show + slice_longfilenames = false, + slice_longfilenames_amount = 70, + + --Playlist header template + --%mediatitle or %filename = title or name of playing file + --%pos = position of playing file + --%cursor = position of navigation + --%plen = playlist length + --%N = newline + playlist_header = "[%cursor/%plen]", + + --Playlist file templates + --%pos = position of file with leading zeros + --%name = title or name of file + --%N = newline + --you can also use the ass tags mentioned above. For example: + -- selected_file="{\\c&HFF00FF&}➔ %name" | to add a color for selected file. However, if you + -- use ass tags you need to reset them for every line (see https://github.com/jonniek/mpv-playlistmanager/issues/20) + normal_file = "○ %name", + hovered_file = "● %name", + selected_file = "➔ %name", + playing_file = "▷ %name", + playing_hovered_file = "▶ %name", + playing_selected_file = "➤ %name", + + + -- what to show when playlist is truncated + playlist_sliced_prefix = "...", + playlist_sliced_suffix = "...", + + --output visual feedback to OSD for tasks + display_osd_feedback = true, +} +local opts = require("mp.options") +opts.read_options(settings, "playlistmanager", function(list) update_opts(list) end) + +local utils = require("mp.utils") +local msg = require("mp.msg") +local assdraw = require("mp.assdraw") + + +--check os +if settings.system=="auto" then + local o = {} + if mp.get_property_native('options/vo-mmcss-profile', o) ~= o then + settings.system = "windows" + else + settings.system = "linux" + end +end + +--global variables +local playlist_visible = false +local strippedname = nil +local path = nil +local directory = nil +local filename = nil +local pos = 0 +local plen = 0 +local cursor = 0 +--table for saved media titles for later if we prefer them +local title_table = {} +-- table for urls and local file paths that we have requested to be resolved to titles +local requested_titles = {} + +local filetype_lookup = {} + +function update_opts(changelog) + msg.verbose('updating options') + + --parse filename json + if changelog.filename_replace then + if(settings.filename_replace~="") then + settings.filename_replace = utils.parse_json(settings.filename_replace) + else + settings.filename_replace = false + end + end + + --parse loadfiles json + if changelog.loadfiles_filetypes then + settings.loadfiles_filetypes = utils.parse_json(settings.loadfiles_filetypes) + + filetype_lookup = {} + --create loadfiles set + for _, ext in ipairs(settings.loadfiles_filetypes) do + filetype_lookup[ext] = true + end + end + + if changelog.resolve_url_titles then + resolve_titles() + end + + if changelog.resolve_local_titles then + resolve_titles() + end + + if changelog.playlist_display_timeout then + keybindstimer = mp.add_periodic_timer(settings.playlist_display_timeout, remove_keybinds) + keybindstimer:kill() + end + + if playlist_visible then showplaylist() end +end + +update_opts({filename_replace = true, loadfiles_filetypes = true}) + +local sort_modes = { + { + id="name-asc", + title="name ascending", + sort_fn=function (a, b, playlist) + return alphanumsort(playlist[a].string, playlist[b].string) + end, + }, + { + id="name-desc", + title="name descending", + sort_fn=function (a, b, playlist) + return alphanumsort(playlist[b].string, playlist[a].string) + end, + }, + { + id="date-asc", + title="date ascending", + sort_fn=function (a, b) + return (get_file_info(a).mtime or 0) < (get_file_info(b).mtime or 0) + end, + }, + { + id="date-desc", + title="date descending", + sort_fn=function (a, b) + return (get_file_info(a).mtime or 0) > (get_file_info(b).mtime or 0) + end, + }, + { + id="size-asc", + title="size ascending", + sort_fn=function (a, b) + return (get_file_info(a).size or 0) < (get_file_info(b).size or 0) + end, + }, + { + id="size-desc", + title="size descending", + sort_fn=function (a, b) + return (get_file_info(a).size or 0) > (get_file_info(b).size or 0) + end, + }, +} + +local sort_mode = 1 +for mode, sort_data in pairs(sort_modes) do + if sort_data.id == settings.default_sort then + sort_mode = mode + end +end + +function is_protocol(path) + return type(path) == 'string' and path:match('^%a[%a%d-_]+://') ~= nil +end + +function on_file_loaded() + refresh_globals() + filename = mp.get_property("filename") + path = mp.get_property('path') + local media_title = mp.get_property("media-title") + if is_protocol(path) and not title_table[path] and path ~= media_title then + title_table[path] = media_title + end + + if settings.sync_cursor_on_load then + cursor=pos + --refresh playlist if cursor moved + if playlist_visible then draw_playlist() end + end + + strippedname = stripfilename(mp.get_property('media-title')) + if settings.show_playlist_on_fileload == 2 then + showplaylist() + elseif settings.show_playlist_on_fileload == 1 then + mp.commandv('show-text', strippedname) + end + if settings.set_title_stripped then + mp.set_property("title", settings.title_prefix..strippedname..settings.title_suffix) + end +end + +function on_start_file() + refresh_globals() + filename = mp.get_property("filename") + path = mp.get_property('path') + --if not a url then join path with working directory + if not is_protocol(path) then + path = utils.join_path(mp.get_property('working-directory'), path) + directory = utils.split_path(path) + else + directory = nil + end + + if settings.loadfiles_on_start and plen == 1 then + local ext = filename:match("%.([^%.]+)$") + -- a directory or playlist has been loaded, let's not do anything as mpv will expand it into files + if ext and filetype_lookup[ext:lower()] then + msg.info("Loading files from playing files directory") + playlist() + end + end +end + +function on_end_file() + if settings.save_playlist_on_file_end then save_playlist() end + strippedname = nil + path = nil + directory = nil + filename = nil + if playlist_visible then showplaylist() end +end + +function refresh_globals() + pos = mp.get_property_number('playlist-pos', 0) + plen = mp.get_property_number('playlist-count', 0) +end + +function escapepath(dir, escapechar) + return string.gsub(dir, escapechar, '\\'..escapechar) +end + +function replace_table_has_value(value, valid_values) + if value == nil or valid_values == nil then + return false + end + return valid_values['all'] or valid_values[value] +end + +local filename_replace_functions = { + --decode special characters in url + hex_to_char = function(x) return string.char(tonumber(x, 16)) end +} + +--strip a filename based on its extension or protocol according to rules in settings +function stripfilename(pathfile, media_title) + if pathfile == nil then return '' end + local ext = pathfile:match("%.([^%.]+)$") + local protocol = pathfile:match("^(%a%a+)://") + if not ext then ext = "" end + local tmp = pathfile + if settings.filename_replace and not media_title then + for k,v in ipairs(settings.filename_replace) do + if replace_table_has_value(ext, v['ext']) or replace_table_has_value(protocol, v['protocol']) then + for ruleindex, indexrules in ipairs(v['rules']) do + for rule, override in pairs(indexrules) do + override = filename_replace_functions[override] or override + tmp = tmp:gsub(rule, override) + end + end + end + end + end + if settings.slice_longfilenames and tmp:len()>settings.slice_longfilenames_amount+5 then + tmp = tmp:sub(1, settings.slice_longfilenames_amount).." ..." + end + return tmp +end + +--gets the file info of an item +function get_file_info(item) + local path = mp.get_property('playlist/' .. item - 1 .. '/filename') + if is_protocol(path) then return {} end + local file_info = utils.file_info(path) + if not file_info then + msg.warn('failed to read file info for', path) + return {} + end + + return file_info +end + +--gets a nicename of playlist entry at 0-based position i +function get_name_from_index(i, notitle) + refresh_globals() + if plen <= i then msg.error("no index in playlist", i, "length", plen); return nil end + local _, name = nil + local title = mp.get_property('playlist/'..i..'/title') + local name = mp.get_property('playlist/'..i..'/filename') + + local should_use_title = settings.prefer_titles == 'all' or is_protocol(name) and settings.prefer_titles == 'url' + --check if file has a media title stored or as property + if not title and should_use_title then + local mtitle = mp.get_property('media-title') + if i == pos and mp.get_property('filename') ~= mtitle then + if not title_table[name] then + title_table[name] = mtitle + end + title = mtitle + elseif title_table[name] then + title = title_table[name] + end + end + + --if we have media title use a more conservative strip + if title and not notitle and should_use_title then + -- Escape a string for verbatim display on the OSD + -- Ref: https://github.com/mpv-player/mpv/blob/94677723624fb84756e65c8f1377956667244bc9/player/lua/stats.lua#L145 + return stripfilename(title, true):gsub("\\", '\\\239\187\191'):gsub("{", "\\{"):gsub("^ ", "\\h") + end + + --remove paths if they exist, keeping protocols for stripping + if string.sub(name, 1, 1) == '/' or name:match("^%a:[/\\]") then + _, name = utils.split_path(name) + end + return stripfilename(name):gsub("\\", '\\\239\187\191'):gsub("{", "\\{"):gsub("^ ", "\\h") +end + +function parse_header(string) + local esc_title = stripfilename(mp.get_property("media-title"), true):gsub("%%", "%%%%") + local esc_file = stripfilename(mp.get_property("filename")):gsub("%%", "%%%%") + return string:gsub("%%N", "\\N") + :gsub("%%pos", mp.get_property_number("playlist-pos",0)+1) + :gsub("%%plen", mp.get_property("playlist-count")) + :gsub("%%cursor", cursor+1) + :gsub("%%mediatitle", esc_title) + :gsub("%%filename", esc_file) + -- undo name escape + :gsub("%%%%", "%%") +end + +function parse_filename(string, name, index) + local base = tostring(plen):len() + local esc_name = stripfilename(name):gsub("%%", "%%%%") + return string:gsub("%%N", "\\N") + :gsub("%%pos", string.format("%0"..base.."d", index+1)) + :gsub("%%name", esc_name) + -- undo name escape + :gsub("%%%%", "%%") +end + +function parse_filename_by_index(index) + local template = settings.normal_file + + local is_idle = mp.get_property_native('idle-active') + local position = is_idle and -1 or pos + + if index == position then + if index == cursor then + if selection then + template = settings.playing_selected_file + else + template = settings.playing_hovered_file + end + else + template = settings.playing_file + end + elseif index == cursor then + if selection then + template = settings.selected_file + else + template = settings.hovered_file + end + end + + return parse_filename(template, get_name_from_index(index), index) +end + + +function draw_playlist() + refresh_globals() + local ass = assdraw.ass_new() + + local _, _, a = mp.get_osd_size() + local h = 360 + local w = h * a + + if settings.curtain_opacity ~= nil and settings.curtain_opacity ~= 0 and settings.curtain_opacity < 1.0 then + -- curtain dim from https://github.com/christoph-heinrich/mpv-quality-menu/blob/501794bfbef468ee6a61e54fc8821fe5cd72c4ed/quality-menu.lua#L699-L707 + local alpha = 255 - math.ceil(255 * settings.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, w, h) + ass:draw_stop() + ass:new_event() + end + + ass:append(settings.style_ass_tags) + + -- TODO: padding should work even on different osd alignments + if mp.get_property("osd-align-x") == "left" and mp.get_property("osd-align-y") == "top" then + ass:pos(settings.text_padding_x, settings.text_padding_y) + end + + if settings.playlist_header ~= "" then + ass:append(parse_header(settings.playlist_header).."\\N") + end + + -- (visible index, playlist index) pairs of playlist entries that should be rendered + local visible_indices = {} + + local one_based_cursor = cursor + 1 + table.insert(visible_indices, one_based_cursor) + + local offset = 1; + local visible_indices_length = 1; + while visible_indices_length < settings.showamount and visible_indices_length < plen do + -- add entry for offset steps below the cursor + local below = one_based_cursor + offset + if below <= plen then + table.insert(visible_indices, below) + visible_indices_length = visible_indices_length + 1; + end + + -- add entry for offset steps above the cursor + -- also need to double check that there is still space, this happens if we have even numbered limit + local above = one_based_cursor - offset + if above >= 1 and visible_indices_length < settings.showamount and visible_indices_length < plen then + table.insert(visible_indices, 1, above) + visible_indices_length = visible_indices_length + 1; + end + + offset = offset + 1 + end + + -- both indices are 1 based + for display_index, playlist_index in pairs(visible_indices) do + if display_index == 1 and playlist_index ~= 1 then + ass:append(settings.playlist_sliced_prefix.."\\N") + elseif display_index == settings.showamount and playlist_index ~= plen then + ass:append(settings.playlist_sliced_suffix) + else + -- parse_filename_by_index expects 0 based index + ass:append(parse_filename_by_index(playlist_index - 1).."\\N") + end + end + + if settings.scale_playlist_by_window then w,h = 0, 0 end + mp.set_osd_ass(w, h, ass.text) +end + +local peek_display_timer = nil +local peek_button_pressed = false + +function peek_timeout() + peek_display_timer:kill() + if not peek_button_pressed and not playlist_visible then + remove_keybinds() + end +end + +function handle_complex_playlist_toggle(table) + local event = table["event"] + if event == "press" then + msg.error("Complex key event not supported. Falling back to normal playlist display.") + showplaylist() + elseif event == "down" then + showplaylist(1000000) + if settings.peek_respect_display_timeout then + peek_button_pressed = true + peek_display_timer = mp.add_periodic_timer(settings.playlist_display_timeout, peek_timeout) + end + elseif event == "up" then + -- set playlist state to not visible, doesn't actually hide playlist yet + -- this will allow us to check if other functionality has rendered playlist before removing binds + playlist_visible = false + + function remove_keybinds_after_timeout() + -- if playlist is still not visible then lets actually hide it + -- this lets other keys that interupt the peek to render playlist without peek up event closing it + if not playlist_visible then + remove_keybinds() + end + end + + if settings.peek_respect_display_timeout then + peek_button_pressed = false + if not peek_display_timer:is_enabled() then + mp.add_timeout(0.01, remove_keybinds_after_timeout) + end + else + -- use small delay to let dynamic binds run before keys are potentially unbound + mp.add_timeout(0.01, remove_keybinds_after_timeout) + end + end +end + +function toggle_playlist(show_function) + local show = show_function or showplaylist + if playlist_visible then + remove_keybinds() + else + -- toggle always shows without timeout + show(0) + end +end + +function showplaylist(duration) + refresh_globals() + if plen == 0 then return end + if not playlist_visible and settings.reset_cursor_on_open then + resetcursor() + end + + playlist_visible = true + add_keybinds() + + draw_playlist() + keybindstimer:kill() + + local dur = duration or settings.playlist_display_timeout + if dur > 0 then + keybindstimer = mp.add_periodic_timer(dur, remove_keybinds) + end +end + +function showplaylist_non_interactive(duration) + refresh_globals() + if plen == 0 then return end + if not playlist_visible and settings.reset_cursor_on_open then + resetcursor() + end + playlist_visible = true + draw_playlist() + keybindstimer:kill() + + local dur = duration or settings.playlist_display_timeout + if dur > 0 then + keybindstimer = mp.add_periodic_timer(dur, remove_keybinds) + end +end + +selection=nil +function selectfile() + refresh_globals() + if plen == 0 then return end + if not selection then + selection=cursor + else + selection=nil + end + showplaylist() +end + +function unselectfile() + selection=nil + showplaylist() +end + +function resetcursor() + selection = nil + cursor = mp.get_property_number('playlist-pos', 1) +end + +function removefile() + refresh_globals() + if plen == 0 then return end + selection = nil + if cursor==pos then mp.command("script-message unseenplaylist mark true \"playlistmanager avoid conflict when removing file\"") end + mp.commandv("playlist-remove", cursor) + if cursor==plen-1 then cursor = cursor - 1 end + if plen == 1 then + remove_keybinds() + else + showplaylist() + end +end + +function moveup() + refresh_globals() + if plen == 0 then return end + if cursor~=0 then + if selection then mp.commandv("playlist-move", cursor,cursor-1) end + cursor = cursor-1 + elseif settings.loop_cursor then + if selection then mp.commandv("playlist-move", cursor,plen) end + cursor = plen-1 + end + showplaylist() +end + +function movedown() + refresh_globals() + if plen == 0 then return end + if cursor ~= plen-1 then + if selection then mp.commandv("playlist-move", cursor,cursor+2) end + cursor = cursor + 1 + elseif settings.loop_cursor then + if selection then mp.commandv("playlist-move", cursor,0) end + cursor = 0 + end + showplaylist() +end + +function movepageup() + refresh_globals() + if plen == 0 or cursor == 0 then return end + local prev_cursor = cursor + cursor = cursor - settings.showamount + if cursor < 0 then cursor = 0 end + if selection then mp.commandv("playlist-move", prev_cursor, cursor) end + showplaylist() +end + +function movepagedown() + refresh_globals() + if plen == 0 or cursor == plen-1 then return end + local prev_cursor = cursor + cursor = cursor + settings.showamount + if cursor >= plen then cursor = plen-1 end + if selection then mp.commandv("playlist-move", prev_cursor, cursor+1) end + showplaylist() +end + +function movebegin() + refresh_globals() + if plen == 0 or cursor == 0 then return end + local prev_cursor = cursor + cursor = 0 + if selection then mp.commandv("playlist-move", prev_cursor, cursor) end + showplaylist() +end + +function moveend() + refresh_globals() + if plen == 0 or cursor == plen-1 then return end + local prev_cursor = cursor + cursor = plen-1 + if selection then mp.commandv("playlist-move", prev_cursor, cursor+1) end + showplaylist() +end + +function write_watch_later(force_write) + if settings.allow_write_watch_later_config then + if mp.get_property_bool("save-position-on-quit") or force_write then + mp.command("write-watch-later-config") + end + end +end + +function playlist_next(force_write) + write_watch_later(force_write) + mp.commandv("playlist-next", "weak") +end + +function playlist_prev(force_write) + write_watch_later(force_write) + mp.commandv("playlist-prev", "weak") +end + +function playfile() + refresh_globals() + if plen == 0 then return end + selection = nil + local is_idle = mp.get_property_native('idle-active') + if cursor ~= pos or is_idle then + write_watch_later() + mp.set_property("playlist-pos", cursor) + else + if cursor~=plen-1 then + cursor = cursor + 1 + end + write_watch_later() + mp.commandv("playlist-next", "weak") + end + if settings.show_playlist_on_fileload ~= 2 then + remove_keybinds() + end +end + +function file_filter(filenames) + local files = {} + for i = 1, #filenames do + local file = filenames[i] + local ext = file:match('%.([^%.]+)$') + if ext and filetype_lookup[ext:lower()] then + table.insert(files, file) + end + end + return files +end + +function get_playlist_filenames_set() + local filenames = {} + for n=0,plen-1,1 do + local filename = mp.get_property('playlist/'..n..'/filename') + local _, file = utils.split_path(filename) + filenames[file] = true + end + return filenames +end + +--Creates a playlist of all files in directory, will keep the order and position +--For exaple, Folder has 12 files, you open the 5th file and run this, the remaining 7 are added behind the 5th file and prior 4 files before it +function playlist(force_dir) + refresh_globals() + if not directory and plen > 0 then return end + local hasfile = true + if plen == 0 then + hasfile = false + dir = mp.get_property('working-directory') + else + dir = directory + end + + if dir == "." then dir = "" end + if force_dir then dir = force_dir end + + local files = file_filter(utils.readdir(dir, "files")) + table.sort(files, alphanumsort) + + if files == nil then + msg.verbose("no files in directory") + return + end + + local filenames = get_playlist_filenames_set() + local c, c2 = 0,0 + if files then + local cur = false + local filename = mp.get_property("filename") + for _, file in ipairs(files) do + if file == nil or file[1] == "." then + break + end + local appendstr = "append" + if not hasfile then + cur = true + appendstr = "append-play" + hasfile = true + end + if filename == file then + cur = true + elseif filenames[file] then + -- skip files already in playlist + elseif cur == true or settings.loadfiles_always_append then + mp.commandv("loadfile", utils.join_path(dir, file), appendstr) + msg.info("Appended to playlist: " .. file) + c2 = c2 + 1 + else + mp.commandv("loadfile", utils.join_path(dir, file), appendstr) + msg.info("Prepended to playlist: " .. file) + mp.commandv("playlist-move", mp.get_property_number("playlist-count", 1)-1, c) + c = c + 1 + end + end + if c2 > 0 or c>0 then + msg.info("Added "..c + c2.." files to playlist") + else + msg.info("No additional files found") + end + cursor = mp.get_property_number('playlist-pos', 1) + else + msg.error("Could not scan for files: "..(error or "")) + end + refresh_globals() + if playlist_visible then + showplaylist() + elseif settings.display_osd_feedback then + if c2 > 0 or c>0 then + mp.osd_message("Added "..c + c2.." files to playlist") + else + mp.osd_message("No additional files found") + end + end + return c + c2 +end + +function parse_home(path) + if not path:find("^~") then + return path + end + local home_dir = os.getenv("HOME") or os.getenv("USERPROFILE") + if not home_dir then + local drive = os.getenv("HOMEDRIVE") + local path = os.getenv("HOMEPATH") + if drive and path then + home_dir = utils.join_path(drive, path) + else + msg.error("Couldn't find home dir.") + return nil + end + end + local result = path:gsub("^~", home_dir) + return result +end + +local interactive_save = false +function activate_playlist_save() + if interactive_save then + remove_keybinds() + mp.command("script-message playlistmanager-save-interactive \"start interactive filenaming process\"") + else + save_playlist() + end +end + +--saves the current playlist into a m3u file +function save_playlist(filename) + local length = mp.get_property_number('playlist-count', 0) + if length == 0 then return end + + --get playlist save path + local savepath + if settings.playlist_savepath == nil or settings.playlist_savepath == "" then + savepath = mp.command_native({"expand-path", "~~home/"}).."/playlists" + else + savepath = parse_home(settings.playlist_savepath) + if savepath == nil then return end + end + + --create savepath if it doesn't exist + if utils.readdir(savepath) == nil then + local windows_args = {'powershell', '-NoProfile', '-Command', 'mkdir', savepath} + local unix_args = { 'mkdir', savepath } + local args = settings.system == 'windows' and windows_args or unix_args + local res = utils.subprocess({ args = args, cancellable = false }) + if res.status ~= 0 then + msg.error("Failed to create playlist save directory "..savepath..". Error: "..(res.error or "unknown")) + return + end + end + + local name = filename + if name == nil then + if settings.playlist_save_filename == nil or settings.playlist_save_filename == "" then + local date = os.date("*t") + local datestring = ("%02d-%02d-%02d_%02d-%02d-%02d"):format(date.year, date.month, date.day, date.hour, date.min, date.sec) + + name = datestring.."_playlist-size_"..length..".m3u" + else + name = settings.playlist_save_filename + end + end + + local savepath = utils.join_path(savepath, name) + local file, err = io.open(savepath, "w") + if not file then + msg.error("Error in creating playlist file, check permissions. Error: "..(err or "unknown")) + else + file:write("#EXTM3U\n") + local i=0 + while i < length do + local pwd = mp.get_property("working-directory") + local filename = mp.get_property('playlist/'..i..'/filename') + local fullpath = filename + if not is_protocol(filename) then + fullpath = utils.join_path(pwd, filename) + end + local title = mp.get_property('playlist/'..i..'/title') or title_table[filename] + if title then + file:write("#EXTINF:,"..title.."\n") + end + file:write(fullpath, "\n") + i=i+1 + end + local saved_msg = "Playlist written to: "..savepath + if settings.display_osd_feedback then mp.osd_message(saved_msg) end + msg.info(saved_msg) + file:close() + end +end + +function alphanumsort(a, b) + local function padnum(d) + local dec, n = string.match(d, "(%.?)0*(.+)") + return #dec > 0 and ("%.12f"):format(d) or ("%s%03d%s"):format(dec, #n, n) + end + return tostring(a):lower():gsub("%.?%d+",padnum)..("%3d"):format(#b) + < tostring(b):lower():gsub("%.?%d+",padnum)..("%3d"):format(#a) +end + +-- fast sort algo from https://github.com/zsugabubus/dotfiles/blob/master/.config/mpv/scripts/playlist-filtersort.lua +function sortplaylist(startover) + local playlist = mp.get_property_native('playlist') + if #playlist < 2 then return end + + local order = {} + for i=1, #playlist do + order[i] = i + playlist[i].string = get_name_from_index(i - 1, true) + end + + table.sort(order, function(a, b) + return sort_modes[sort_mode].sort_fn(a, b, playlist) + end) + + for i=1, #playlist do + playlist[order[i]].new_pos = i + end + + for i=1, #playlist do + while true do + local j = playlist[i].new_pos + if i == j then + break + end + mp.commandv('playlist-move', (i) - 1, (j + 1) - 1) + mp.commandv('playlist-move', (j - 1) - 1, (i) - 1) + playlist[j], playlist[i] = playlist[i], playlist[j] + end + end + + for i = 1, #playlist do + local filename = mp.get_property('playlist/' .. i - 1 .. '/filename') + local ext = filename:match("%.([^%.]+)$") + if not ext or not filetype_lookup[ext:lower()] then + --move the directory to the end of the playlist + mp.commandv('playlist-move', i - 1, #playlist) + end + end + + cursor = mp.get_property_number('playlist-pos', 0) + if startover then + mp.set_property('playlist-pos', 0) + end + if playlist_visible then + showplaylist() + end + if settings.display_osd_feedback then + mp.osd_message("Playlist sorted with "..sort_modes[sort_mode].title) + end +end + +function reverseplaylist() + local length = mp.get_property_number('playlist-count', 0) + if length < 2 then return end + for outer=1, length-1, 1 do + mp.commandv('playlist-move', outer, 0) + end + if playlist_visible then + showplaylist() + elseif settings.display_osd_feedback then + mp.osd_message("Playlist reversed") + end +end + +function shuffleplaylist() + refresh_globals() + if plen < 2 then return end + mp.command("playlist-shuffle") + math.randomseed(os.time()) + mp.commandv("playlist-move", pos, math.random(0, plen-1)) + + local playlist = mp.get_property_native('playlist') + for i = 1, #playlist do + local filename = mp.get_property('playlist/' .. i - 1 .. '/filename') + local ext = filename:match("%.([^%.]+)$") + if not ext or not filetype_lookup[ext:lower()] then + --move the directory to the end of the playlist + mp.commandv('playlist-move', i - 1, #playlist) + end + end + + mp.set_property('playlist-pos', 0) + refresh_globals() + if playlist_visible then + showplaylist() + elseif settings.display_osd_feedback then + mp.osd_message("Playlist shuffled") + end +end + +function bind_keys(keys, name, func, opts) + if keys == nil or keys == "" then + mp.add_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_key_binding(key, name..prefix, func, opts) + i = i + 1 + end +end + +function bind_keys_forced(keys, name, func, opts) + if keys == nil or 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 keys == nil or 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 add_keybinds() + bind_keys_forced(settings.key_moveup, 'moveup', moveup, "repeatable") + bind_keys_forced(settings.key_movedown, 'movedown', movedown, "repeatable") + bind_keys_forced(settings.key_movepageup, 'movepageup', movepageup, "repeatable") + bind_keys_forced(settings.key_movepagedown, 'movepagedown', movepagedown, "repeatable") + bind_keys_forced(settings.key_movebegin, 'movebegin', movebegin, "repeatable") + bind_keys_forced(settings.key_moveend, 'moveend', moveend, "repeatable") + bind_keys_forced(settings.key_selectfile, 'selectfile', selectfile) + bind_keys_forced(settings.key_unselectfile, 'unselectfile', unselectfile) + bind_keys_forced(settings.key_playfile, 'playfile', playfile) + bind_keys_forced(settings.key_removefile, 'removefile', removefile, "repeatable") + bind_keys_forced(settings.key_closeplaylist, 'closeplaylist', remove_keybinds) +end + +function remove_keybinds() + keybindstimer:kill() + keybindstimer = mp.add_periodic_timer(settings.playlist_display_timeout, remove_keybinds) + keybindstimer:kill() + mp.set_osd_ass(0, 0, "") + playlist_visible = false + if settings.reset_cursor_on_close then + resetcursor() + end + if settings.dynamic_binds then + unbind_keys(settings.key_moveup, 'moveup') + unbind_keys(settings.key_movedown, 'movedown') + unbind_keys(settings.key_movepageup, 'movepageup') + unbind_keys(settings.key_movepagedown, 'movepagedown') + unbind_keys(settings.key_movebegin, 'movebegin') + unbind_keys(settings.key_moveend, 'moveend') + unbind_keys(settings.key_selectfile, 'selectfile') + unbind_keys(settings.key_unselectfile, 'unselectfile') + unbind_keys(settings.key_playfile, 'playfile') + unbind_keys(settings.key_removefile, 'removefile') + unbind_keys(settings.key_closeplaylist, 'closeplaylist') + end +end + +keybindstimer = mp.add_periodic_timer(settings.playlist_display_timeout, remove_keybinds) +keybindstimer:kill() + +if not settings.dynamic_binds then + add_keybinds() +end + +if settings.loadfiles_on_idle_start and mp.get_property_number('playlist-count', 0) == 0 then + playlist() +end + +mp.observe_property('playlist-count', "number", function(_, plcount) + --if we promised to listen and sort on playlist size increase do it + if settings.sortplaylist_on_file_add and (plcount > plen) then + msg.info("Added files will be automatically sorted") + refresh_globals() + sortplaylist() + end + if playlist_visible then showplaylist() end + resolve_titles() +end) + + +url_request_queue = {} +function url_request_queue.push(item) table.insert(url_request_queue, item) end +function url_request_queue.pop() return table.remove(url_request_queue, 1) end +local url_titles_to_fetch = url_request_queue +local ongoing_url_requests = {} + +function url_fetching_throttler() + if #url_titles_to_fetch == 0 then + url_title_fetch_timer:kill() + end + + local ongoing_url_requests_count = 0 + for _, ongoing in pairs(ongoing_url_requests) do + if ongoing then + ongoing_url_requests_count = ongoing_url_requests_count + 1 + end + end + + -- start resolving some url titles if there is available slots + local amount_to_fetch = math.max(0, settings.concurrent_title_resolve_limit - ongoing_url_requests_count) + for index=1,amount_to_fetch,1 do + local file = url_titles_to_fetch.pop() + if file then + ongoing_url_requests[file] = true + resolve_ytdl_title(file) + end + end +end + +url_title_fetch_timer = mp.add_periodic_timer(0.1, url_fetching_throttler) +url_title_fetch_timer:kill() + +local_request_queue = {} +function local_request_queue.push(item) table.insert(local_request_queue, item) end +function local_request_queue.pop() return table.remove(local_request_queue, 1) end +local local_titles_to_fetch = local_request_queue +local ongoing_local_request = false + +-- this will only allow 1 concurrent local title resolve process +function local_fetching_throttler() + if not ongoing_local_request then + local file = local_titles_to_fetch.pop() + if file then + ongoing_local_request = true + resolve_ffprobe_title(file) + end + end +end + +function resolve_titles() + if settings.prefer_titles == 'none' then return end + if not settings.resolve_url_titles and not settings.resolve_local_titles then return end + + local length = mp.get_property_number('playlist-count', 0) + if length < 2 then return end + -- loop all items in playlist because we can't predict how it has changed + local added_urls = false + local added_local = false + for i=0,length - 1,1 do + local filename = mp.get_property('playlist/'..i..'/filename') + local title = mp.get_property('playlist/'..i..'/title') + if i ~= pos + and filename + and not title + and not title_table[filename] + and not requested_titles[filename] + then + requested_titles[filename] = true + if filename:match('^https?://') then + url_titles_to_fetch.push(filename) + added_urls = true + elseif settings.prefer_titles == "all" then + local_titles_to_fetch.push(filename) + added_local = true + end + end + end + if added_urls then + url_title_fetch_timer:resume() + end + if added_local then + local_fetching_throttler() + end +end + +function resolve_ytdl_title(filename) + local args = { + settings.youtube_dl_executable, + '--no-playlist', + '--flat-playlist', + '-sJ', + '--no-config', + filename, + } + local req = mp.command_native_async( + { + name = "subprocess", + args = args, + playback_only = false, + capture_stdout = true + }, + function (success, res) + ongoing_url_requests[filename] = false + if res.killed_by_us then + msg.verbose('Request to resolve url title ' .. filename .. ' timed out') + return + end + if res.status == 0 then + local json, err = utils.parse_json(res.stdout) + if not err then + local is_playlist = json['_type'] and json['_type'] == 'playlist' + local title = (is_playlist and '[playlist]: ' or '') .. json['title'] + msg.verbose(filename .. " resolved to '" .. title .. "'") + title_table[filename] = title + refresh_globals() + if playlist_visible then showplaylist() end + else + msg.error("Failed parsing json, reason: "..(err or "unknown")) + end + else + msg.error("Failed to resolve url title "..filename.." Error: "..(res.error or "unknown")) + end + end + ) + + mp.add_timeout( + settings.resolve_title_timeout, + function() + mp.abort_async_command(req) + ongoing_url_requests[filename] = false + end + ) +end + +function resolve_ffprobe_title(filename) + local args = { "ffprobe", "-show_format", "-show_entries", "format=tags", "-loglevel", "quiet", filename } + local req = mp.command_native_async( + { + name = "subprocess", + args = args, + playback_only = false, + capture_stdout = true + }, + function (success, res) + ongoing_local_request = false + local_fetching_throttler() + if res.killed_by_us then + msg.verbose('Request to resolve local title ' .. filename .. ' timed out') + return + end + if res.status == 0 then + local title = string.match(res.stdout, "title=([^\n\r]+)") + if title then + msg.verbose(filename .. " resolved to '" .. title .. "'") + title_table[filename] = title + refresh_globals() + if playlist_visible then showplaylist() end + end + else + msg.error("Failed to resolve local title "..filename.." Error: "..(res.error or "unknown")) + end + end + ) +end + +--script message handler +function handlemessage(msg, value, value2) + if msg == "show" and value == "playlist" then + if value2 ~= "toggle" then + showplaylist(value2) + return + else + toggle_playlist(showplaylist) + return + end + end + if msg == "show" and value == "playlist-nokeys" then + if value2 ~= "toggle" then + showplaylist_non_interactive(value2) + return + else + toggle_playlist(showplaylist_non_interactive) + return + end + end + if msg == "show" and value == "filename" and strippedname and value2 then + mp.commandv('show-text', strippedname, tonumber(value2)*1000 ) ; return + end + if msg == "show" and value == "filename" and strippedname then + mp.commandv('show-text', strippedname ) ; return + end + if msg == "sort" then sortplaylist(value) ; return end + if msg == "shuffle" then shuffleplaylist() ; return end + if msg == "reverse" then reverseplaylist() ; return end + if msg == "loadfiles" then playlist(value) ; return end + if msg == "save" then save_playlist(value) ; return end + if msg == "playlist-next" then playlist_next(true) ; return end + if msg == "playlist-prev" then playlist_prev(true) ; return end + if msg == "enable-interactive-save" then interactive_save = true end + if msg == "close" then remove_keybinds() end +end + +mp.register_script_message("playlistmanager", handlemessage) + +bind_keys(settings.key_sortplaylist, "sortplaylist", function() + sortplaylist() + sort_mode = sort_mode + 1 + if sort_mode > #sort_modes then sort_mode = 1 end +end) +bind_keys(settings.key_shuffleplaylist, "shuffleplaylist", shuffleplaylist) +bind_keys(settings.key_reverseplaylist, "reverseplaylist", reverseplaylist) +bind_keys(settings.key_loadfiles, "loadfiles", playlist) +bind_keys(settings.key_saveplaylist, "saveplaylist", activate_playlist_save) +bind_keys(settings.key_showplaylist, "showplaylist", showplaylist) +bind_keys( + settings.key_peek_at_playlist, + "peek_at_playlist", + handle_complex_playlist_toggle, + { complex=true } +) + +mp.register_event("start-file", on_start_file) +mp.register_event("file-loaded", on_file_loaded) +mp.register_event("end-file", on_end_file) diff --git a/mpv/scripts/quality-menu.conf b/mpv/scripts/quality-menu.conf new file mode 100755 index 0000000..f0640d5 --- /dev/null +++ b/mpv/scripts/quality-menu.conf @@ -0,0 +1,88 @@ +# KEY BINDINGS + +# move the menu cursor up +up_binding=UP WHEEL_UP +# move the menu cursor down +down_binding=DOWN WHEEL_DOWN +# select menu entry +select_binding=ENTER MBTN_LEFT +# close menu +close_menu_binding=ESC MBTN_RIGHT Ctrl+f Alt+f + +# youtube-dl version(could be youtube-dl or yt-dlp, or something else) +ytdl_ver=yt-dlp + +# formatting / cursors +selected_and_active=▶ - +selected_and_inactive=● - +unselected_and_active=▷ - +unselected_and_inactive=○ - + +# font size scales by window, if false requires larger font and padding sizes +scale_playlist_by_window=yes + +# playlist 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 playlist. More specific styling will need to be hacked in +# +# (a monospaced font is recommended but not required) +style_ass_tags={\\fnmonospace\\fs10\\bord1} + +# paddings for top left corner +text_padding_x=5 +text_padding_y=5 + +# how many seconds until the quality menu times out +# setting this to 0 deactivates the timeout +menu_timeout=6 + +# use youtube-dl to fetch a list of available formats (overrides quality_strings) +fetch_formats=yes + +# list of ytdl-format strings to choose from +quality_strings=[ {"4320p" : "bestvideo[height<=?4320p]+bestaudio/best"}, {"2160p" : "bestvideo[height<=?2160]+bestaudio/best"}, {"1440p" : "bestvideo[height<=?1440]+bestaudio/best"}, {"1080p" : "bestvideo[height<=?1080]+bestaudio/best"}, {"720p" : "bestvideo[height<=?720]+bestaudio/best"}, {"480p" : "bestvideo[height<=?480]+bestaudio/best"}, {"360p" : "bestvideo[height<=?360]+bestaudio/best"}, {"240p" : "bestvideo[height<=?240]+bestaudio/best"}, {"144p" : "bestvideo[height<=?144]+bestaudio/best"} ] + +# reset youtube-dl format to the original format string when changing files (e.g. going to the next playlist entry) +# if file was opened previously, reset to previously selected format +reset_format=yes + +# automatically fetch available formats when opening a file +fetch_on_start=yes + +# show the video format menu after opening a file +start_with_menu=yes + +# hide columns that are identical for all formats +hide_identical_columns=yes + +# which columns are shown in which order +# comma separated list, prefix column with "-" to align left +# +# columns that might be useful are: +# resolution, width, height, fps, dynamic_range, tbr, vbr, abr, asr, +# filesize, filesize_approx, vcodec, acodec, ext, video_ext, audio_ext, +# language, format, format_note, quality +# +# columns that are derived from the above, but with special treatment: +# frame_rate, bitrate_total, bitrate_video, bitrate_audio, +# codec_video, codec_audio, audio_sample_rate +# +# If those still aren't enough or you're just curious, run: +# yt-dlp -j +# This outputs unformatted JSON. +# Format it and look under "formats" to see what's available. +# +# Not all videos have all columns available. +# Be careful, misspelled columns simply won't be displayed, there is no error. +columns_video=-resolution,frame_rate,dynamic_range,language,bitrate_total,size,codec_video,codec_audio +columns_audio=audio_sample_rate,bitrate_total,size,language,codec_audio + +# columns used for sorting, see "columns_video" for available columns +# comma separated list, prefix column with "-" to reverse sorting order +# Leaving this empty keeps the order from yt-dlp/youtube-dl. +# Be careful, misspelled columns won't result in an error, +# but they might influence the result. +sort_video=height,fps,tbr,size,format_id +sort_audio=asr,tbr,size,format_id diff --git a/mpv/scripts/quality-menu.lua b/mpv/scripts/quality-menu.lua new file mode 100755 index 0000000..39d8ec3 --- /dev/null +++ b/mpv/scripts/quality-menu.lua @@ -0,0 +1,633 @@ +-- quality-menu.lua +-- +-- Change the stream video and audio quality on the fly. +-- +-- Usage: +-- add bindings to input.conf: +-- Ctrl+f script-message-to quality_menu video_formats_toggle +-- Alt+f script-message-to quality_menu audio_formats_toggle +-- +-- Displays a menu that lets you switch to different ytdl-format settings while +-- you're in the middle of a video (just like you were using the web player). + +local mp = require 'mp' +local utils = require 'mp.utils' +local msg = require 'mp.msg' +local assdraw = require 'mp.assdraw' + +local opts = { + --key bindings + up_binding = "UP WHEEL_UP", + down_binding = "DOWN WHEEL_DOWN", + select_binding = "ENTER MBTN_LEFT", + close_menu_binding = "ESC MBTN_RIGHT Ctrl+f Alt+f", + + --youtube-dl version(could be youtube-dl or yt-dlp, or something else) + ytdl_ver = "yt-dlp", + + --formatting / cursors + selected_and_active = "▶ - ", + selected_and_inactive = "● - ", + unselected_and_active = "▷ - ", + unselected_and_inactive = "○ - ", + + --font size scales by window, if false requires larger font and padding sizes + scale_playlist_by_window=true, + + --playlist 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 playlist. More specific styling will need to be hacked in + -- + --(a monospaced font is recommended but not required) + style_ass_tags = "{\\fnmonospace\\fs10\\bord1}", + + --paddings for top left corner + text_padding_x = 5, + text_padding_y = 5, + + --how many seconds until the quality menu times out + --setting this to 0 deactivates the timeout + menu_timeout = 6, + + --use youtube-dl to fetch a list of available formats (overrides quality_strings) + fetch_formats = true, + + --default menu entries + quality_strings=[[ + [ + {"4320p" : "bestvideo[height<=?4320p]+bestaudio/best"}, + {"2160p" : "bestvideo[height<=?2160]+bestaudio/best"}, + {"1440p" : "bestvideo[height<=?1440]+bestaudio/best"}, + {"1080p" : "bestvideo[height<=?1080]+bestaudio/best"}, + {"720p" : "bestvideo[height<=?720]+bestaudio/best"}, + {"480p" : "bestvideo[height<=?480]+bestaudio/best"}, + {"360p" : "bestvideo[height<=?360]+bestaudio/best"}, + {"240p" : "bestvideo[height<=?240]+bestaudio/best"}, + {"144p" : "bestvideo[height<=?144]+bestaudio/best"} + ] + ]], + + --reset ytdl-format to the original format string when changing files (e.g. going to the next playlist entry) + --if file was opened previously, reset to previously selected format + reset_format = true, + + --automatically fetch available formats when opening a file + fetch_on_start = true, + + --show the video format menu after opening a file + start_with_menu = true, + + --hide columns that are identical for all formats + hide_identical_columns = true, + + --which columns are shown in which order + --comma separated list, prefix column with "-" to align left + -- + --columns that might be useful are: + --resolution, width, height, fps, dynamic_range, tbr, vbr, abr, asr, + --filesize, filesize_approx, vcodec, acodec, ext, video_ext, audio_ext, + --language, format, format_note, quality + -- + --columns that are derived from the above, but with special treatment: + --frame_rate, bitrate_total, bitrate_video, bitrate_audio, + --codec_video, codec_audio, audio_sample_rate + -- + --If those still aren't enough or you're just curious, run: + --yt-dlp -j + --This outputs unformatted JSON. + --Format it and look under "formats" to see what's available. + -- + --Not all videos have all columns available. + --Be careful, misspelled columns simply won't be displayed, there is no error. + columns_video = '-resolution,frame_rate,dynamic_range,language,bitrate_total,size,codec_video,codec_audio', + columns_audio = 'audio_sample_rate,bitrate_total,size,language,codec_audio', + + --columns used for sorting, see "columns_video" for available columns + --comma separated list, prefix column with "-" to reverse sorting order + --Leaving this empty keeps the order from yt-dlp/youtube-dl. + --Be careful, misspelled columns won't result in an error, + --but they might influence the result. + sort_video = 'height,fps,tbr,size,format_id', + sort_audio = 'asr,tbr,size,format_id', +} +(require 'mp.options').read_options(opts, "quality-menu") +opts.quality_strings = utils.parse_json(opts.quality_strings) + +-- special thanks to reload.lua (https://github.com/4e6/mpv-reload/) +local function reload_resume() + local playlist_pos = mp.get_property_number("playlist-pos") + local reload_duration = mp.get_property_native("duration") + local time_pos = mp.get_property("time-pos") + + mp.set_property_number("playlist-pos", playlist_pos) + + -- Tries to determine live stream vs. pre-recorded VOD. VOD has non-zero + -- duration property. When reloading VOD, to keep the current time position + -- we should provide offset from the start. Stream doesn't have fixed start. + -- Decent choice would be to reload stream from it's current 'live' position. + -- That's the reason we don't pass the offset when reloading streams. + if reload_duration and reload_duration > 0 then + local function seeker() + mp.commandv("seek", time_pos, "absolute") + mp.unregister_event(seeker) + end + mp.register_event("file-loaded", seeker) + end +end + +local ytdl = { + path = opts.ytdl_ver, + searched = false, + blacklisted = {} +} + +local url_data={} +local function download_formats() + + local function get_url() + local path = mp.get_property("path") + path = string.gsub(path, "ytdl://", "") -- Strip possible ytdl:// prefix. + + local function is_url(s) + -- adapted the regex from https://stackoverflow.com/questions/3809401/what-is-a-good-regular-expression-to-match-a-url + return nil ~= string.match(path, "^[%w]-://[-a-zA-Z0-9@:%._\\+~#=]+%.[a-zA-Z0-9()][a-zA-Z0-9()]?[a-zA-Z0-9()]?[a-zA-Z0-9()]?[a-zA-Z0-9()]?[a-zA-Z0-9()]?[-a-zA-Z0-9()@:%_\\+.~#?&/=]*") + end + + return is_url(path) and path or nil + end + + local url = get_url() + if url == nil then + return + end + + if url_data[url] then + local data = url_data[url] + return data.voptions, data.aoptions, data.vfmt, data.afmt, url + end + + if opts.fetch_formats == false then + local vres = {} + for i,v in ipairs(opts.quality_strings) do + for k,v2 in pairs(v) do + vres[i] = {label = k, format=v2} + end + end + url_data[url] = {voptions=vres, aoptions={}, vfmt=nil, afmt=nil} + return vres, {}, nil, nil, url + end + + mp.osd_message("fetching available formats with youtube-dl...", 60) + + if not (ytdl.searched) then + local ytdl_mcd = mp.find_config_file(opts.ytdl_ver) + if not (ytdl_mcd == nil) then + msg.verbose("found youtube-dl at: " .. ytdl_mcd) + ytdl.path = ytdl_mcd + end + ytdl.searched = true + end + + local function exec(args) + local res, err = mp.command_native({name = "subprocess", args = args, capture_stdout = true, capture_stderr = true}) + return res.status, res.stdout, res.stderr + end + + local ytdl_format = mp.get_property("ytdl-format") + local command = nil + if (ytdl_format == nil or ytdl_format == "") then + command = {ytdl.path, "--no-warnings", "--no-playlist", "-j", url} + else + command = {ytdl.path, "--no-warnings", "--no-playlist", "-j", "-f", ytdl_format, url} + end + + msg.verbose("calling youtube-dl with command: " .. table.concat(command, " ")) + + local es, stdout, stderr = exec(command) + + if (es < 0) or (stdout == nil) or (stdout == "") then + mp.osd_message("fetching formats failed...", 2) + msg.error("failed to get format list: " .. es) + msg.error("stderr: " .. stderr) + return + end + + local json, err = utils.parse_json(stdout) + + if (json == nil) then + mp.osd_message("fetching formats failed...", 2) + msg.error("failed to parse JSON data: " .. err) + return + end + + msg.verbose("youtube-dl succeeded!") + + if json.formats == nil then + return + end + + local function string_split (inputstr, sep) + if sep == nil then + sep = "%s" + end + local t={} + for str in string.gmatch(inputstr, "([^"..sep.."]+)") do + table.insert(t, str) + end + return t + end + + local original_format=json.format_id + local formats_split = string_split(original_format, "+") + local vfmt = formats_split[1] + local afmt = formats_split[2] + + local video_formats = {} + local audio_formats = {} + local all_formats = {} + for i = #json.formats, 1, -1 do + local format = json.formats[i] + -- "none" means it is not a video + -- nil means it is unknown + local is_video = format.vcodec ~= "none" + local is_audio = format.acodec ~= "none" + if is_video then + video_formats[#video_formats+1] = format + all_formats[#all_formats+1] = format + elseif is_audio and not is_video then + audio_formats[#audio_formats+1] = format + all_formats[#all_formats+1] = format + end + end + + local function populate_special_fields(format) + format.size = format.filesize or format.filesize_approx + format.frame_rate = format.fps + format.bitrate_total = format.tbr + format.bitrate_video = format.vbr + format.bitrate_audio = format.abr + format.codec_video = format.vcodec + format.codec_audio = format.acodec + format.audio_sample_rate = format.asr + end + + for _,format in ipairs(all_formats) do + populate_special_fields(format) + end + + local function strip_minus(list) + local stripped_list = {} + local had_minus = {} + for i, val in ipairs(list) do + if string.sub(val, 1, 1) == "-" then + val = string.sub(val, 2) + had_minus[val] = true + end + stripped_list[i] = val + end + return stripped_list, had_minus + end + + local sort_video, reverse_video = strip_minus(string_split(opts.sort_video, ',')) + local sort_audio, reverse_audio = strip_minus(string_split(opts.sort_audio, ',')) + + local function comp(properties, reverse) + return function (a, b) + for _,prop in ipairs(properties) do + local a_val = a[prop] + local b_val = b[prop] + if a_val and b_val and type(a_val) ~= 'table' and a_val ~= b_val then + if reverse[prop] then + return a_val < b_val + else + return a_val > b_val + end + end + end + return false + end + end + + if #sort_video > 0 then + table.sort(video_formats, comp(sort_video, reverse_video)) + end + if #sort_audio > 0 then + table.sort(audio_formats, comp(sort_audio, reverse_audio)) + end + + local function scale_filesize(size) + if size == nil then + return "" + end + size = tonumber(size) + + local counter = 0 + while size > 1024 do + size = size / 1024 + counter = counter+1 + end + + if counter >= 3 then return string.format("%.1fGiB", size) + elseif counter >= 2 then return string.format("%.1fMiB", size) + elseif counter >= 1 then return string.format("%.1fKiB", size) + else return string.format("%.1fB ", size) + end + end + + local function scale_bitrate(br) + if br == nil then + return "" + end + br = tonumber(br) + + local counter = 0 + while br > 1000 do + br = br / 1000 + counter = counter+1 + end + + if counter >= 2 then return string.format("%.1fGbps", br) + elseif counter >= 1 then return string.format("%.1fMbps", br) + else return string.format("%.1fKbps", br) + end + end + + local function format_special_fields(format) + local size_prefix = not format.filesize and format.filesize_approx and "~" or "" + format.size = (size_prefix) .. scale_filesize(format.size) + format.frame_rate = format.fps and format.fps.."fps" or "" + format.bitrate_total = scale_bitrate(format.tbr) + format.bitrate_video = scale_bitrate(format.vbr) + format.bitrate_audio = scale_bitrate(format.abr) + format.codec_video = format.vcodec == nil and "unknown" or format.vcodec == "none" and "" or format.vcodec + format.codec_audio = format.acodec == nil and "unknown" or format.acodec == "none" and "" or format.acodec + format.audio_sample_rate = format.asr and tostring(format.asr) .. "Hz" or "" + end + + for _,format in ipairs(all_formats) do + format_special_fields(format) + end + + local function format_table(formats, columns) + local function calc_shown_columns() + local display_col = {} + local column_widths = {} + local column_values = {} + local columns, column_align_left = strip_minus(columns) + + for _,format in pairs(formats) do + for col, prop in ipairs(columns) do + local label = tostring(format[prop] or "") + format[prop] = label + + if not column_widths[col] or column_widths[col] < label:len() then + column_widths[col] = label:len() + end + + column_values[col] = column_values[col] or label + display_col[col] = display_col[col] or (column_values[col] ~= label) + end + end + + local show_columns={} + for i, width in ipairs(column_widths) do + if width > 0 and not opts.hide_identical_columns or display_col[i] then + local prop = columns[i] + show_columns[#show_columns+1] = { + prop=prop, + width=width, + align_left=column_align_left[prop] + } + end + end + return show_columns + end + + local show_columns = calc_shown_columns() + + local spacing = 2 + for i=2, #show_columns do + -- lua errors out with width > 99 ("invalid conversion specification") + show_columns[i].width = math.min(show_columns[i].width + spacing, 99) + end + + local res = {} + for _,f in ipairs(formats) do + local row = '' + for _,column in ipairs(show_columns) do + local width = column.width * (column.align_left and -1 or 1) + row = row .. string.format('%' .. width .. 's', f[column.prop] or "") + end + res[#res+1] = {label=row, format=f.format_id} + end + return res + end + + local columns_video = string_split(opts.columns_video, ',') + local columns_audio = string_split(opts.columns_audio, ',') + local vres = format_table(video_formats, columns_video) + local ares = format_table(audio_formats, columns_audio) + + url_data[url] = {voptions=vres, aoptions=ares, vfmt=vfmt, afmt=afmt} + return vres, ares , vfmt, afmt, url +end + +local function format_string(vfmt, afmt) + if vfmt and afmt then + return vfmt.."+"..afmt + elseif vfmt then + return vfmt + elseif afmt then + return afmt + else + return "" + end +end + +local destroyer = nil +local function show_menu(isvideo) + + if destroyer then + destroyer() + end + + local voptions, aoptions, vfmt, afmt, url = download_formats() + + local options + if isvideo then + options = voptions + else + options = aoptions + end + + if options == nil then + return + end + + msg.verbose("current ytdl-format: "..format_string(vfmt, afmt)) + + local active = 0 + local selected = 1 + --set the cursor to the current format + for i,v in ipairs(options) do + if v.format == (isvideo and vfmt or afmt) then + active = i + selected = active + break + end + end + + local function table_size(t) + local s = 0 + for i,v in ipairs(t) do + s = s+1 + end + return s + end + + local function choose_prefix(i) + if i == selected and i == active then return opts.selected_and_active + elseif i == selected then return opts.selected_and_inactive end + + if i ~= selected and i == active then return opts.unselected_and_active + elseif i ~= selected then return opts.unselected_and_inactive end + return "> " --shouldn't get here. + end + + local function draw_menu() + local ass = assdraw.ass_new() + + ass:pos(opts.text_padding_x, opts.text_padding_y) + ass:append(opts.style_ass_tags) + + if options[1] then + for i,v in ipairs(options) do + ass:append(choose_prefix(i)..v.label.."\\N") + end + else + ass:append("no formats found") + end + + local w, h = mp.get_osd_size() + if opts.scale_playlist_by_window then w,h = 0, 0 end + mp.set_osd_ass(w, h, ass.text) + end + + local num_options = table_size(options) + local timeout = nil + + local function selected_move(amt) + selected = selected + amt + if selected < 1 then selected = num_options + elseif selected > num_options then selected = 1 end + if timeout then + timeout:kill() + timeout:resume() + end + draw_menu() + end + + local 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 + + local 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 + + local function destroy() + if timeout then + timeout:kill() + end + mp.set_osd_ass(0,0,"") + unbind_keys(opts.up_binding, "move_up") + unbind_keys(opts.down_binding, "move_down") + unbind_keys(opts.select_binding, "select") + unbind_keys(opts.close_menu_binding, "close") + destroyer = nil + end + + if opts.menu_timeout > 0 then + timeout = mp.add_periodic_timer(opts.menu_timeout, destroy) + end + destroyer = destroy + + bind_keys(opts.up_binding, "move_up", function() selected_move(-1) end, {repeatable=true}) + bind_keys(opts.down_binding, "move_down", function() selected_move(1) end, {repeatable=true}) + if options[1] then + bind_keys(opts.select_binding, "select", function() + destroy() + if selected == active then return end + + if isvideo == true then + vfmt = options[selected].format + url_data[url].vfmt = vfmt + else + afmt = options[selected].format + url_data[url].afmt = afmt + end + mp.set_property("ytdl-raw-options", "") --reset youtube-dl raw options before changing format + mp.set_property("ytdl-format", format_string(vfmt, afmt)) + reload_resume() + end) + end + bind_keys(opts.close_menu_binding, "close", destroy) --close menu using ESC + mp.osd_message("", 0) + draw_menu() +end + +local function video_formats_toggle() + show_menu(true) +end + +local function audio_formats_toggle() + show_menu(false) +end + +-- keybind to launch menu +mp.add_key_binding(nil, "video_formats_toggle", video_formats_toggle) +mp.add_key_binding(nil, "audio_formats_toggle", audio_formats_toggle) +mp.add_key_binding(nil, "reload", reload_resume) + +local original_format = mp.get_property("ytdl-format") +local path = nil +local function file_start() + local new_path = mp.get_property("path") + if opts.reset_format and path and new_path ~= path then + local data = url_data[new_path] + if data then + msg.verbose("setting previously set format") + mp.set_property("ytdl-format", format_string(data.vfmt, data.afmt)) + else + msg.verbose("setting original format") + mp.set_property("ytdl-format", original_format) + end + end + if opts.start_with_menu and new_path ~= path then + video_formats_toggle() + elseif opts.fetch_on_start then + download_formats() + end + path = new_path +end +mp.register_event("start-file", file_start) diff --git a/nvim b/nvim new file mode 160000 index 0000000..56b9114 --- /dev/null +++ b/nvim @@ -0,0 +1 @@ +Subproject commit 56b9114bf29cdc0c0f5de78b5deae1fe0ab65db1 diff --git a/tmux/tmux.conf b/tmux/tmux.conf new file mode 100644 index 0000000..2eacec3 --- /dev/null +++ b/tmux/tmux.conf @@ -0,0 +1,20 @@ +set -g default-terminal "tmux-256color" +set -s escape-time 0 +set -g base-index 1 + +unbind C-b +set-option -g prefix C-a +bind-key C-a send-prefix +bind-key a send-prefix + +# vi key movement for copy/paste mode +# set-window-option -g mode-keys vi +# bind -T copy-mode-vi v send-keys -X begin-selection +# bind -T copy-mode-vi y send-keys -X copy-pipe-and-cancel + +bind r source-file $HOME/.config/tmux/tmux.conf \; display-message "tmux.conf reloaded" + +bind -r h select-pane -L +bind -r j select-pane -D +bind -r k select-pane -U +bind -r l select-pane -R diff --git a/zathura/zathurarc b/zathura/zathurarc new file mode 100644 index 0000000..ea10740 --- /dev/null +++ b/zathura/zathurarc @@ -0,0 +1 @@ +set selection-clipboard clipboard