├── .gitignore ├── CHANGELOG.md ├── README.md ├── dotfiles ├── autostart │ └── .config │ │ └── autostart │ │ └── kitty.desktop ├── helix │ └── .config │ │ └── helix │ │ ├── config.toml │ │ └── languages.toml ├── kitty │ └── .config │ │ └── kitty │ │ └── kitty.conf ├── lazygit │ └── .config │ │ └── lazygit │ │ └── config.yml ├── mpv │ └── .config │ │ └── mpv │ │ ├── input.conf │ │ ├── mpv.conf │ │ └── scripts │ │ └── playlistmanager.lua ├── pandoc │ └── .local │ │ └── share │ │ └── pandoc │ │ └── filters │ │ ├── diagram-generator.lua │ │ ├── include-code-files.lua │ │ ├── include-files.lua │ │ ├── pagebreak.lua │ │ └── wordcount.lua └── shell │ ├── .bash_profile │ ├── .bashrc │ └── .gitconfig ├── el9-rebuilds ├── README.md ├── aiksaurus-1.2.1-48.el9.x86_64.rpm └── zathura-pdf-mupdf-0.3.9-1.el9.x86_64.rpm ├── extras ├── SymbolsNerdFontMono-Regular.ttf └── nasa-Q1p7bh3SHj8-unsplash.jpg ├── functions.bash ├── images ├── centos-8-install-options.png ├── fzf.vim.png ├── neo-70s.jpg ├── rocky-fedora.kra └── rocky-fedora.png ├── install-setup.bash └── install.yml /.gitignore: -------------------------------------------------------------------------------- 1 | .helix/ 2 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | 12th May 2023 v4.1 2 | 3 | - Rewrite the README from scratch 4 | - Add Fedora 38 compatibility 5 | - Update scripts to use new repositories, remove binaries, and fix things that have changed 6 | - Update dependencies 7 | 8 | 29th Jan 2023 v4 9 | 10 | - Rewrite install script in Ansible 11 | 12 | 5th Jan 2022: v3.1 13 | 14 | - Archive Neovim and replace with Helix 15 | - Update binaries 16 | - Add tt terminal based typing test 17 | - Improve and tweak config files 18 | 19 | 10th July 2022: v3.0 20 | 21 | - Compatible with RHEL 9 and clones, not backwards compatible with 8 22 | - Everything refactored and improved 23 | 24 | 13th May 2022: v2.2 25 | 26 | - New Neovim 0.7 config re-written in Lua 27 | - New Neovim shortcuts and plugins 28 | 29 | 31st March 2022: v2.1 30 | 31 | - Add [todo.txt](https://github.com/todotxt/todo.txt) file type 32 | - Use new Neovim 0.6.1 EPEL8 version instead of appimage for RHEL8 clones 33 | - Automatically install Neovim plugins before first run 34 | - Fix Neovim giving error without adding dictionary file 35 | - Update Neovim plugins pinned commits 36 | - Update binaries 37 | - Update `nnn` repo to Fedora 35 from 34 38 | - Improve user messages 39 | 40 | 24th Feb 2022: v2.0 41 | 42 | - All software updated 43 | - Massive refactoring. Functions split out into modules and shared among install 44 | and setup scripts 45 | - New script for installing binaries that also uses `GitHub CLI` 46 | - More integration in the tools. For example, `Delta` diff viewer works for 47 | `Lazygit` and `fzf.vim`. 48 | - Frozen the Neovim plugins until Neovim 0.7 comes out. They are stable now. 49 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Developer Workstation Setup Script 2 | 3 | ![neo-70s](./images/neo-70s.jpg)![rocky-fedora-logos](./images/rocky-fedora.png) 4 | 5 | UPDATE: Work has now moved to the [Debian version](https://github.com/David-Else/developer-workstation-setup-script-debian). Debian 12 is now the best platform. 6 | 7 | Are you tired of spending hours setting up your development environment every time you switch to a new machine? Look no further than the Developer Workstation Setup Script! 8 | 9 | This setup script uses Ansible and Bash to quickly and easily install a variety of development and general use software on both cutting edge Fedora and stable Red Hat Enterprise Linux 9 compatible distributions. 10 | 11 | ## Features 12 | 13 | The Developer Workstation Setup Script has the following features: 14 | 15 | - Works with both cutting edge Fedora (tested up to [38](https://download.fedoraproject.org/pub/fedora/linux/releases/38/Workstation/x86_64/iso/Fedora-Workstation-Live-x86_64-38-1.6.iso)) and stable Red Hat Enterprise Linux 9 (tested up to [9.2](https://mirrors.almalinux.org/isos/x86_64/9.2.html)) compatible distributions. 16 | - Easy to customize, just add and remove packages/config from the scripts before running. 17 | - Uses [stow](https://www.gnu.org/software/stow/) to install and manage dotfiles. 18 | - Includes a variety of development and general use software: 19 | 20 | | Development | Browsers | Graphics | Sound and video | Security and backup | 21 | | --- | --- | --- | --- | --- | 22 | | Helix | Firefox | Krita | MPV | KeepassXC | 23 | | Node.js / Deno | nnn file browser | ImageMagick | Handbrake | BorgBackup | 24 | | Kitty | | | MKVToolNix | | 25 | | Lazygit | | | Blender | | 26 | | GitHub CLI | | | OBS Studio | | 27 | | Pandoc | | | | | 28 | | Shellcheck / Shfmt | | | | | 29 | | Bat | | | | | 30 | | Ripgrep | | | | | 31 | | Delta | | | | | 32 | 33 | ## Installation 34 | 35 | These scripts are designed to be run immediately after installing the operating system. 36 | 37 | ![el9](./images/centos-8-install-options.png) 38 | 39 | 1. Install a fresh copy of Fedora or a Red Hat Enterprise Linux 9 compatible distribution. If you are using an el9 clone, select `workstation` from the software selection option during installation. You must also give your user account administrative privileges, this is a tick-box when you are creating the user. 40 | 2. Clone the repository and `cd` into it: `git clone https://github.com/David-Else/developer-workstation-setup-script` 41 | 3. Install Ansible: 42 | 43 | If you are using el9, you need to first enable the epel repository: 44 | 45 | `sudo dnf config-manager --set-enabled crb` and `sudo dnf install epel-release`. 46 | 47 | Install Ansible and the community collection: 48 | 49 | `sudo dnf install ansible-core ansible-collection-community-general` 50 | 51 | 4. Customize the software selection by modifying the `install.yml` and `install-setup.bash` scripts with your own software preferences. 52 | 5. Run the scripts: `ansible-playbook ./install.yml -K` and `./install-setup.bash` 53 | 54 | Note: Your `BECOME` password in Ansible is your user password, your account must have administrative privileges. 55 | 56 | After installation, you must run `nnn` once with `-a` to create the fifo file for the preview feature to work. 57 | 58 | ## Optional Tweaks 59 | 60 | Based on your software selection, hardware, and personal preferences, you may want to make the following changes: 61 | 62 | ### Audio 63 | 64 | - Set the available sample rates for your audio interface: 65 | 66 | 1. Find your audio interface(s) and available sample rates: 67 | 68 | `cat /proc/asound/cards` 69 | 70 | Example output: 71 | 72 | ```sh 73 | 0 [HDMI ]: HDA-Intel - HDA ATI HDMI 74 | HDA ATI HDMI at 0xf7e60000 irq 31 75 | 1 [USB ]: USB-Audio - Scarlett 6i6 USB 76 | Focusrite Scarlett 6i6 USB at usb-0000:00:14.0-10, high speed 77 | ``` 78 | 79 | Play some audio and examine the stream for your audio interface (in this case `card1`): 80 | 81 | `cat /proc/asound/card1/stream0` 82 | 83 | Example output: 84 | 85 | ```sh 86 | Focusrite Scarlett 6i6 USB at usb-0000:00:14.0-10, high speed : USB Audio 87 | 88 | Playback: 89 | Status: Running 90 | Interface = 1 91 | Altset = 1 92 | Packet Size = 216 93 | Momentary freq = 48000 Hz (0x6.0000) 94 | Feedback Format = 16.16 95 | Interface 1 96 | Altset 1 97 | Format: S32_LE 98 | Channels: 6 99 | Endpoint: 0x01 (1 OUT) (ASYNC) 100 | Rates: 44100, 48000, 88200, 96000, 176400, 192000 101 | Data packet interval: 125 us 102 | Bits: 24 103 | Channel map: FL FR FC LFE RL RR 104 | Sync Endpoint: 0x81 (1 IN) 105 | Sync EP Interface: 1 106 | Sync EP Altset: 1 107 | Implicit Feedback Mode: No 108 | ``` 109 | 110 | 2. Create a PipeWire user config file: `cp /usr/share/pipewire/pipewire.conf ~/.config/pipewire/` 111 | 3. Add/modify your sound cards available sample rates by editing `~/.config/pipewire/pipewire.conf`: 112 | 113 | The Fedora default is: 114 | 115 | ```sh 116 | #default.clock.allowed-rates = [ 48000 ] 117 | ``` 118 | 119 | For the Scarlett 6i6 example above replace it with: 120 | 121 | ```sh 122 | default.clock.allowed-rates = [ 44100 48000 88200 96000 176400 192000 ] 123 | ``` 124 | 125 | Don't forget to remove the `#` comment. 126 | 127 | - Setup PipeWire for low latency audio by following the guide at https://jackaudio.org/faq/linux_rt_config.html and creating the following file: 128 | 129 | Note: Copy code blocks by clicking on the top right-hand corner, then just paste them into your terminal. 130 | 131 | ```sh 132 | cat <<'EOF' | sudo tee /etc/security/limits.d/audio.conf 133 | @audio - rtprio 95 134 | @audio - memlock unlimited 135 | EOF 136 | ``` 137 | 138 | Add yourself to the `audio` group that you have given the privileges to with `sudo usermod -aG audio [username]`. 139 | 140 | Create a user config file for your (PipeWire) JACK settings: 141 | 142 | ```sh 143 | mkdir -p ~/.config/pipewire/jack.conf.d/ 144 | cat >~/.config/pipewire/jack.conf.d/jack.conf < deno.sh` and `sudo mv deno.sh /etc/profile.d`. 181 | - Setup Vale: 182 | 183 | Change the global `.vale.ini` file in your `$HOME` directory to point to an empty directory you want to store your styles, for example: 184 | 185 | ```sh 186 | StylesPath = ~/Documents/styles 187 | ``` 188 | 189 | Run `vale sync`. You can create a new config file at [Config Generator](https://vale.sh/generator) 190 | 191 | - Setup HEIF, AVIF and WebP image formats (inc Apple `.HEIC` photos) by adding: 192 | 193 | ```sh 194 | sudo dnf install libheif-freeworld libheif-tools heif-pixbuf-loader webp-pixbuf-loader 195 | ``` 196 | 197 | - Setup Git: 198 | 199 | ```sh 200 | git config --global user.email "you@example.com" 201 | git config --global user.name "Your Name" 202 | ``` 203 | 204 | ```sh 205 | git config --global user.signingkey key 206 | git config --global commit.gpgsign true 207 | ``` 208 | 209 | # FAQ 210 | 211 | If you would like to use Code for things that Helix still struggles with (like debugging), and still use all the modal keyboard shortcuts, I suggest installing `silverquark.dancehelix` or `asvetliakov.vscode-neovim` and using these settings: 212 | 213 | `settings.json` 214 | 215 | ```jsonc 216 | { 217 | // font size 218 | "editor.fontSize": 15, 219 | "markdown.preview.fontSize": 15, 220 | "terminal.integrated.fontSize": 15, 221 | // asvetliakov.vscode-neovim 222 | "editor.scrollBeyondLastLine": false, 223 | "vscode-neovim.neovimExecutablePaths.linux": "/usr/local/bin/nvim", // for el9 clones, or "/usr/bin/nvim" for Fedora 224 | "workbench.list.automaticKeyboardNavigation": false, 225 | // various 226 | "window.titleBarStyle": "custom", // adjust the appearance of the window title bar for linux 227 | "editor.minimap.enabled": false, // controls whether the minimap is shown 228 | "workbench.activityBar.visible": false, // controls the visibility of the activity bar in the workbench 229 | "window.menuBarVisibility": "hidden", // control the visibility of the menu bar 230 | "files.restoreUndoStack": false, // don't restore the undo stack when a file is reopened 231 | "editor.dragAndDrop": false, // controls whether the editor should allow moving selections via drag and drop 232 | "telemetry.enableTelemetry": false // disable diagnostic data collection 233 | } 234 | ``` 235 | 236 | You might also like to install `ms-vscode.live-server` for live debugging in Code or the browser. 237 | 238 | **Q**: Does this script disable the caps lock key? I've noticed that it works during login but after that it stops working altogether. 239 | 240 | **A**: It makes the caps lock into delete for touch typing purposes, to change it modify this line in `install.yml`: 241 | 242 | ```yml 243 | - { key: "/org/gnome/desktop/input-sources/xkb-options", value: "['caps:backspace', 'terminate:ctrl_alt_bksp', 'lv3:rwin_switch', 'altwin:meta_alt']" } 244 | ``` 245 | -------------------------------------------------------------------------------- /dotfiles/autostart/.config/autostart/kitty.desktop: -------------------------------------------------------------------------------- 1 | [Desktop Entry] 2 | Version=1.0 3 | Type=Application 4 | Name=kitty 5 | GenericName=Terminal emulator 6 | Comment=Fast, feature-rich, GPU based terminal 7 | TryExec=kitty 8 | Exec=kitty 9 | Icon=kitty 10 | Categories=System;TerminalEmulator; 11 | -------------------------------------------------------------------------------- /dotfiles/helix/.config/helix/config.toml: -------------------------------------------------------------------------------- 1 | theme = "dark_plus" 2 | 3 | [keys.normal] 4 | G = "goto_file_end" # vim 5 | Z = { Z = ":wq", Q = ":q!" } # vim 6 | "*" = ["move_prev_word_start", "move_next_word_end", "search_selection"] # vim 7 | "#" = "toggle_comments" 8 | 9 | [keys.normal.space] 10 | w = ":write" 11 | l = ":toggle soft-wrap.enable" 12 | i = ":toggle lsp.display-inlay-hints" 13 | c = { r = ":sh kitty @ send-text --match 'title:^Terminal' cargo run \\\\n", t = ":sh kitty @ send-text --match 'title:^Terminal' 'clear \\ncargo test \\n'" } 14 | 15 | [keys.insert] 16 | j = { k = "normal_mode" } 17 | C-backspace = "delete_word_backward" 18 | 19 | [keys.select] 20 | G = "goto_file_end" # vim 21 | 22 | [editor.cursor-shape] 23 | insert = "bar" # change cursor shape in insert mode 24 | 25 | [editor.file-picker] 26 | hidden = false # don't ignore hidden files 27 | 28 | [editor.statusline] 29 | center = ["version-control"] 30 | 31 | [editor.soft-wrap] 32 | enable = true 33 | -------------------------------------------------------------------------------- /dotfiles/helix/.config/helix/languages.toml: -------------------------------------------------------------------------------- 1 | [[language]] 2 | name = "bash" 3 | formatter = { command = 'shfmt', args = ["-i", "4"] } 4 | auto-format = true 5 | 6 | [[language]] 7 | name = "javascript" 8 | formatter = { command = 'deno', args = ["fmt", "-", "--ext", "js"] } 9 | auto-format = true 10 | 11 | [[language]] 12 | name = "typescript" 13 | formatter = { command = 'deno', args = ["fmt", "-", "--ext", "ts"] } 14 | auto-format = true 15 | 16 | [[language]] 17 | name = "git-commit" 18 | language-server = { command = "ltex-ls" } 19 | 20 | [[language]] 21 | name = "markdown" 22 | text-width = 80 23 | soft-wrap = { wrap-at-text-width = true } 24 | language-server = { command = "ltex-ls" } 25 | config = { ltex.disabledRules = { "en-US" = [ 26 | "PROFANITY", 27 | ], "en-GB" = [ 28 | "PROFANITY", 29 | ] }, ltex.dictionary = { "en-US" = [ 30 | "builtin", 31 | ], "en-GB" = [ 32 | "builtin", 33 | ] } } 34 | formatter = { command = 'prettier', args = [ 35 | "--parser", 36 | "markdown", 37 | "--prose-wrap", 38 | "never", # 39 | ] } 40 | # auto-format = true 41 | 42 | [[language]] 43 | name = "rust" 44 | [language.config] 45 | checkOnSave = { command = "clippy" } 46 | -------------------------------------------------------------------------------- /dotfiles/kitty/.config/kitty/kitty.conf: -------------------------------------------------------------------------------- 1 | font_family Monospace Regular 2 | font_size 12.0 3 | adjust_column_width 110% 4 | hide_window_decorations yes 5 | enabled_layouts stack,tall 6 | enable_audio_bell no 7 | 8 | tab_bar_edge top 9 | tab_bar_style separator 10 | tab_separator " | " 11 | 12 | allow_remote_control yes 13 | listen_on unix:/tmp/kitty 14 | 15 | map ctrl+= change_font_size all 22.0 16 | map ctrl+0 change_font_size all 0 17 | 18 | map ctrl+t launch --cwd=current --type=tab 19 | map ctrl+enter launch --cwd=current 20 | map ctrl+1 first_window 21 | map ctrl+2 second_window 22 | map ctrl+3 third_window 23 | map ctrl+4 fourth_window 24 | map ctrl+5 fifth_window 25 | map ctrl+6 sixth_window 26 | map ctrl+] next_window 27 | map ctrl+[ previous_window 28 | 29 | # Nerd Fonts v2.3.3 'Symbols-2048-em Nerd Font Complete Mono.ttf' 30 | symbol_map U+23FB-U+23FE,U+2665,U+26A1,U+2B58,U+E000-U+E00A,U+E0A0-U+E0A3,U+E0B0-U+E0C8,U+E0CA,U+E0CC-U+E0D2,U+E0D4,U+E200-U+E2A9,U+E300-U+E3E3,U+E5FA-U+E634,U+E700-U+E7C5,U+EA60-U+EBEB,U+F000-U+F2E0,U+F300-U+F32F,U+F400-U+F4A9,U+F500-U+F8FF Symbols Nerd Font Mono 31 | 32 | -------------------------------------------------------------------------------- /dotfiles/lazygit/.config/lazygit/config.yml: -------------------------------------------------------------------------------- 1 | git: 2 | paging: 3 | colorArg: always 4 | pager: delta --paging=never 5 | 6 | refresher: 7 | refreshInterval: 5 8 | 9 | gui: 10 | showIcons: true -------------------------------------------------------------------------------- /dotfiles/mpv/.config/mpv/input.conf: -------------------------------------------------------------------------------- 1 | Alt+h add video-pan-x -0.05 2 | Alt+l add video-pan-x 0.05 3 | Alt+k add video-pan-y -0.05 4 | Alt+j add video-pan-y 0.05 5 | 6 | Alt+= add video-zoom 0.05 7 | Alt+- add video-zoom -0.05 8 | -------------------------------------------------------------------------------- /dotfiles/mpv/.config/mpv/mpv.conf: -------------------------------------------------------------------------------- 1 | profile=gpu-hq 2 | hwdec=auto 3 | fullscreen=yes 4 | 5 | image-display-duration=inf 6 | reset-on-next-file=all 7 | -------------------------------------------------------------------------------- /dotfiles/mpv/.config/mpv/scripts/playlistmanager.lua: -------------------------------------------------------------------------------- 1 | local settings = { 2 | 3 | -- #### FUNCTIONALITY SETTINGS 4 | 5 | --navigation keybindings force override only while playlist is visible 6 | --if "no" then you can display the playlist by any of the navigation keys 7 | dynamic_binds = true, 8 | 9 | -- main key 10 | key_showplaylist = "SHIFT+ENTER", 11 | 12 | -- dynamic keys - to bind multiple keys separate them by a space 13 | key_moveup = "UP", 14 | key_movedown = "DOWN", 15 | key_movepageup = "PGUP", 16 | key_movepagedown = "PGDWN", 17 | key_movebegin = "HOME", 18 | key_moveend = "END", 19 | key_selectfile = "RIGHT LEFT", 20 | key_unselectfile = "", 21 | key_playfile = "ENTER", 22 | key_removefile = "BS", 23 | key_closeplaylist = "ESC", 24 | 25 | -- extra functionality keys 26 | key_sortplaylist = "", 27 | key_shuffleplaylist = "", 28 | key_reverseplaylist = "", 29 | key_loadfiles = "P", 30 | key_saveplaylist = "", 31 | 32 | --replaces matches on filenames based on extension, put as empty string to not replace anything 33 | --replace rules are executed in provided order 34 | --replace rule key is the pattern and value is the replace value 35 | --uses :gsub('pattern', 'replace'), read more http://lua-users.org/wiki/StringLibraryTutorial 36 | --'all' will match any extension or protocol if it has one 37 | --uses json and parses it into a lua table to be able to support .conf file 38 | 39 | filename_replace = "", 40 | 41 | --[=====[ START OF SAMPLE REPLACE, to use remove start and end line 42 | --Sample replace: replaces underscore to space on all files 43 | --for mp4 and webm; remove extension, remove brackets and surrounding whitespace, change dot between alphanumeric to space 44 | filename_replace = [[ 45 | [ 46 | { 47 | "ext": { "all": true}, 48 | "rules": [ 49 | { "_" : " " } 50 | ] 51 | },{ 52 | "ext": { "mp4": true, "mkv": true }, 53 | "rules": [ 54 | { "^(.+)%..+$": "%1" }, 55 | { "%s*[%[%(].-[%]%)]%s*": "" }, 56 | { "(%w)%.(%w)": "%1 %2" } 57 | ] 58 | },{ 59 | "protocol": { "http": true, "https": true }, 60 | "rules": [ 61 | { "^%a+://w*%.?": "" } 62 | ] 63 | } 64 | ] 65 | ]], 66 | --END OF SAMPLE REPLACE ]=====] 67 | 68 | --json array of filetypes to search from directory 69 | loadfiles_filetypes = [[ 70 | [ 71 | "jpg", "jpeg", "png", "tif", "tiff", "gif", "webp", "svg", "bmp", 72 | "mp3", "wav", "ogm", "flac", "tta", "m4a", "wma", "ogg", "opus", 73 | "mkv", "avi", "mp4", "ogv", "webm", "rmvb", "flv", "wmv", "mpeg", "mpg", "m4v", "3gp" 74 | ] 75 | ]], 76 | 77 | --loadfiles at startup if 1 or more items in playlist 78 | loadfiles_on_start = false, 79 | -- loadfiles from working directory on idle startup 80 | loadfiles_on_idle_start = false, 81 | --always put loaded files after currently playing file 82 | loadfiles_always_append = false, 83 | 84 | --sort playlist on mpv start 85 | sortplaylist_on_start = false, 86 | 87 | --sort playlist when files are added to playlist 88 | sortplaylist_on_file_add = false, 89 | 90 | --use alphanumerical sort 91 | alphanumsort = true, 92 | 93 | --"linux | windows | auto" 94 | system = "auto", 95 | 96 | --Use ~ for home directory. Leave as empty to use mpv/playlists 97 | playlist_savepath = "", 98 | 99 | --save playlist automatically after current file was unloaded 100 | save_playlist_on_file_end = false, 101 | 102 | 103 | --show playlist or filename every time a new file is loaded 104 | --2 shows playlist, 1 shows current file(filename strip applied) as osd text, 0 shows nothing 105 | --instead of using this you can also call script-message playlistmanager show playlist/filename 106 | --ex. KEY playlist-next ; script-message playlistmanager show playlist 107 | show_playlist_on_fileload = 0, 108 | 109 | --sync cursor when file is loaded from outside reasons(file-ending, playlist-next shortcut etc.) 110 | --has the sideeffect of moving cursor if file happens to change when navigating 111 | --good side is cursor always following current file when going back and forth files with playlist-next/prev 112 | sync_cursor_on_load = true, 113 | 114 | --playlist open key will toggle visibility instead of refresh, best used with long timeout 115 | open_toggles = true, 116 | 117 | --allow the playlist cursor to loop from end to start and vice versa 118 | loop_cursor = true, 119 | 120 | --youtube-dl executable for title resolving if enabled, probably "youtube-dl" or "yt-dlp", can be absolute path 121 | youtube_dl_executable = "youtube-dl", 122 | 123 | 124 | --#### VISUAL SETTINGS 125 | 126 | --prefer to display titles for following files: "all", "url", "none". Sorting still uses filename. 127 | prefer_titles = "url", 128 | 129 | --call youtube-dl to resolve the titles of urls in the playlist 130 | resolve_titles = false, 131 | 132 | -- timeout in seconds for title resolving 133 | resolve_title_timeout = 15, 134 | 135 | --osd timeout on inactivity, with high value on this open_toggles is good to be true 136 | playlist_display_timeout = 5, 137 | 138 | --amount of entries to show before slicing. Optimal value depends on font/video size etc. 139 | showamount = 16, 140 | 141 | --font size scales by window, if false requires larger font and padding sizes 142 | scale_playlist_by_window=true, 143 | --playlist ass style overrides inside curly brackets, \keyvalue is one field, extra \ for escape in lua 144 | --example {\\fnUbuntu\\fs10\\b0\\bord1} equals: font=Ubuntu, size=10, bold=no, border=1 145 | --read http://docs.aegisub.org/3.2/ASS_Tags/ for reference of tags 146 | --undeclared tags will use default osd settings 147 | --these styles will be used for the whole playlist 148 | style_ass_tags = "{}", 149 | --paddings from top left corner 150 | text_padding_x = 10, 151 | text_padding_y = 30, 152 | 153 | --set title of window with stripped name 154 | set_title_stripped = false, 155 | title_prefix = "", 156 | title_suffix = " - mpv", 157 | 158 | --slice long filenames, and how many chars to show 159 | slice_longfilenames = false, 160 | slice_longfilenames_amount = 70, 161 | 162 | --Playlist header template 163 | --%mediatitle or %filename = title or name of playing file 164 | --%pos = position of playing file 165 | --%cursor = position of navigation 166 | --%plen = playlist length 167 | --%N = newline 168 | playlist_header = "[%cursor/%plen]", 169 | 170 | --Playlist file templates 171 | --%pos = position of file with leading zeros 172 | --%name = title or name of file 173 | --%N = newline 174 | --you can also use the ass tags mentioned above. For example: 175 | -- selected_file="{\\c&HFF00FF&}➔ %name" | to add a color for selected file. However, if you 176 | -- use ass tags you need to reset them for every line (see https://github.com/jonniek/mpv-playlistmanager/issues/20) 177 | normal_file = "○ %name", 178 | hovered_file = "● %name", 179 | selected_file = "➔ %name", 180 | playing_file = "▷ %name", 181 | playing_hovered_file = "▶ %name", 182 | playing_selected_file = "➤ %name", 183 | 184 | 185 | -- what to show when playlist is truncated 186 | playlist_sliced_prefix = "...", 187 | playlist_sliced_suffix = "...", 188 | 189 | --output visual feedback to OSD for tasks 190 | display_osd_feedback = true, 191 | 192 | -- reset cursor navigation when playlist is not visible 193 | reset_cursor_on_close = true, 194 | } 195 | local opts = require("mp.options") 196 | opts.read_options(settings, "playlistmanager", function(list) update_opts(list) end) 197 | 198 | local utils = require("mp.utils") 199 | local msg = require("mp.msg") 200 | local assdraw = require("mp.assdraw") 201 | 202 | 203 | --check os 204 | if settings.system=="auto" then 205 | local o = {} 206 | if mp.get_property_native('options/vo-mmcss-profile', o) ~= o then 207 | settings.system = "windows" 208 | else 209 | settings.system = "linux" 210 | end 211 | end 212 | 213 | --global variables 214 | local playlist_visible = false 215 | local strippedname = nil 216 | local path = nil 217 | local directory = nil 218 | local filename = nil 219 | local pos = 0 220 | local plen = 0 221 | local cursor = 0 222 | --table for saved media titles for later if we prefer them 223 | local url_table = {} 224 | -- table for urls that we have request to be resolved to titles 225 | local requested_urls = {} 226 | --state for if we sort on playlist size change 227 | local sort_watching = false 228 | 229 | local filetype_lookup = {} 230 | 231 | function update_opts(changelog) 232 | msg.verbose('updating options') 233 | 234 | --parse filename json 235 | if changelog.filename_replace then 236 | if(settings.filename_replace~="") then 237 | settings.filename_replace = utils.parse_json(settings.filename_replace) 238 | else 239 | settings.filename_replace = false 240 | end 241 | end 242 | 243 | --parse loadfiles json 244 | if changelog.loadfiles_filetypes then 245 | settings.loadfiles_filetypes = utils.parse_json(settings.loadfiles_filetypes) 246 | 247 | filetype_lookup = {} 248 | --create loadfiles set 249 | for _, ext in ipairs(settings.loadfiles_filetypes) do 250 | filetype_lookup[ext] = true 251 | end 252 | end 253 | 254 | if changelog.resolve_titles then 255 | resolve_titles() 256 | end 257 | 258 | if changelog.playlist_display_timeout then 259 | keybindstimer = mp.add_periodic_timer(settings.playlist_display_timeout, remove_keybinds) 260 | keybindstimer:kill() 261 | end 262 | 263 | if playlist_visible then showplaylist() end 264 | end 265 | 266 | update_opts({filename_replace = true, loadfiles_filetypes = true}) 267 | 268 | function on_loaded() 269 | filename = mp.get_property("filename") 270 | path = mp.get_property('path') 271 | --if not a url then join path with working directory 272 | if not path:match("^%a%a+:%/%/") then 273 | path = utils.join_path(mp.get_property('working-directory'), path) 274 | directory = utils.split_path(path) 275 | else 276 | directory = nil 277 | end 278 | 279 | refresh_globals() 280 | if settings.sync_cursor_on_load then 281 | cursor=pos 282 | --refresh playlist if cursor moved 283 | if playlist_visible then draw_playlist() end 284 | end 285 | 286 | local media_title = mp.get_property("media-title") 287 | if path:match('^https?://') and not url_table[path] and path ~= media_title then 288 | url_table[path] = media_title 289 | end 290 | 291 | strippedname = stripfilename(mp.get_property('media-title')) 292 | if settings.show_playlist_on_fileload == 2 then 293 | showplaylist() 294 | elseif settings.show_playlist_on_fileload == 1 then 295 | mp.commandv('show-text', strippedname) 296 | end 297 | if settings.set_title_stripped then 298 | mp.set_property("title", settings.title_prefix..strippedname..settings.title_suffix) 299 | end 300 | 301 | local didload = false 302 | if settings.loadfiles_on_start and plen == 1 then 303 | didload = true --save reference for sorting 304 | msg.info("Loading files from playing files directory") 305 | playlist() 306 | end 307 | 308 | --if we promised to sort files on launch do it 309 | if promised_sort then 310 | promised_sort = false 311 | msg.info("Your playlist is sorted before starting playback") 312 | if didload then sortplaylist() else sortplaylist(true) end 313 | end 314 | 315 | --if we promised to listen and sort on playlist size increase do it 316 | if promised_sort_watch then 317 | promised_sort_watch = false 318 | sort_watching = true 319 | msg.info("Added files will be automatically sorted") 320 | mp.observe_property('playlist-count', "number", autosort) 321 | end 322 | end 323 | 324 | function on_closed() 325 | if settings.save_playlist_on_file_end then save_playlist() end 326 | strippedname = nil 327 | path = nil 328 | directory = nil 329 | filename = nil 330 | if playlist_visible then showplaylist() end 331 | end 332 | 333 | function refresh_globals() 334 | pos = mp.get_property_number('playlist-pos', 0) 335 | plen = mp.get_property_number('playlist-count', 0) 336 | end 337 | 338 | function escapepath(dir, escapechar) 339 | return string.gsub(dir, escapechar, '\\'..escapechar) 340 | end 341 | 342 | --strip a filename based on its extension or protocol according to rules in settings 343 | function stripfilename(pathfile, media_title) 344 | if pathfile == nil then return '' end 345 | local ext = pathfile:match("^.+%.(.+)$") 346 | local protocol = pathfile:match("^(%a%a+)://") 347 | if not ext then ext = "" end 348 | local tmp = pathfile 349 | if settings.filename_replace and not media_title then 350 | for k,v in ipairs(settings.filename_replace) do 351 | if ( v['ext'] and (v['ext'][ext] or (ext and not protocol and v['ext']['all'])) ) 352 | or ( v['protocol'] and (v['protocol'][protocol] or (protocol and not ext and v['protocol']['all'])) ) then 353 | for ruleindex, indexrules in ipairs(v['rules']) do 354 | for rule, override in pairs(indexrules) do 355 | tmp = tmp:gsub(rule, override) 356 | end 357 | end 358 | end 359 | end 360 | end 361 | if settings.slice_longfilenames and tmp:len()>settings.slice_longfilenames_amount+5 then 362 | tmp = tmp:sub(1, settings.slice_longfilenames_amount).." ..." 363 | end 364 | return tmp 365 | end 366 | 367 | --gets a nicename of playlist entry at 0-based position i 368 | function get_name_from_index(i, notitle) 369 | refresh_globals() 370 | if plen <= i then msg.error("no index in playlist", i, "length", plen); return nil end 371 | local _, name = nil 372 | local title = mp.get_property('playlist/'..i..'/title') 373 | local name = mp.get_property('playlist/'..i..'/filename') 374 | 375 | local should_use_title = settings.prefer_titles == 'all' or name:match('^https?://') and settings.prefer_titles == 'url' 376 | --check if file has a media title stored or as property 377 | if not title and should_use_title then 378 | local mtitle = mp.get_property('media-title') 379 | if i == pos and mp.get_property('filename') ~= mtitle then 380 | if not url_table[name] then 381 | url_table[name] = mtitle 382 | end 383 | title = mtitle 384 | elseif url_table[name] then 385 | title = url_table[name] 386 | end 387 | end 388 | 389 | --if we have media title use a more conservative strip 390 | if title and not notitle and should_use_title then return stripfilename(title, true) end 391 | 392 | --remove paths if they exist, keeping protocols for stripping 393 | if string.sub(name, 1, 1) == '/' or name:match("^%a:[/\\]") then 394 | _, name = utils.split_path(name) 395 | end 396 | return stripfilename(name) 397 | end 398 | 399 | function parse_header(string) 400 | local esc_title = stripfilename(mp.get_property("media-title"), true):gsub("%%", "%%%%") 401 | local esc_file = stripfilename(mp.get_property("filename")):gsub("%%", "%%%%") 402 | return string:gsub("%%N", "\\N") 403 | :gsub("%%pos", mp.get_property_number("playlist-pos",0)+1) 404 | :gsub("%%plen", mp.get_property("playlist-count")) 405 | :gsub("%%cursor", cursor+1) 406 | :gsub("%%mediatitle", esc_title) 407 | :gsub("%%filename", esc_file) 408 | -- undo name escape 409 | :gsub("%%%%", "%%") 410 | end 411 | 412 | function parse_filename(string, name, index) 413 | local base = tostring(plen):len() 414 | local esc_name = stripfilename(name):gsub("%%", "%%%%") 415 | return string:gsub("%%N", "\\N") 416 | :gsub("%%pos", string.format("%0"..base.."d", index+1)) 417 | :gsub("%%name", esc_name) 418 | -- undo name escape 419 | :gsub("%%%%", "%%") 420 | end 421 | 422 | function parse_filename_by_index(index) 423 | local template = settings.normal_file 424 | 425 | local is_idle = mp.get_property_native('idle-active') 426 | local position = is_idle and -1 or pos 427 | 428 | if index == position then 429 | if index == cursor then 430 | if selection then 431 | template = settings.playing_selected_file 432 | else 433 | template = settings.playing_hovered_file 434 | end 435 | else 436 | template = settings.playing_file 437 | end 438 | elseif index == cursor then 439 | if selection then 440 | template = settings.selected_file 441 | else 442 | template = settings.hovered_file 443 | end 444 | end 445 | 446 | return parse_filename(template, get_name_from_index(index), index) 447 | end 448 | 449 | 450 | function draw_playlist() 451 | refresh_globals() 452 | local ass = assdraw.ass_new() 453 | ass:pos(settings.text_padding_x, settings.text_padding_y) 454 | ass:new_event() 455 | ass:append(settings.style_ass_tags) 456 | 457 | if settings.playlist_header ~= "" then 458 | ass:append(parse_header(settings.playlist_header).."\\N") 459 | end 460 | local start = cursor - math.floor(settings.showamount/2) 461 | local showall = false 462 | local showrest = false 463 | if start<0 then start=0 end 464 | if plen <= settings.showamount then 465 | start=0 466 | showall=true 467 | end 468 | if start > math.max(plen-settings.showamount-1, 0) then 469 | start=plen-settings.showamount 470 | showrest=true 471 | end 472 | if start > 0 and not showall then ass:append(settings.playlist_sliced_prefix.."\\N") end 473 | for index=start,start+settings.showamount-1,1 do 474 | if index == plen then break end 475 | 476 | ass:append(parse_filename_by_index(index).."\\N") 477 | if index == start+settings.showamount-1 and not showall and not showrest then 478 | ass:append(settings.playlist_sliced_suffix) 479 | end 480 | end 481 | local w, h = mp.get_osd_size() 482 | if settings.scale_playlist_by_window then w,h = 0, 0 end 483 | mp.set_osd_ass(w, h, ass.text) 484 | end 485 | 486 | function toggle_playlist() 487 | if settings.open_toggles then 488 | if playlist_visible then 489 | remove_keybinds() 490 | return 491 | end 492 | end 493 | showplaylist() 494 | end 495 | 496 | function showplaylist(duration) 497 | refresh_globals() 498 | if plen == 0 then return end 499 | playlist_visible = true 500 | add_keybinds() 501 | 502 | draw_playlist() 503 | keybindstimer:kill() 504 | if duration then 505 | keybindstimer = mp.add_periodic_timer(duration, remove_keybinds) 506 | else 507 | keybindstimer:resume() 508 | end 509 | end 510 | 511 | selection=nil 512 | function selectfile() 513 | refresh_globals() 514 | if plen == 0 then return end 515 | if not selection then 516 | selection=cursor 517 | else 518 | selection=nil 519 | end 520 | showplaylist() 521 | end 522 | 523 | function unselectfile() 524 | selection=nil 525 | showplaylist() 526 | end 527 | 528 | function resetcursor() 529 | cursor = mp.get_property_number('playlist-pos', 1) 530 | end 531 | 532 | function removefile() 533 | refresh_globals() 534 | if plen == 0 then return end 535 | selection = nil 536 | if cursor==pos then mp.command("script-message unseenplaylist mark true \"playlistmanager avoid conflict when removing file\"") end 537 | mp.commandv("playlist-remove", cursor) 538 | if cursor==plen-1 then cursor = cursor - 1 end 539 | showplaylist() 540 | end 541 | 542 | function moveup() 543 | refresh_globals() 544 | if plen == 0 then return end 545 | if cursor~=0 then 546 | if selection then mp.commandv("playlist-move", cursor,cursor-1) end 547 | cursor = cursor-1 548 | elseif settings.loop_cursor then 549 | if selection then mp.commandv("playlist-move", cursor,plen) end 550 | cursor = plen-1 551 | end 552 | showplaylist() 553 | end 554 | 555 | function movedown() 556 | refresh_globals() 557 | if plen == 0 then return end 558 | if cursor ~= plen-1 then 559 | if selection then mp.commandv("playlist-move", cursor,cursor+2) end 560 | cursor = cursor + 1 561 | elseif settings.loop_cursor then 562 | if selection then mp.commandv("playlist-move", cursor,0) end 563 | cursor = 0 564 | end 565 | showplaylist() 566 | end 567 | 568 | function movepageup() 569 | refresh_globals() 570 | if plen == 0 or cursor == 0 then return end 571 | local prev_cursor = cursor 572 | cursor = cursor - settings.showamount 573 | if cursor < 0 then cursor = 0 end 574 | if selection then mp.commandv("playlist-move", prev_cursor, cursor) end 575 | showplaylist() 576 | end 577 | 578 | function movepagedown() 579 | refresh_globals() 580 | if plen == 0 or cursor == plen-1 then return end 581 | local prev_cursor = cursor 582 | cursor = cursor + settings.showamount 583 | if cursor >= plen then cursor = plen-1 end 584 | if selection then mp.commandv("playlist-move", prev_cursor, cursor+1) end 585 | showplaylist() 586 | end 587 | 588 | function movebegin() 589 | refresh_globals() 590 | if plen == 0 or cursor == 0 then return end 591 | local prev_cursor = cursor 592 | cursor = 0 593 | if selection then mp.commandv("playlist-move", prev_cursor, cursor) end 594 | showplaylist() 595 | end 596 | 597 | function moveend() 598 | refresh_globals() 599 | if plen == 0 or cursor == plen-1 then return end 600 | local prev_cursor = cursor 601 | cursor = plen-1 602 | if selection then mp.commandv("playlist-move", prev_cursor, cursor+1) end 603 | showplaylist() 604 | end 605 | 606 | function write_watch_later(force_write) 607 | if mp.get_property_bool("save-position-on-quit") or force_write then 608 | mp.command("write-watch-later-config") 609 | end 610 | end 611 | 612 | function playlist_next(force_write) 613 | write_watch_later(force_write) 614 | mp.commandv("playlist-next", "weak") 615 | end 616 | 617 | function playlist_prev(force_write) 618 | write_watch_later(force_write) 619 | mp.commandv("playlist-prev", "weak") 620 | end 621 | 622 | function playfile() 623 | refresh_globals() 624 | if plen == 0 then return end 625 | selection = nil 626 | local is_idle = mp.get_property_native('idle-active') 627 | if cursor ~= pos or is_idle then 628 | write_watch_later() 629 | mp.set_property("playlist-pos", cursor) 630 | else 631 | if cursor~=plen-1 then 632 | cursor = cursor + 1 633 | end 634 | write_watch_later() 635 | mp.commandv("playlist-next", "weak") 636 | end 637 | if settings.show_playlist_on_fileload ~= 2 then 638 | remove_keybinds() 639 | end 640 | end 641 | 642 | function get_files_windows(dir) 643 | local args = { 644 | 'powershell', '-NoProfile', '-Command', [[& { 645 | Trap { 646 | Write-Error -ErrorRecord $_ 647 | Exit 1 648 | } 649 | $path = "]]..dir..[[" 650 | $escapedPath = [WildcardPattern]::Escape($path) 651 | cd $escapedPath 652 | 653 | $list = (Get-ChildItem -File | Sort-Object { [regex]::Replace($_.Name, '\d+', { $args[0].Value.PadLeft(20) }) }).Name 654 | $string = ($list -join "/") 655 | $u8list = [System.Text.Encoding]::UTF8.GetBytes($string) 656 | [Console]::OpenStandardOutput().Write($u8list, 0, $u8list.Length) 657 | }]] 658 | } 659 | local process = utils.subprocess({ args = args, cancellable = false }) 660 | return parse_files(process, '%/') 661 | end 662 | 663 | function get_files_linux(dir) 664 | local args = { 'ls', '-1pv', dir } 665 | local process = utils.subprocess({ args = args, cancellable = false }) 666 | return parse_files(process, '\n') 667 | end 668 | 669 | function parse_files(res, delimiter) 670 | if not res.error and res.status == 0 then 671 | local valid_files = {} 672 | for line in res.stdout:gmatch("[^"..delimiter.."]+") do 673 | local ext = line:match("^.+%.(.+)$") 674 | if ext and filetype_lookup[ext:lower()] then 675 | table.insert(valid_files, line) 676 | end 677 | end 678 | return valid_files, nil 679 | else 680 | return nil, res.error 681 | end 682 | end 683 | 684 | function get_playlist_filenames_set() 685 | local filenames = {} 686 | for n=0,plen-1,1 do 687 | local filename = mp.get_property('playlist/'..n..'/filename') 688 | local _, file = utils.split_path(filename) 689 | filenames[file] = true 690 | end 691 | return filenames 692 | end 693 | 694 | --Creates a playlist of all files in directory, will keep the order and position 695 | --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 696 | function playlist(force_dir) 697 | refresh_globals() 698 | if not directory and plen > 0 then return end 699 | local hasfile = true 700 | if plen == 0 then 701 | hasfile = false 702 | dir = mp.get_property('working-directory') 703 | else 704 | dir = directory 705 | end 706 | if force_dir then dir = force_dir end 707 | 708 | local files, error 709 | if settings.system == "linux" then 710 | files, error = get_files_linux(dir) 711 | else 712 | files, error = get_files_windows(dir) 713 | end 714 | 715 | local filenames = get_playlist_filenames_set() 716 | local c, c2 = 0,0 717 | if files then 718 | local cur = false 719 | local filename = mp.get_property("filename") 720 | for _, file in ipairs(files) do 721 | local appendstr = "append" 722 | if not hasfile then 723 | cur = true 724 | appendstr = "append-play" 725 | hasfile = true 726 | end 727 | if filename == file then 728 | cur = true 729 | elseif filenames[file] then 730 | -- skip files already in playlist 731 | elseif cur == true or settings.loadfiles_always_append then 732 | mp.commandv("loadfile", utils.join_path(dir, file), appendstr) 733 | msg.info("Appended to playlist: " .. file) 734 | c2 = c2 + 1 735 | else 736 | mp.commandv("loadfile", utils.join_path(dir, file), appendstr) 737 | msg.info("Prepended to playlist: " .. file) 738 | mp.commandv("playlist-move", mp.get_property_number("playlist-count", 1)-1, c) 739 | c = c + 1 740 | end 741 | end 742 | if c2 > 0 or c>0 then 743 | mp.osd_message("Added "..c + c2.." files to playlist") 744 | else 745 | mp.osd_message("No additional files found") 746 | end 747 | cursor = mp.get_property_number('playlist-pos', 1) 748 | else 749 | msg.error("Could not scan for files: "..(error or "")) 750 | end 751 | if sort_watching then 752 | msg.info("Ignoring directory structure and using playlist sort") 753 | sortplaylist() 754 | end 755 | refresh_globals() 756 | if playlist_visible then showplaylist() end 757 | return c + c2 758 | end 759 | 760 | function parse_home(path) 761 | if not path:find("^~") then 762 | return path 763 | end 764 | local home_dir = os.getenv("HOME") or os.getenv("USERPROFILE") 765 | if not home_dir then 766 | local drive = os.getenv("HOMEDRIVE") 767 | local path = os.getenv("HOMEPATH") 768 | if drive and path then 769 | home_dir = utils.join_path(drive, path) 770 | else 771 | msg.error("Couldn't find home dir.") 772 | return nil 773 | end 774 | end 775 | local result = path:gsub("^~", home_dir) 776 | return result 777 | end 778 | 779 | local interactive_save = false 780 | function activate_playlist_save() 781 | if interactive_save then 782 | remove_keybinds() 783 | mp.command("script-message playlistmanager-save-interactive \"start interactive filenaming process\"") 784 | else 785 | save_playlist() 786 | end 787 | end 788 | 789 | --saves the current playlist into a m3u file 790 | function save_playlist(filename) 791 | local length = mp.get_property_number('playlist-count', 0) 792 | if length == 0 then return end 793 | 794 | --get playlist save path 795 | local savepath 796 | if settings.playlist_savepath == nil or settings.playlist_savepath == "" then 797 | savepath = mp.command_native({"expand-path", "~~home/"}).."/playlists" 798 | else 799 | savepath = parse_home(settings.playlist_savepath) 800 | if savepath == nil then return end 801 | end 802 | 803 | --create savepath if it doesn't exist 804 | if utils.readdir(savepath) == nil then 805 | local windows_args = {'powershell', '-NoProfile', '-Command', 'mkdir', savepath} 806 | local unix_args = { 'mkdir', savepath } 807 | local args = settings.system == 'windows' and windows_args or unix_args 808 | local res = utils.subprocess({ args = args, cancellable = false }) 809 | if res.status ~= 0 then 810 | msg.error("Failed to create playlist save directory "..savepath..". Error: "..(res.error or "unknown")) 811 | return 812 | end 813 | end 814 | 815 | local date = os.date("*t") 816 | local datestring = ("%02d-%02d-%02d_%02d-%02d-%02d"):format(date.year, date.month, date.day, date.hour, date.min, date.sec) 817 | 818 | local name = filename or datestring.."_playlist-size_"..length..".m3u" 819 | 820 | local savepath = utils.join_path(savepath, name) 821 | local file, err = io.open(savepath, "w") 822 | if not file then 823 | msg.error("Error in creating playlist file, check permissions. Error: "..(err or "unknown")) 824 | else 825 | local i=0 826 | while i < length do 827 | local pwd = mp.get_property("working-directory") 828 | local filename = mp.get_property('playlist/'..i..'/filename') 829 | local fullpath = filename 830 | if not filename:match("^%a%a+:%/%/") then 831 | fullpath = utils.join_path(pwd, filename) 832 | end 833 | local title = mp.get_property('playlist/'..i..'/title') or url_table[filename] 834 | if title then 835 | file:write("#EXTINF:,"..title.."\n") 836 | end 837 | file:write(fullpath, "\n") 838 | i=i+1 839 | end 840 | local saved_msg = "Playlist written to: "..savepath 841 | if settings.display_osd_feedback then mp.osd_message(saved_msg) end 842 | msg.info(saved_msg) 843 | file:close() 844 | end 845 | end 846 | 847 | function alphanumsort(a, b) 848 | local function padnum(d) 849 | local dec, n = string.match(d, "(%.?)0*(.+)") 850 | return #dec > 0 and ("%.12f"):format(d) or ("%s%03d%s"):format(dec, #n, n) 851 | end 852 | return tostring(a):lower():gsub("%.?%d+",padnum)..("%3d"):format(#b) 853 | < tostring(b):lower():gsub("%.?%d+",padnum)..("%3d"):format(#a) 854 | end 855 | 856 | function dosort(a,b) 857 | if settings.alphanumsort then 858 | return alphanumsort(a,b) 859 | else 860 | return a < b 861 | end 862 | end 863 | 864 | -- fast sort algo from https://github.com/zsugabubus/dotfiles/blob/master/.config/mpv/scripts/playlist-filtersort.lua 865 | function sortplaylist(startover) 866 | local playlist = mp.get_property_native('playlist') 867 | if #playlist < 2 then return end 868 | 869 | local order = {} 870 | for i=1, #playlist do 871 | order[i] = i 872 | playlist[i].string = get_name_from_index(i - 1, true) 873 | end 874 | 875 | table.sort(order, function(a, b) 876 | return dosort(playlist[a].string, playlist[b].string) 877 | end) 878 | 879 | for i=1, #playlist do 880 | playlist[order[i]].new_pos = i 881 | end 882 | 883 | for i=1, #playlist do 884 | while true do 885 | local j = playlist[i].new_pos 886 | if i == j then 887 | break 888 | end 889 | mp.commandv('playlist-move', (i) - 1, (j + 1) - 1) 890 | mp.commandv('playlist-move', (j - 1) - 1, (i) - 1) 891 | playlist[j], playlist[i] = playlist[i], playlist[j] 892 | end 893 | end 894 | cursor = mp.get_property_number('playlist-pos', 0) 895 | if startover then 896 | mp.set_property('playlist-pos', 0) 897 | end 898 | if playlist_visible then showplaylist() end 899 | end 900 | 901 | function autosort(name, param) 902 | if param == 0 then return end 903 | if plen < param then 904 | msg.info("Playlistmanager autosorting playlist") 905 | refresh_globals() 906 | sortplaylist() 907 | end 908 | end 909 | 910 | function reverseplaylist() 911 | local length = mp.get_property_number('playlist-count', 0) 912 | if length < 2 then return end 913 | for outer=1, length-1, 1 do 914 | mp.commandv('playlist-move', outer, 0) 915 | end 916 | if playlist_visible then 917 | showplaylist() 918 | elseif settings.display_osd_feedback then 919 | mp.osd_message("Playlist reversed") 920 | end 921 | end 922 | 923 | function shuffleplaylist() 924 | refresh_globals() 925 | if plen < 2 then return end 926 | mp.command("playlist-shuffle") 927 | math.randomseed(os.time()) 928 | mp.commandv("playlist-move", pos, math.random(0, plen-1)) 929 | mp.set_property('playlist-pos', 0) 930 | refresh_globals() 931 | if playlist_visible then 932 | showplaylist() 933 | elseif settings.display_osd_feedback then 934 | mp.osd_message("Playlist shuffled") 935 | end 936 | end 937 | 938 | function bind_keys(keys, name, func, opts) 939 | if not keys then 940 | mp.add_forced_key_binding(keys, name, func, opts) 941 | return 942 | end 943 | local i = 1 944 | for key in keys:gmatch("[^%s]+") do 945 | local prefix = i == 1 and '' or i 946 | mp.add_forced_key_binding(key, name..prefix, func, opts) 947 | i = i + 1 948 | end 949 | end 950 | 951 | function unbind_keys(keys, name) 952 | if not keys then 953 | mp.remove_key_binding(name) 954 | return 955 | end 956 | local i = 1 957 | for key in keys:gmatch("[^%s]+") do 958 | local prefix = i == 1 and '' or i 959 | mp.remove_key_binding(name..prefix) 960 | i = i + 1 961 | end 962 | end 963 | 964 | function add_keybinds() 965 | bind_keys(settings.key_moveup, 'moveup', moveup, "repeatable") 966 | bind_keys(settings.key_movedown, 'movedown', movedown, "repeatable") 967 | bind_keys(settings.key_movepageup, 'movepageup', movepageup, "repeatable") 968 | bind_keys(settings.key_movepagedown, 'movepagedown', movepagedown, "repeatable") 969 | bind_keys(settings.key_movebegin, 'movebegin', movebegin, "repeatable") 970 | bind_keys(settings.key_moveend, 'moveend', moveend, "repeatable") 971 | bind_keys(settings.key_selectfile, 'selectfile', selectfile) 972 | bind_keys(settings.key_unselectfile, 'unselectfile', unselectfile) 973 | bind_keys(settings.key_playfile, 'playfile', playfile) 974 | bind_keys(settings.key_removefile, 'removefile', removefile, "repeatable") 975 | bind_keys(settings.key_closeplaylist, 'closeplaylist', remove_keybinds) 976 | end 977 | 978 | function remove_keybinds() 979 | keybindstimer:kill() 980 | keybindstimer = mp.add_periodic_timer(settings.playlist_display_timeout, remove_keybinds) 981 | keybindstimer:kill() 982 | mp.set_osd_ass(0, 0, "") 983 | playlist_visible = false 984 | if settings.reset_cursor_on_close then 985 | resetcursor() 986 | end 987 | if settings.dynamic_binds then 988 | unbind_keys(settings.key_moveup, 'moveup') 989 | unbind_keys(settings.key_movedown, 'movedown') 990 | unbind_keys(settings.key_movepageup, 'movepageup') 991 | unbind_keys(settings.key_movepagedown, 'movepagedown') 992 | unbind_keys(settings.key_movebegin, 'movebegin') 993 | unbind_keys(settings.key_moveend, 'moveend') 994 | unbind_keys(settings.key_selectfile, 'selectfile') 995 | unbind_keys(settings.key_unselectfile, 'unselectfile') 996 | unbind_keys(settings.key_playfile, 'playfile') 997 | unbind_keys(settings.key_removefile, 'removefile') 998 | unbind_keys(settings.key_closeplaylist, 'closeplaylist') 999 | end 1000 | end 1001 | 1002 | keybindstimer = mp.add_periodic_timer(settings.playlist_display_timeout, remove_keybinds) 1003 | keybindstimer:kill() 1004 | 1005 | if not settings.dynamic_binds then 1006 | add_keybinds() 1007 | end 1008 | 1009 | if settings.loadfiles_on_idle_start and mp.get_property_number('playlist-count', 0) == 0 then 1010 | playlist() 1011 | end 1012 | 1013 | promised_sort_watch = false 1014 | if settings.sortplaylist_on_file_add then 1015 | promised_sort_watch = true 1016 | end 1017 | 1018 | promised_sort = false 1019 | if settings.sortplaylist_on_start then 1020 | promised_sort = true 1021 | end 1022 | 1023 | mp.observe_property('playlist-count', "number", function() 1024 | if playlist_visible then showplaylist() end 1025 | if settings.prefer_titles == 'none' then return end 1026 | -- resolve titles 1027 | resolve_titles() 1028 | end) 1029 | 1030 | --resolves url titles by calling youtube-dl 1031 | function resolve_titles() 1032 | if not settings.resolve_titles then return end 1033 | local length = mp.get_property_number('playlist-count', 0) 1034 | if length < 2 then return end 1035 | local i=0 1036 | -- loop all items in playlist because we can't predict how it has changed 1037 | while i < length do 1038 | local filename = mp.get_property('playlist/'..i..'/filename') 1039 | local title = mp.get_property('playlist/'..i..'/title') 1040 | if i ~= pos 1041 | and filename 1042 | and filename:match('^https?://') 1043 | and not title 1044 | and not url_table[filename] 1045 | and not requested_urls[filename] 1046 | then 1047 | requested_urls[filename] = true 1048 | 1049 | local args = { settings.youtube_dl_executable, '--no-playlist', '--flat-playlist', '-sJ', filename } 1050 | local req = mp.command_native_async( 1051 | { 1052 | name = "subprocess", 1053 | args = args, 1054 | playback_only = false, 1055 | capture_stdout = true 1056 | }, function (success, res) 1057 | if res.killed_by_us then 1058 | msg.verbose('Request to resolve url title ' .. filename .. ' timed out') 1059 | return 1060 | end 1061 | if res.status == 0 then 1062 | local json, err = utils.parse_json(res.stdout) 1063 | if not err then 1064 | local is_playlist = json['_type'] and json['_type'] == 'playlist' 1065 | local title = (is_playlist and '[playlist]: ' or '') .. json['title'] 1066 | msg.verbose(filename .. " resolved to '" .. title .. "'") 1067 | url_table[filename] = title 1068 | refresh_globals() 1069 | if playlist_visible then showplaylist() end 1070 | return 1071 | else 1072 | msg.error("Failed parsing json, reason: "..(err or "unknown")) 1073 | end 1074 | else 1075 | msg.error("Failed to resolve url title "..filename.." Error: "..(res.error or "unknown")) 1076 | end 1077 | end) 1078 | 1079 | mp.add_timeout(settings.resolve_title_timeout, function() 1080 | mp.abort_async_command(req) 1081 | end) 1082 | 1083 | end 1084 | i=i+1 1085 | end 1086 | end 1087 | 1088 | --script message handler 1089 | function handlemessage(msg, value, value2) 1090 | if msg == "show" and value == "playlist" then 1091 | if value2 ~= "toggle" then 1092 | showplaylist(value2) 1093 | return 1094 | else 1095 | toggle_playlist() 1096 | return 1097 | end 1098 | end 1099 | if msg == "show" and value == "filename" and strippedname and value2 then 1100 | mp.commandv('show-text', strippedname, tonumber(value2)*1000 ) ; return 1101 | end 1102 | if msg == "show" and value == "filename" and strippedname then 1103 | mp.commandv('show-text', strippedname ) ; return 1104 | end 1105 | if msg == "sort" then sortplaylist(value) ; return end 1106 | if msg == "shuffle" then shuffleplaylist() ; return end 1107 | if msg == "reverse" then reverseplaylist() ; return end 1108 | if msg == "loadfiles" then playlist(value) ; return end 1109 | if msg == "save" then save_playlist(value) ; return end 1110 | if msg == "playlist-next" then playlist_next(true) ; return end 1111 | if msg == "playlist-prev" then playlist_prev(true) ; return end 1112 | if msg == "enable-interactive-save" then interactive_save = true end 1113 | end 1114 | 1115 | mp.register_script_message("playlistmanager", handlemessage) 1116 | 1117 | mp.register_event("file-loaded", on_loaded) 1118 | mp.register_event("end-file", on_closed) 1119 | 1120 | mp.add_key_binding(settings.key_loadfiles, "loadfiles", playlist) 1121 | mp.add_key_binding(settings.key_sortplaylist, "sortplaylist", sortplaylist) 1122 | mp.add_key_binding(settings.key_saveplaylist, "saveplaylist", activate_playlist_save) 1123 | mp.add_key_binding(settings.key_showplaylist, "showplaylist", toggle_playlist) 1124 | mp.add_key_binding(settings.key_shuffleplaylist, "shuffleplaylist", shuffleplaylist) 1125 | mp.add_key_binding(settings.key_reverseplaylist, "reverseplaylist", reverseplaylist) 1126 | -------------------------------------------------------------------------------- /dotfiles/pandoc/.local/share/pandoc/filters/diagram-generator.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | diagram-generator – create images and figures from code blocks. 3 | 4 | This Lua filter is used to create images with or without captions 5 | from code blocks. Currently PlantUML, GraphViz, Tikz, and Python 6 | can be processed. For further details, see README.md. 7 | 8 | Copyright: © 2018-2021 John MacFarlane , 9 | 2018 Florian Schätzig , 10 | 2019 Thorsten Sommer , 11 | 2019-2021 Albert Krewinkel 12 | License: MIT – see LICENSE file for details 13 | ]] 14 | -- Module pandoc.system is required and was added in version 2.7.3 15 | PANDOC_VERSION:must_be_at_least '2.7.3' 16 | 17 | local system = require 'pandoc.system' 18 | local utils = require 'pandoc.utils' 19 | local stringify = function (s) 20 | return type(s) == 'string' and s or utils.stringify(s) 21 | end 22 | local with_temporary_directory = system.with_temporary_directory 23 | local with_working_directory = system.with_working_directory 24 | 25 | -- The PlantUML path. If set, uses the environment variable PLANTUML or the 26 | -- value "plantuml.jar" (local PlantUML version). In order to define a 27 | -- PlantUML version per pandoc document, use the meta data to define the key 28 | -- "plantuml_path". 29 | local plantuml_path = os.getenv("PLANTUML") or "plantuml.jar" 30 | 31 | -- The Inkscape path. In order to define an Inkscape version per pandoc 32 | -- document, use the meta data to define the key "inkscape_path". 33 | local inkscape_path = os.getenv("INKSCAPE") or "inkscape" 34 | 35 | -- The Python path. In order to define a Python version per pandoc document, 36 | -- use the meta data to define the key "python_path". 37 | local python_path = os.getenv("PYTHON") or "python" 38 | 39 | -- The Python environment's activate script. Can be set on a per document 40 | -- basis by using the meta data key "activatePythonPath". 41 | local python_activate_path = os.getenv("PYTHON_ACTIVATE") 42 | 43 | -- The Java path. In order to define a Java version per pandoc document, 44 | -- use the meta data to define the key "java_path". 45 | local java_path = os.getenv("JAVA_HOME") 46 | if java_path then 47 | java_path = java_path .. package.config:sub(1,1) .. "bin" 48 | .. package.config:sub(1,1) .. "java" 49 | else 50 | java_path = "java" 51 | end 52 | 53 | -- The dot (Graphviz) path. In order to define a dot version per pandoc 54 | -- document, use the meta data to define the key "dot_path". 55 | local dot_path = os.getenv("DOT") or "dot" 56 | 57 | -- The pdflatex path. In order to define a pdflatex version per pandoc 58 | -- document, use the meta data to define the key "pdflatex_path". 59 | local pdflatex_path = os.getenv("PDFLATEX") or "pdflatex" 60 | 61 | -- The asymptote path. There is also the metadata variable 62 | -- "asymptote_path". 63 | local asymptote_path = os.getenv ("ASYMPTOTE") or "asy" 64 | 65 | -- The default format is SVG i.e. vector graphics: 66 | local filetype = "svg" 67 | local mimetype = "image/svg+xml" 68 | 69 | -- Check for output formats that potentially cannot use SVG 70 | -- vector graphics. In these cases, we use a different format 71 | -- such as PNG: 72 | if FORMAT == "docx" then 73 | filetype = "png" 74 | mimetype = "image/png" 75 | elseif FORMAT == "pptx" then 76 | filetype = "png" 77 | mimetype = "image/png" 78 | elseif FORMAT == "rtf" then 79 | filetype = "png" 80 | mimetype = "image/png" 81 | end 82 | 83 | -- Execute the meta data table to determine the paths. This function 84 | -- must be called first to get the desired path. If one of these 85 | -- meta options was set, it gets used instead of the corresponding 86 | -- environment variable: 87 | function Meta(meta) 88 | plantuml_path = stringify( 89 | meta.plantuml_path or meta.plantumlPath or plantuml_path 90 | ) 91 | inkscape_path = stringify( 92 | meta.inkscape_path or meta.inkscapePath or inkscape_path 93 | ) 94 | python_path = stringify( 95 | meta.python_path or meta.pythonPath or python_path 96 | ) 97 | python_activate_path = 98 | meta.activate_python_path or meta.activatePythonPath or python_activate_path 99 | python_activate_path = python_activate_path and stringify(python_activate_path) 100 | java_path = stringify( 101 | meta.java_path or meta.javaPath or java_path 102 | ) 103 | dot_path = stringify( 104 | meta.path_dot or meta.dotPath or dot_path 105 | ) 106 | pdflatex_path = stringify( 107 | meta.pdflatex_path or meta.pdflatexPath or pdflatex_path 108 | ) 109 | asymptote_path = stringify( 110 | meta.asymptote_path or meta.asymptotePath or asymptote_path 111 | ) 112 | end 113 | 114 | -- Call plantuml.jar with some parameters (cf. PlantUML help): 115 | local function plantuml(puml, filetype) 116 | return pandoc.pipe( 117 | java_path, 118 | {"-jar", plantuml_path, "-t" .. filetype, "-pipe", "-charset", "UTF8"}, 119 | puml 120 | ) 121 | end 122 | 123 | -- Call dot (GraphViz) in order to generate the image 124 | -- (thanks @muxueqz for this code): 125 | local function graphviz(code, filetype) 126 | return pandoc.pipe(dot_path, {"-T" .. filetype}, code) 127 | end 128 | 129 | -- 130 | -- TikZ 131 | -- 132 | 133 | --- LaTeX template used to compile TikZ images. Takes additional 134 | --- packages as the first, and the actual TikZ code as the second 135 | --- argument. 136 | local tikz_template = [[ 137 | \documentclass{standalone} 138 | \usepackage{tikz} 139 | %% begin: additional packages 140 | %s 141 | %% end: additional packages 142 | \begin{document} 143 | %s 144 | \end{document} 145 | ]] 146 | 147 | -- Returns a function which takes the filename of a PDF or SVG file 148 | -- and a target filename, and writes the input as the given format. 149 | -- Returns `nil` if conversion into the target format is not possible. 150 | local function convert_with_inkscape(filetype) 151 | -- Build the basic Inkscape command for the conversion 152 | local inkscape_output_args 153 | 154 | -- Check inksape version. 155 | -- TODO: this can be removed if supporting older Inkscape is not important. 156 | local inkscape_v_string = io.popen(inkscape_path .. " --version"):read() 157 | local inkscape_v_major = inkscape_v_string:gmatch("([0-9]*)%.")() 158 | local isv1 = tonumber(inkscape_v_major) >= 1 159 | 160 | local cmd_arg = isv1 and '"%s" "%s" -o "%s" ' or '"%s" --without-gui --file="%s" ' 161 | 162 | if filetype == 'png' then 163 | local png_arg = isv1 and '--export-type=png' or '--export-png="%s"' 164 | output_args = png_arg .. '--export-dpi=300' 165 | elseif filetype == 'svg' then 166 | output_args = isv1 and '--export-type=svg --export-plain-svg' or '--export-plain-svg="%s"' 167 | else 168 | return nil 169 | end 170 | 171 | return function (pdf_file, outfile) 172 | local inkscape_command = string.format( 173 | cmd_arg .. output_args, 174 | inkscape_path, 175 | pdf_file, 176 | outfile 177 | ) 178 | local command_output = io.popen(inkscape_command) 179 | -- TODO: print output when debugging. 180 | command_output:close() 181 | end 182 | end 183 | 184 | --- Compile LaTeX with Tikz code to an image 185 | local function tikz2image(src, filetype, additional_packages) 186 | local convert = convert_with_inkscape(filetype) 187 | -- Bail if there is now known way from PDF to the target format. 188 | if not convert then 189 | error(string.format("Don't know how to convert pdf to %s.", filetype)) 190 | end 191 | return with_temporary_directory("tikz2image", function (tmpdir) 192 | return with_working_directory(tmpdir, function () 193 | -- Define file names: 194 | local file_template = "%s/tikz-image.%s" 195 | local tikz_file = file_template:format(tmpdir, "tex") 196 | local pdf_file = file_template:format(tmpdir, "pdf") 197 | local outfile = file_template:format(tmpdir, filetype) 198 | 199 | -- Build and write the LaTeX document: 200 | local f = io.open(tikz_file, 'w') 201 | f:write(tikz_template:format(additional_packages or '', src)) 202 | f:close() 203 | 204 | -- Execute the LaTeX compiler: 205 | pandoc.pipe(pdflatex_path, {'-output-directory', tmpdir, tikz_file}, '') 206 | 207 | convert(pdf_file, outfile) 208 | 209 | -- Try to open and read the image: 210 | local img_data 211 | local r = io.open(outfile, 'rb') 212 | if r then 213 | img_data = r:read("*all") 214 | r:close() 215 | else 216 | -- TODO: print warning 217 | end 218 | 219 | return img_data 220 | end) 221 | end) 222 | end 223 | 224 | -- Run Python to generate an image: 225 | local function py2image(code, filetype) 226 | 227 | -- Define the temp files: 228 | local outfile = string.format('%s.%s', os.tmpname(), filetype) 229 | local pyfile = os.tmpname() 230 | 231 | -- Replace the desired destination's file type in the Python code: 232 | local extendedCode = string.gsub(code, "%$FORMAT%$", filetype) 233 | 234 | -- Replace the desired destination's path in the Python code: 235 | extendedCode = string.gsub(extendedCode, "%$DESTINATION%$", outfile) 236 | 237 | -- Write the Python code: 238 | local f = io.open(pyfile, 'w') 239 | f:write(extendedCode) 240 | f:close() 241 | 242 | -- Execute Python in the desired environment: 243 | local pycmd = python_path .. ' ' .. pyfile 244 | local command = python_activate_path 245 | and python_activate_path .. ' && ' .. pycmd 246 | or pycmd 247 | os.execute(command) 248 | 249 | -- Try to open the written image: 250 | local r = io.open(outfile, 'rb') 251 | local imgData = nil 252 | 253 | -- When the image exist, read it: 254 | if r then 255 | imgData = r:read("*all") 256 | r:close() 257 | else 258 | io.stderr:write(string.format("File '%s' could not be opened", outfile)) 259 | error 'Could not create image from python code.' 260 | end 261 | 262 | -- Delete the tmp files: 263 | os.remove(pyfile) 264 | os.remove(outfile) 265 | 266 | return imgData 267 | end 268 | 269 | -- 270 | -- Asymptote 271 | -- 272 | 273 | local function asymptote(code, filetype) 274 | local convert 275 | if filetype ~= 'svg' and filetype ~= 'png' then 276 | error(string.format("Conversion to %s not implemented", filetype)) 277 | end 278 | return with_temporary_directory( 279 | "asymptote", 280 | function(tmpdir) 281 | return with_working_directory( 282 | tmpdir, 283 | function () 284 | local asy_file = "pandoc_diagram.asy" 285 | local svg_file = "pandoc_diagram.svg" 286 | local f = io.open(asy_file, 'w') 287 | f:write(code) 288 | f:close() 289 | 290 | pandoc.pipe(asymptote_path, {"-f", "svg", "-o", "pandoc_diagram", asy_file}, "") 291 | 292 | local r 293 | if filetype == 'svg' then 294 | r = io.open(svg_file, 'rb') 295 | else 296 | local png_file = "pandoc_diagram.png" 297 | convert_with_inkscape("png")(svg_file, png_file) 298 | r = io.open(png_file, 'rb') 299 | end 300 | 301 | local img_data 302 | if r then 303 | img_data = r:read("*all") 304 | r:close() 305 | else 306 | error("could not read asymptote result file") 307 | end 308 | return img_data 309 | end) 310 | end) 311 | end 312 | 313 | -- Executes each document's code block to find matching code blocks: 314 | function CodeBlock(block) 315 | -- Using a table with all known generators i.e. converters: 316 | local converters = { 317 | plantuml = plantuml, 318 | graphviz = graphviz, 319 | tikz = tikz2image, 320 | py2image = py2image, 321 | asymptote = asymptote, 322 | } 323 | 324 | -- Check if a converter exists for this block. If not, return the block 325 | -- unchanged. 326 | local img_converter = converters[block.classes[1]] 327 | if not img_converter then 328 | return nil 329 | end 330 | 331 | -- Call the correct converter which belongs to the used class: 332 | local success, img = pcall(img_converter, block.text, 333 | filetype, block.attributes["additionalPackages"] or nil) 334 | 335 | -- Bail if an error occured; img contains the error message when that 336 | -- happens. 337 | if not (success and img) then 338 | io.stderr:write(tostring(img or "no image data has been returned.")) 339 | io.stderr:write('\n') 340 | error 'Image conversion failed. Aborting.' 341 | end 342 | 343 | -- If we got here, then the transformation went ok and `img` contains 344 | -- the image data. 345 | 346 | -- Create figure name by hashing the image content 347 | local fname = pandoc.sha1(img) .. "." .. filetype 348 | 349 | -- Store the data in the media bag: 350 | pandoc.mediabag.insert(fname, mimetype, img) 351 | 352 | local enable_caption = nil 353 | 354 | -- If the user defines a caption, read it as Markdown. 355 | local caption = block.attributes.caption 356 | and pandoc.read(block.attributes.caption).blocks[1].content 357 | or {} 358 | 359 | -- A non-empty caption means that this image is a figure. We have to 360 | -- set the image title to "fig:" for pandoc to treat it as such. 361 | local title = #caption > 0 and "fig:" or "" 362 | 363 | -- Transfer identifier and other relevant attributes from the code 364 | -- block to the image. The `name` is kept as an attribute. 365 | -- This allows a figure block starting with: 366 | -- 367 | -- ```{#fig:example .plantuml caption="Image created by **PlantUML**."} 368 | -- 369 | -- to be referenced as @fig:example outside of the figure when used 370 | -- with `pandoc-crossref`. 371 | local img_attr = { 372 | id = block.identifier, 373 | name = block.attributes.name, 374 | width = block.attributes.width, 375 | height = block.attributes.height 376 | } 377 | 378 | -- Create a new image for the document's structure. Attach the user's 379 | -- caption. Also use a hack (fig:) to enforce pandoc to create a 380 | -- figure i.e. attach a caption to the image. 381 | local img_obj = pandoc.Image(caption, fname, title, img_attr) 382 | 383 | -- Finally, put the image inside an empty paragraph. By returning the 384 | -- resulting paragraph object, the source code block gets replaced by 385 | -- the image: 386 | return pandoc.Para{ img_obj } 387 | end 388 | 389 | -- Normally, pandoc will run the function in the built-in order Inlines -> 390 | -- Blocks -> Meta -> Pandoc. We instead want Meta -> Blocks. Thus, we must 391 | -- define our custom order: 392 | return { 393 | {Meta = Meta}, 394 | {CodeBlock = CodeBlock}, 395 | } 396 | -------------------------------------------------------------------------------- /dotfiles/pandoc/.local/share/pandoc/filters/include-code-files.lua: -------------------------------------------------------------------------------- 1 | --- include-code-files.lua – filter to include code from source files 2 | --- 3 | --- Copyright: © 2020 Bruno BEAUFILS 4 | --- License: MIT – see LICENSE file for details 5 | 6 | --- Dedent a line 7 | local function dedent (line, n) 8 | return line:sub(1,n):gsub(" ","") .. line:sub(n+1) 9 | end 10 | 11 | --- Filter function for code blocks 12 | local function transclude (cb) 13 | if cb.attributes.include then 14 | local content = "" 15 | local fh = io.open(cb.attributes.include) 16 | if not fh then 17 | io.stderr:write("Cannot open file " .. cb.attributes.include .. " | Skipping includes\n") 18 | else 19 | local number = 1 20 | local start = 1 21 | 22 | -- change hyphenated attributes to PascalCase 23 | for i,pascal in pairs({"startLine", "endLine"}) 24 | do 25 | local hyphen = pascal:gsub("%u", "-%0"):lower() 26 | if cb.attributes[hyphen] then 27 | cb.attributes[pascal] = cb.attributes[hyphen] 28 | cb.attributes[hyphen] = nil 29 | end 30 | end 31 | 32 | if cb.attributes.startLine then 33 | cb.attributes.startFrom = cb.attributes.startLine 34 | start = tonumber(cb.attributes.startLine) 35 | end 36 | for line in fh:lines ("L") 37 | do 38 | if cb.attributes.dedent then 39 | line = dedent(line, cb.attributes.dedent) 40 | end 41 | if number >= start then 42 | if not cb.attributes.endLine or number <= tonumber(cb.attributes.endLine) then 43 | content = content .. line 44 | end 45 | end 46 | number = number + 1 47 | end 48 | fh:close() 49 | end 50 | -- remove key-value pair for used keys 51 | cb.attributes.include = nil 52 | cb.attributes.startLine = nil 53 | cb.attributes.endLine = nil 54 | cb.attributes.dedent = nil 55 | -- return final code block 56 | return pandoc.CodeBlock(content, cb.attr) 57 | end 58 | end 59 | 60 | return { 61 | { CodeBlock = transclude } 62 | } 63 | -------------------------------------------------------------------------------- /dotfiles/pandoc/.local/share/pandoc/filters/include-files.lua: -------------------------------------------------------------------------------- 1 | --- include-files.lua – filter to include Markdown files 2 | --- 3 | --- Copyright: © 2019–2021 Albert Krewinkel 4 | --- License: MIT – see LICENSE file for details 5 | 6 | -- Module pandoc.path is required and was added in version 2.12 7 | PANDOC_VERSION:must_be_at_least '2.12' 8 | 9 | local List = require 'pandoc.List' 10 | local path = require 'pandoc.path' 11 | local system = require 'pandoc.system' 12 | 13 | --- Get include auto mode 14 | local include_auto = false 15 | function get_vars (meta) 16 | if meta['include-auto'] then 17 | include_auto = true 18 | end 19 | end 20 | 21 | --- Keep last heading level found 22 | local last_heading_level = 0 23 | function update_last_level(header) 24 | last_heading_level = header.level 25 | end 26 | 27 | --- Update contents of included file 28 | local function update_contents(blocks, shift_by, include_path) 29 | local update_contents_filter = { 30 | -- Shift headings in block list by given number 31 | Header = function (header) 32 | if shift_by then 33 | header.level = header.level + shift_by 34 | end 35 | return header 36 | end, 37 | -- If image paths are relative then prepend include file path 38 | Image = function (image) 39 | if path.is_relative(image.src) then 40 | image.src = path.normalize(path.join({include_path, image.src})) 41 | end 42 | return image 43 | end, 44 | -- Update path for include-code-files.lua filter style CodeBlocks 45 | CodeBlock = function (cb) 46 | if cb.attributes.include and path.is_relative(cb.attributes.include) then 47 | cb.attributes.include = 48 | path.normalize(path.join({include_path, cb.attributes.include})) 49 | end 50 | return cb 51 | end 52 | } 53 | 54 | return pandoc.walk_block(pandoc.Div(blocks), update_contents_filter).content 55 | end 56 | 57 | --- Filter function for code blocks 58 | local transclude 59 | function transclude (cb) 60 | -- ignore code blocks which are not of class "include". 61 | if not cb.classes:includes 'include' then 62 | return 63 | end 64 | 65 | -- Markdown is used if this is nil. 66 | local format = cb.attributes['format'] 67 | 68 | -- Attributes shift headings 69 | local shift_heading_level_by = 0 70 | local shift_input = cb.attributes['shift-heading-level-by'] 71 | if shift_input then 72 | shift_heading_level_by = tonumber(shift_input) 73 | else 74 | if include_auto then 75 | -- Auto shift headings 76 | shift_heading_level_by = last_heading_level 77 | end 78 | end 79 | 80 | --- keep track of level before recusion 81 | local buffer_last_heading_level = last_heading_level 82 | 83 | local blocks = List:new() 84 | for line in cb.text:gmatch('[^\n]+') do 85 | if line:sub(1,2) ~= '//' then 86 | local fh = io.open(line) 87 | if not fh then 88 | io.stderr:write("Cannot open file " .. line .. " | Skipping includes\n") 89 | else 90 | -- read file as the given format with global reader options 91 | local contents = pandoc.read( 92 | fh:read '*a', 93 | format, 94 | PANDOC_READER_OPTIONS 95 | ).blocks 96 | last_heading_level = 0 97 | -- recursive transclusion 98 | contents = system.with_working_directory( 99 | path.directory(line), 100 | function () 101 | return pandoc.walk_block( 102 | pandoc.Div(contents), 103 | { Header = update_last_level, CodeBlock = transclude } 104 | ) 105 | end).content 106 | --- reset to level before recursion 107 | last_heading_level = buffer_last_heading_level 108 | blocks:extend(update_contents(contents, shift_heading_level_by, 109 | path.directory(line))) 110 | fh:close() 111 | end 112 | end 113 | end 114 | return blocks 115 | end 116 | 117 | return { 118 | { Meta = get_vars }, 119 | { Header = update_last_level, CodeBlock = transclude } 120 | } 121 | -------------------------------------------------------------------------------- /dotfiles/pandoc/.local/share/pandoc/filters/pagebreak.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | pagebreak – convert raw LaTeX page breaks to other formats 3 | 4 | Copyright © 2017-2021 Benct Philip Jonsson, Albert Krewinkel 5 | 6 | Permission to use, copy, modify, and/or distribute this software for any 7 | purpose with or without fee is hereby granted, provided that the above 8 | copyright notice and this permission notice appear in all copies. 9 | 10 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 11 | WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 12 | MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 13 | ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 14 | WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 15 | ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 16 | OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 17 | ]] 18 | local stringify_orig = (require 'pandoc.utils').stringify 19 | 20 | local function stringify(x) 21 | return type(x) == 'string' and x or stringify_orig(x) 22 | end 23 | 24 | --- configs – these are populated in the Meta filter. 25 | local pagebreak = { 26 | asciidoc = '<<<\n\n', 27 | context = '\\page', 28 | epub = '

', 29 | html = '
', 30 | latex = '\\newpage{}', 31 | ms = '.bp', 32 | ooxml = '', 33 | odt = '' 34 | } 35 | 36 | local function pagebreaks_from_config (meta) 37 | local html_class = 38 | (meta.newpage_html_class and stringify(meta.newpage_html_class)) 39 | or os.getenv 'PANDOC_NEWPAGE_HTML_CLASS' 40 | if html_class and html_class ~= '' then 41 | pagebreak.html = string.format('
', html_class) 42 | end 43 | 44 | local odt_style = 45 | (meta.newpage_odt_style and stringify(meta.newpage_odt_style)) 46 | or os.getenv 'PANDOC_NEWPAGE_ODT_STYLE' 47 | if odt_style and odt_style ~= '' then 48 | pagebreak.odt = string.format('', odt_style) 49 | end 50 | end 51 | 52 | --- Return a block element causing a page break in the given format. 53 | local function newpage(format) 54 | if format:match 'asciidoc' then 55 | return pandoc.RawBlock('asciidoc', pagebreak.asciidoc) 56 | elseif format == 'context' then 57 | return pandoc.RawBlock('context', pagebreak.context) 58 | elseif format == 'docx' then 59 | return pandoc.RawBlock('openxml', pagebreak.ooxml) 60 | elseif format:match 'epub' then 61 | return pandoc.RawBlock('html', pagebreak.epub) 62 | elseif format:match 'html.*' then 63 | return pandoc.RawBlock('html', pagebreak.html) 64 | elseif format:match 'latex' then 65 | return pandoc.RawBlock('tex', pagebreak.latex) 66 | elseif format:match 'ms' then 67 | return pandoc.RawBlock('ms', pagebreak.ms) 68 | elseif format:match 'odt' then 69 | return pandoc.RawBlock('opendocument', pagebreak.odt) 70 | else 71 | -- fall back to insert a form feed character 72 | return pandoc.Para{pandoc.Str '\f'} 73 | end 74 | end 75 | 76 | local function is_newpage_command(command) 77 | return command:match '^\\newpage%{?%}?$' 78 | or command:match '^\\pagebreak%{?%}?$' 79 | end 80 | 81 | -- Filter function called on each RawBlock element. 82 | function RawBlock (el) 83 | -- Don't do anything if the output is TeX 84 | if FORMAT:match 'tex$' then 85 | return nil 86 | end 87 | -- check that the block is TeX or LaTeX and contains only 88 | -- \newpage or \pagebreak. 89 | if el.format:match 'tex' and is_newpage_command(el.text) then 90 | -- use format-specific pagebreak marker. FORMAT is set by pandoc to 91 | -- the targeted output format. 92 | return newpage(FORMAT) 93 | end 94 | -- otherwise, leave the block unchanged 95 | return nil 96 | end 97 | 98 | -- Turning paragraphs which contain nothing but a form feed 99 | -- characters into line breaks. 100 | function Para (el) 101 | if #el.content == 1 and el.content[1].text == '\f' then 102 | return newpage(FORMAT) 103 | end 104 | end 105 | 106 | return { 107 | {Meta = pagebreaks_from_config}, 108 | {RawBlock = RawBlock, Para = Para} 109 | } 110 | -------------------------------------------------------------------------------- /dotfiles/pandoc/.local/share/pandoc/filters/wordcount.lua: -------------------------------------------------------------------------------- 1 | -- counts words in a document 2 | 3 | words = 0 4 | characters = 0 5 | characters_and_spaces = 0 6 | process_anyway = false 7 | 8 | wordcount = { 9 | Str = function(el) 10 | -- we don't count a word if it's entirely punctuation: 11 | if el.text:match("%P") then 12 | words = words + 1 13 | end 14 | characters = characters + utf8.len(el.text) 15 | characters_and_spaces = characters_and_spaces + utf8.len(el.text) 16 | end, 17 | 18 | Space = function(el) 19 | characters_and_spaces = characters_and_spaces + 1 20 | end, 21 | 22 | Code = function(el) 23 | _,n = el.text:gsub("%S+","") 24 | words = words + n 25 | text_nospace = el.text:gsub("%s", "") 26 | characters = characters + utf8.len(text_nospace) 27 | characters_and_spaces = characters_and_spaces + utf8.len(el.text) 28 | end, 29 | 30 | CodeBlock = function(el) 31 | _,n = el.text:gsub("%S+","") 32 | words = words + n 33 | text_nospace = el.text:gsub("%s", "") 34 | characters = characters + utf8.len(text_nospace) 35 | characters_and_spaces = characters_and_spaces + utf8.len(el.text) 36 | end 37 | } 38 | 39 | -- check if the `wordcount` variable is set to `process-anyway` 40 | function Meta(meta) 41 | if meta.wordcount and (meta.wordcount=="process-anyway" 42 | or meta.wordcount=="process" or meta.wordcount=="convert") then 43 | process_anyway = true 44 | end 45 | end 46 | 47 | function Pandoc(el) 48 | -- skip metadata, just count body: 49 | pandoc.walk_block(pandoc.Div(el.blocks), wordcount) 50 | print(words .. " words in body") 51 | print(characters .. " characters in body") 52 | print(characters_and_spaces .. " characters in body (including spaces)") 53 | if not process_anyway then 54 | os.exit(0) 55 | end 56 | end 57 | -------------------------------------------------------------------------------- /dotfiles/shell/.bash_profile: -------------------------------------------------------------------------------- 1 | # .bash_profile 2 | 3 | # Get the aliases and functions 4 | if [ -f ~/.bashrc ]; then 5 | . ~/.bashrc 6 | fi 7 | 8 | # User specific environment and startup programs 9 | export EDITOR="hx" 10 | export SUDO_EDITOR="$HOME/.cargo/bin/hx" 11 | -------------------------------------------------------------------------------- /dotfiles/shell/.bashrc: -------------------------------------------------------------------------------- 1 | # .bashrc 2 | 3 | # Source global definitions 4 | if [ -f /etc/bashrc ]; then 5 | . /etc/bashrc 6 | fi 7 | 8 | # User specific environment 9 | if ! [[ "$PATH" =~ "$HOME/.local/bin:$HOME/bin:" ]]; then 10 | PATH="$HOME/.local/bin:$HOME/bin:$HOME/.deno/bin:$HOME/Documents/scripts:$HOME/adb-fastboot/platform-tools/:$PATH" 11 | fi 12 | export PATH 13 | 14 | # Aliases 15 | alias ls="ls -ltha --color --group-directories-first --hyperlink=auto" 16 | alias tree="tree -Catr --noreport --dirsfirst --filelimit 100" 17 | alias mpv="flatpak run io.mpv.Mpv" 18 | alias avif="heif-enc -A -q 85" # encode avif at almost lossless quality 19 | alias hg="kitty +kitten hyperlinked_grep" # https://sw.kovidgoyal.net/kitty/kittens/hyperlinked_grep/ 20 | 21 | # Functions 22 | clip() { xclip -sel clip -rmlastnl; } 23 | 24 | md() { 25 | filename="${1##*/}" 26 | pandoc --embed-resources --standalone "$1" -o /tmp/"$filename".html 27 | xdg-open /tmp/"$filename".html 28 | } 29 | 30 | ghpr() { GH_FORCE_TTY=100% gh pr list --limit 300 | 31 | fzf --ansi --preview 'GH_FORCE_TTY=100% gh pr view {1}' --preview-window 'down,70%' --header-lines 3 | 32 | awk '{print $1}' | 33 | xargs gh pr checkout; } 34 | 35 | wordcount() { pandoc --lua-filter wordcount.lua "$@"; } 36 | 37 | # nnn 38 | [ -n "$NNNLVL" ] && PS1="N$NNNLVL $PS1" # prompt you are within a shell that will return you to nnn 39 | export NNN_PLUG="f:fzcd;p:preview-tui;m:mtpmount" 40 | export NNN_BMS="d:~/Documents;p:~/Pictures;v:~/Videos;m:~/Music;h:~/;u:/run/media/$USERNAME;D:~/Downloads;M:${XDG_CONFIG_HOME:-$HOME/.config}/nnn/mounts" 41 | export NNN_TRASH=1 # use trash-cli: https://pypi.org/project/trash-cli/ 42 | export NNN_FIFO=/tmp/nnn.fifo 43 | 44 | # bat 45 | export BAT_THEME="Visual Studio Dark+" 46 | 47 | # fzf 48 | export FZF_DEFAULT_COMMAND='rg --files --hidden --follow --no-ignore-vcs -g "!{node_modules,.git}"' 49 | 50 | # ytfzf 51 | export video_pref="bestvideo[height<=?2160]+bestaudio/best" 52 | 53 | stty -ixon # disable terminal flow control to free ctrl-s for shortcut 54 | stty werase \^H # set ctrl-backspace to delete previous word instead of ctrl-w 55 | 56 | [ -f ~/.fzf.bash ] && source ~/.fzf.bash 57 | -------------------------------------------------------------------------------- /dotfiles/shell/.gitconfig: -------------------------------------------------------------------------------- 1 | [delta] 2 | syntax-theme = "Visual Studio Dark+" 3 | side-by-side = true 4 | 5 | [pager] 6 | diff = delta 7 | log = delta 8 | reflog = delta 9 | show = delta 10 | 11 | [commit] 12 | gpgsign = true 13 | -------------------------------------------------------------------------------- /el9-rebuilds/README.md: -------------------------------------------------------------------------------- 1 | The files in this directory were created using `rpmbuild`. As an example, here are the exact steps used to rebuild stow: 2 | 3 | ```bash 4 | sudo dnf config-manager --set-enabled crb 5 | sudo dnf install epel-release 6 | sudo dnf install perl rpm-build perl-generators perl-IO-stringy perl-Test-Output 7 | 8 | curl -O https://download-ib01.fedoraproject.org/pub/epel/8/Everything/SRPMS/Packages/s/stow-2.3.1-1.el8.src.rpm 9 | rpmbuild --rebuild stow-2.3.1-1.el8.src.rpm 10 | ``` 11 | -------------------------------------------------------------------------------- /el9-rebuilds/aiksaurus-1.2.1-48.el9.x86_64.rpm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/David-Else/developer-workstation-setup-script/22e3e1bb4f4ba2842ea0064fc0be18508762d0e7/el9-rebuilds/aiksaurus-1.2.1-48.el9.x86_64.rpm -------------------------------------------------------------------------------- /el9-rebuilds/zathura-pdf-mupdf-0.3.9-1.el9.x86_64.rpm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/David-Else/developer-workstation-setup-script/22e3e1bb4f4ba2842ea0064fc0be18508762d0e7/el9-rebuilds/zathura-pdf-mupdf-0.3.9-1.el9.x86_64.rpm -------------------------------------------------------------------------------- /extras/SymbolsNerdFontMono-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/David-Else/developer-workstation-setup-script/22e3e1bb4f4ba2842ea0064fc0be18508762d0e7/extras/SymbolsNerdFontMono-Regular.ttf -------------------------------------------------------------------------------- /extras/nasa-Q1p7bh3SHj8-unsplash.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/David-Else/developer-workstation-setup-script/22e3e1bb4f4ba2842ea0064fc0be18508762d0e7/extras/nasa-Q1p7bh3SHj8-unsplash.jpg -------------------------------------------------------------------------------- /functions.bash: -------------------------------------------------------------------------------- 1 | # shellcheck shell=bash 2 | 3 | # Adds a line of code to a file if it is not there already 4 | # ${1} file path ${2} line of code to add to file 5 | add_to_file() { 6 | touch "$1" 7 | grep -qxF "$2" "$1" && echo "$2 exists in ${GREEN}$1${RESET}" || echo "$2" >>"$1" 8 | } 9 | 10 | # Confirm the user is either normal or root and exit if they are not 11 | # ${1} normal|root 12 | confirm_user_is() { 13 | USER_STATUS=$(id -u) 14 | if [[ "$USER_STATUS" != 0 && ${1} == "root" ]]; then 15 | echo "You're not root! Run script with sudo" && exit 1 16 | elif [[ "$USER_STATUS" == 0 && ${1} == "normal" ]]; then 17 | echo "You're root! Run script as user" && exit 1 18 | fi 19 | } 20 | 21 | # Detects OS, and if valid sets a global variable OS to valid_rhel/valid_fedora 22 | detect_os() { 23 | if [[ ("$ID" == "eurolinux" || "$ID" == "centos" || "$ID" == "rocky" || "$ID" == "rhel" || "$ID" == "almalinux") && "${VERSION_ID%.*}" -gt 8 ]]; then 24 | OS='valid_rhel' 25 | elif [[ "$ID" == "fedora" && "${VERSION_ID%.*}" -gt 35 ]]; then 26 | OS='valid_fedora' 27 | else 28 | OS='invalid' 29 | fi 30 | } 31 | 32 | # Display a block of text with color ANSI escape codes 33 | display_text() { 34 | echo -e "$( 35 | cat <