about summary refs log tree commit diff stats
path: root/pkgs
diff options
context:
space:
mode:
authorBenedikt Peetz <benedikt.peetz@b-peetz.de>2024-05-20 16:10:21 +0200
committerBenedikt Peetz <benedikt.peetz@b-peetz.de>2024-05-20 16:14:26 +0200
commit368cb6b0d25db2ae23be42ad51584de059997e51 (patch)
tree3282e45d3ebced63c8498a47e83a255c35de620b /pkgs
parentrefactor(hm): Rename to `modules/home` (diff)
downloadnixos-config-368cb6b0d25db2ae23be42ad51584de059997e51.tar.gz
nixos-config-368cb6b0d25db2ae23be42ad51584de059997e51.zip
refactor(sys): Modularize and move to `modules/system` or `pkgs`
Diffstat (limited to 'pkgs')
-rw-r--r--pkgs/default.nix24
-rw-r--r--pkgs/sources/comments/.envrc4
-rw-r--r--pkgs/sources/comments/.gitignore3
-rw-r--r--pkgs/sources/comments/Cargo.lock446
-rw-r--r--pkgs/sources/comments/Cargo.toml16
-rw-r--r--pkgs/sources/comments/comments.nix25
-rw-r--r--pkgs/sources/comments/default.nix18
-rw-r--r--pkgs/sources/comments/flake.lock61
-rw-r--r--pkgs/sources/comments/flake.nix31
-rw-r--r--pkgs/sources/comments/src/info_json.rs223
-rw-r--r--pkgs/sources/comments/src/main.rs322
-rwxr-xr-xpkgs/sources/comments/update.sh8
-rw-r--r--pkgs/sources/default.nix27
-rw-r--r--pkgs/sources/generate_moz_extension/.envrc1
-rw-r--r--pkgs/sources/generate_moz_extension/.gitignore3
-rw-r--r--pkgs/sources/generate_moz_extension/Cargo.lock1275
-rw-r--r--pkgs/sources/generate_moz_extension/Cargo.toml14
-rw-r--r--pkgs/sources/generate_moz_extension/default.nix16
-rwxr-xr-xpkgs/sources/generate_moz_extension/examples/generate_extensions.sh17
-rw-r--r--pkgs/sources/generate_moz_extension/flake.lock106
-rw-r--r--pkgs/sources/generate_moz_extension/flake.nix75
-rw-r--r--pkgs/sources/generate_moz_extension/generate_firefox_extensions.nix20
-rw-r--r--pkgs/sources/generate_moz_extension/res/generate_extensions.py44
-rw-r--r--pkgs/sources/generate_moz_extension/res/reference.json30
-rw-r--r--pkgs/sources/generate_moz_extension/res/test.json30
-rw-r--r--pkgs/sources/generate_moz_extension/src/main.rs138
-rw-r--r--pkgs/sources/generate_moz_extension/src/types.rs71
-rwxr-xr-xpkgs/sources/generate_moz_extension/update.sh8
-rw-r--r--pkgs/sources/lf-make-map/.envrc11
-rw-r--r--pkgs/sources/lf-make-map/.gitignore6
-rw-r--r--pkgs/sources/lf-make-map/Cargo.lock505
-rw-r--r--pkgs/sources/lf-make-map/Cargo.toml14
-rw-r--r--pkgs/sources/lf-make-map/README.md12
-rw-r--r--pkgs/sources/lf-make-map/default.nix12
-rw-r--r--pkgs/sources/lf-make-map/flake.lock147
-rw-r--r--pkgs/sources/lf-make-map/flake.nix125
-rw-r--r--pkgs/sources/lf-make-map/lf_make_map.nix10
-rw-r--r--pkgs/sources/lf-make-map/src/cli.rs49
-rw-r--r--pkgs/sources/lf-make-map/src/main.rs229
-rw-r--r--pkgs/sources/lf-make-map/src/mapping/map_tree/display.rs91
-rw-r--r--pkgs/sources/lf-make-map/src/mapping/map_tree/iterator.rs53
-rw-r--r--pkgs/sources/lf-make-map/src/mapping/map_tree/lf_mapping.rs19
-rw-r--r--pkgs/sources/lf-make-map/src/mapping/map_tree/mod.rs402
-rw-r--r--pkgs/sources/lf-make-map/src/mapping/mod.rs156
-rwxr-xr-xpkgs/sources/lf-make-map/update.sh6
-rw-r--r--pkgs/sources/plgs-pkgs/README.md92
-rw-r--r--pkgs/sources/plgs-pkgs/check.nix37
-rw-r--r--pkgs/sources/plgs-pkgs/default.nix15
-rw-r--r--pkgs/sources/plgs-pkgs/overrides.nix34
-rw-r--r--pkgs/sources/plgs-pkgs/plugins/.plugins.json7
-rw-r--r--pkgs/sources/plgs-pkgs/plugins/blacklist.txt1
-rw-r--r--pkgs/sources/plgs-pkgs/plugins/default.nix55
-rw-r--r--pkgs/sources/plgs-pkgs/plugins/manifest.txt3
-rw-r--r--pkgs/sources/plgs-pkgs/plugins/plugins.md7
-rw-r--r--pkgs/sources/plgs-pkgs/plugins/whitelist.txt0
-rwxr-xr-xpkgs/sources/plgs-pkgs/update.sh27
-rw-r--r--pkgs/sources/scripts/default.nix412
-rwxr-xr-xpkgs/sources/scripts/source/apps/aumo.sh28
-rwxr-xr-xpkgs/sources/scripts/source/apps/con2pdf.sh234
-rw-r--r--pkgs/sources/scripts/source/apps/fupdate.1.md70
-rwxr-xr-xpkgs/sources/scripts/source/apps/fupdate.sh197
-rwxr-xr-xpkgs/sources/scripts/source/apps/git-edit-index.sh98
-rwxr-xr-xpkgs/sources/scripts/source/small_functions/brightness.sh80
-rwxr-xr-xpkgs/sources/scripts/source/small_functions/nato.py106
-rwxr-xr-xpkgs/sources/scripts/source/small_functions/screenshot_persistent.sh22
-rwxr-xr-xpkgs/sources/scripts/source/small_functions/screenshot_temporary.sh8
-rwxr-xr-xpkgs/sources/scripts/source/small_functions/update-sys.sh85
-rwxr-xr-xpkgs/sources/scripts/source/specific/neorg/neorg_id_function.sh16
-rwxr-xr-xpkgs/sources/scripts/source/specific/neorg/sh/functions/add.sh23
-rwxr-xr-xpkgs/sources/scripts/source/specific/neorg/sh/functions/context.sh43
-rwxr-xr-xpkgs/sources/scripts/source/specific/neorg/sh/functions/dmenu.sh14
-rwxr-xr-xpkgs/sources/scripts/source/specific/neorg/sh/functions/f_start.sh7
-rwxr-xr-xpkgs/sources/scripts/source/specific/neorg/sh/functions/f_stop.sh7
-rwxr-xr-xpkgs/sources/scripts/source/specific/neorg/sh/functions/list.sh8
-rwxr-xr-xpkgs/sources/scripts/source/specific/neorg/sh/functions/project.sh41
-rwxr-xr-xpkgs/sources/scripts/source/specific/neorg/sh/functions/review.sh12
-rwxr-xr-xpkgs/sources/scripts/source/specific/neorg/sh/functions/utils.sh40
-rwxr-xr-xpkgs/sources/scripts/source/specific/neorg/sh/functions/workspace.sh9
-rwxr-xr-xpkgs/sources/scripts/source/specific/neorg/sh/main.sh164
-rwxr-xr-xpkgs/sources/scripts/source/specific/spodi/sh/download.sh58
-rwxr-xr-xpkgs/sources/scripts/source/specific/spodi/sh/update.sh52
-rwxr-xr-xpkgs/sources/scripts/source/specific/spodi/spodi.sh71
-rwxr-xr-xpkgs/sources/scripts/source/specific/ytcc/description.sh8
-rwxr-xr-xpkgs/sources/scripts/source/wrappers/battery.sh11
-rwxr-xr-xpkgs/sources/scripts/source/wrappers/hibernate.sh15
-rwxr-xr-xpkgs/sources/scripts/source/wrappers/ll.sh14
-rwxr-xr-xpkgs/sources/scripts/source/wrappers/lock.sh18
-rwxr-xr-xpkgs/sources/scripts/source/wrappers/lyrics.sh11
-rwxr-xr-xpkgs/sources/scripts/source/wrappers/mpc-fav.sh16
-rwxr-xr-xpkgs/sources/scripts/source/wrappers/mpc-rm.sh10
-rwxr-xr-xpkgs/sources/scripts/source/wrappers/mpc.sh20
-rwxr-xr-xpkgs/sources/scripts/source/wrappers/show.sh9
-rwxr-xr-xpkgs/sources/scripts/source/wrappers/sort_song.sh34
-rwxr-xr-xpkgs/sources/scripts/source/wrappers/virsh-del.sh10
-rwxr-xr-xpkgs/sources/scripts/source/wrappers/yti.sh33
-rw-r--r--pkgs/sources/snap-sync-forked/default.nix24
-rwxr-xr-xpkgs/sources/snap-sync-forked/snap-sync-forked.sh534
-rw-r--r--pkgs/sources/tree-sitter-yts/.editorconfig21
-rw-r--r--pkgs/sources/tree-sitter-yts/.envrc1
-rw-r--r--pkgs/sources/tree-sitter-yts/.gitignore3
-rw-r--r--pkgs/sources/tree-sitter-yts/Cargo.toml21
-rw-r--r--pkgs/sources/tree-sitter-yts/binding.gyp19
-rw-r--r--pkgs/sources/tree-sitter-yts/bindings/node/binding.cc33
-rw-r--r--pkgs/sources/tree-sitter-yts/bindings/node/index.js19
-rw-r--r--pkgs/sources/tree-sitter-yts/bindings/rust/build.rs40
-rw-r--r--pkgs/sources/tree-sitter-yts/bindings/rust/lib.rs52
-rw-r--r--pkgs/sources/tree-sitter-yts/corpus/comments.txt51
-rw-r--r--pkgs/sources/tree-sitter-yts/corpus/comments_correct.txt27
-rw-r--r--pkgs/sources/tree-sitter-yts/corpus/duration.txt84
-rw-r--r--pkgs/sources/tree-sitter-yts/corpus/url.txt84
-rw-r--r--pkgs/sources/tree-sitter-yts/default.nix11
-rw-r--r--pkgs/sources/tree-sitter-yts/flake.lock97
-rw-r--r--pkgs/sources/tree-sitter-yts/flake.nix82
-rw-r--r--pkgs/sources/tree-sitter-yts/grammar.js26
-rw-r--r--pkgs/sources/tree-sitter-yts/highlight.yts4
-rw-r--r--pkgs/sources/tree-sitter-yts/package.json31
-rw-r--r--pkgs/sources/tree-sitter-yts/package.nix63
-rw-r--r--pkgs/sources/tree-sitter-yts/queries/highlights.scm11
-rw-r--r--pkgs/sources/tree-sitter-yts/src/grammar.json238
-rw-r--r--pkgs/sources/tree-sitter-yts/src/node-types.json200
-rw-r--r--pkgs/sources/tree-sitter-yts/src/parser.c1108
-rw-r--r--pkgs/sources/tree-sitter-yts/src/tree_sitter/parser.h241
-rw-r--r--pkgs/sources/tree-sitter-yts/treefmt.toml35
-rwxr-xr-xpkgs/sources/update_pkgs.sh25
-rw-r--r--pkgs/sources/update_vim_plugins/.envrc1
-rwxr-xr-xpkgs/sources/update_vim_plugins/check-duplicates.sh43
-rw-r--r--pkgs/sources/update_vim_plugins/default.nix17
-rw-r--r--pkgs/sources/update_vim_plugins/flake.lock61
-rw-r--r--pkgs/sources/update_vim_plugins/flake.nix24
-rw-r--r--pkgs/sources/update_vim_plugins/package.nix47
-rw-r--r--pkgs/sources/update_vim_plugins/poetry.lock680
-rw-r--r--pkgs/sources/update_vim_plugins/pyproject.toml45
-rwxr-xr-xpkgs/sources/update_vim_plugins/update.sh5
-rw-r--r--pkgs/sources/update_vim_plugins/update_vim_plugins/__init__.py0
-rw-r--r--pkgs/sources/update_vim_plugins/update_vim_plugins/__main__.py15
-rw-r--r--pkgs/sources/update_vim_plugins/update_vim_plugins/cleanup.py100
-rw-r--r--pkgs/sources/update_vim_plugins/update_vim_plugins/helpers.py61
-rw-r--r--pkgs/sources/update_vim_plugins/update_vim_plugins/nix.py121
-rw-r--r--pkgs/sources/update_vim_plugins/update_vim_plugins/plugin.py182
-rw-r--r--pkgs/sources/update_vim_plugins/update_vim_plugins/spec.py143
-rw-r--r--pkgs/sources/update_vim_plugins/update_vim_plugins/tests/__init__.py0
-rw-r--r--pkgs/sources/update_vim_plugins/update_vim_plugins/tests/fixtures.py44
-rw-r--r--pkgs/sources/update_vim_plugins/update_vim_plugins/tests/test_nix.py32
-rw-r--r--pkgs/sources/update_vim_plugins/update_vim_plugins/tests/test_plugin.py144
-rw-r--r--pkgs/sources/update_vim_plugins/update_vim_plugins/tests/test_spec.py136
-rw-r--r--pkgs/sources/update_vim_plugins/update_vim_plugins/update.py212
-rwxr-xr-xpkgs/sources/yt/.env3
-rw-r--r--pkgs/sources/yt/.envrc1
-rw-r--r--pkgs/sources/yt/.gitignore3
-rw-r--r--pkgs/sources/yt/Cargo.lock640
-rw-r--r--pkgs/sources/yt/Cargo.toml35
-rw-r--r--pkgs/sources/yt/default.nix51
-rw-r--r--pkgs/sources/yt/flake.lock61
-rw-r--r--pkgs/sources/yt/flake.nix30
-rw-r--r--pkgs/sources/yt/src/bin/yt/main.rs91
-rw-r--r--pkgs/sources/yt/src/bin/ytc/args.rs26
-rw-r--r--pkgs/sources/yt/src/bin/ytc/main.rs77
-rw-r--r--pkgs/sources/yt/src/bin/yts/args.rs41
-rw-r--r--pkgs/sources/yt/src/bin/yts/main.rs91
-rw-r--r--pkgs/sources/yt/src/constants.rs51
-rw-r--r--pkgs/sources/yt/src/downloader.rs212
-rw-r--r--pkgs/sources/yt/src/help.str8
-rw-r--r--pkgs/sources/yt/src/lib.rs185
-rw-r--r--pkgs/sources/yt/todo1
-rwxr-xr-xpkgs/sources/yt/update.sh8
-rw-r--r--pkgs/sources/yt/yt.nix29
-rw-r--r--pkgs/sources/yt/ytc.nix29
-rw-r--r--pkgs/sources/yt/yts.nix27
168 files changed, 14293 insertions, 0 deletions
diff --git a/pkgs/default.nix b/pkgs/default.nix
new file mode 100644
index 00000000..38c58a95
--- /dev/null
+++ b/pkgs/default.nix
@@ -0,0 +1,24 @@
+{
+  lib,
+  system,
+  overlays ? [],
+  sysLib,
+  homeConfig,
+  nixosConfig,
+}: let
+  additionalPackages = (import ./pkgs) {inherit homeConfig nixosConfig sysLib;};
+  complete_overlays = overlays ++ additionalPackages;
+in {
+  # TODO: inheriting system here is discouraged, localSystem or hostSystem should be inspected
+  inherit system;
+  overlays = complete_overlays;
+  config = {
+    # TODO: this fails because of the root tempsize, which should be increased
+    # contentAddressedByDefault = true;
+
+    allowUnfreePredicate = pkg:
+      builtins.elem (lib.getName pkg) [
+        "pypemicro" # required by pynitrokey
+      ];
+  };
+}
diff --git a/pkgs/sources/comments/.envrc b/pkgs/sources/comments/.envrc
new file mode 100644
index 00000000..2b5fbb29
--- /dev/null
+++ b/pkgs/sources/comments/.envrc
@@ -0,0 +1,4 @@
+use flake
+
+PATH_add ./target/debug
+PATH_add ./target/release
diff --git a/pkgs/sources/comments/.gitignore b/pkgs/sources/comments/.gitignore
new file mode 100644
index 00000000..c84fa049
--- /dev/null
+++ b/pkgs/sources/comments/.gitignore
@@ -0,0 +1,3 @@
+# build dirs
+/target
+/result
diff --git a/pkgs/sources/comments/Cargo.lock b/pkgs/sources/comments/Cargo.lock
new file mode 100644
index 00000000..54f19c46
--- /dev/null
+++ b/pkgs/sources/comments/Cargo.lock
@@ -0,0 +1,446 @@
+# This file is automatically @generated by Cargo.
+# It is not intended for manual editing.
+version = 3
+
+[[package]]
+name = "aho-corasick"
+version = "1.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916"
+dependencies = [
+ "memchr",
+]
+
+[[package]]
+name = "android-tzdata"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0"
+
+[[package]]
+name = "android_system_properties"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311"
+dependencies = [
+ "libc",
+]
+
+[[package]]
+name = "anyhow"
+version = "1.0.83"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "25bdb32cbbdce2b519a9cd7df3a678443100e265d5e25ca763b7572a5104f5f3"
+
+[[package]]
+name = "autocfg"
+version = "1.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0c4b4d0bd25bd0b74681c0ad21497610ce1b7c91b1022cd21c80c6fbdd9476b0"
+
+[[package]]
+name = "bumpalo"
+version = "3.16.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c"
+
+[[package]]
+name = "cc"
+version = "1.0.97"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "099a5357d84c4c61eb35fc8eafa9a79a902c2f76911e5747ced4e032edd8d9b4"
+
+[[package]]
+name = "cfg-if"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
+
+[[package]]
+name = "chrono"
+version = "0.4.38"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a21f936df1771bf62b77f047b726c4625ff2e8aa607c01ec06e5a05bd8463401"
+dependencies = [
+ "android-tzdata",
+ "iana-time-zone",
+ "js-sys",
+ "num-traits",
+ "wasm-bindgen",
+ "windows-targets",
+]
+
+[[package]]
+name = "chrono-humanize"
+version = "0.2.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "799627e6b4d27827a814e837b9d8a504832086081806d45b1afa34dc982b023b"
+dependencies = [
+ "chrono",
+]
+
+[[package]]
+name = "cli-log"
+version = "2.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3d2ab00dc4c82ec28af25ac085aecc11ffeabf353755715a3113a7aa044ca5cc"
+dependencies = [
+ "chrono",
+ "file-size",
+ "log",
+ "proc-status",
+]
+
+[[package]]
+name = "comments"
+version = "0.1.0"
+dependencies = [
+ "anyhow",
+ "chrono",
+ "chrono-humanize",
+ "cli-log",
+ "log",
+ "regex",
+ "serde",
+ "serde_json",
+]
+
+[[package]]
+name = "core-foundation-sys"
+version = "0.8.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "06ea2b9bc92be3c2baa9334a323ebca2d6f074ff852cd1d7b11064035cd3868f"
+
+[[package]]
+name = "file-size"
+version = "1.0.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9544f10105d33957765016b8a9baea7e689bf1f0f2f32c2fa2f568770c38d2b3"
+
+[[package]]
+name = "iana-time-zone"
+version = "0.1.60"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e7ffbb5a1b541ea2561f8c41c087286cc091e21e556a4f09a8f6cbf17b69b141"
+dependencies = [
+ "android_system_properties",
+ "core-foundation-sys",
+ "iana-time-zone-haiku",
+ "js-sys",
+ "wasm-bindgen",
+ "windows-core",
+]
+
+[[package]]
+name = "iana-time-zone-haiku"
+version = "0.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f"
+dependencies = [
+ "cc",
+]
+
+[[package]]
+name = "itoa"
+version = "1.0.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b"
+
+[[package]]
+name = "js-sys"
+version = "0.3.69"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "29c15563dc2726973df627357ce0c9ddddbea194836909d655df6a75d2cf296d"
+dependencies = [
+ "wasm-bindgen",
+]
+
+[[package]]
+name = "libc"
+version = "0.2.154"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ae743338b92ff9146ce83992f766a31066a91a8c84a45e0e9f21e7cf6de6d346"
+
+[[package]]
+name = "log"
+version = "0.4.21"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "90ed8c1e510134f979dbc4f070f87d4313098b704861a105fe34231c70a3901c"
+
+[[package]]
+name = "memchr"
+version = "2.7.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6c8640c5d730cb13ebd907d8d04b52f55ac9a2eec55b440c8892f40d56c76c1d"
+
+[[package]]
+name = "num-traits"
+version = "0.2.19"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841"
+dependencies = [
+ "autocfg",
+]
+
+[[package]]
+name = "once_cell"
+version = "1.19.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92"
+
+[[package]]
+name = "proc-macro2"
+version = "1.0.82"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8ad3d49ab951a01fbaafe34f2ec74122942fe18a3f9814c3268f1bb72042131b"
+dependencies = [
+ "unicode-ident",
+]
+
+[[package]]
+name = "proc-status"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f0e0c0ac915e7b76b47850ba4ffc377abde6c6ff9eeace61d0a89623db449712"
+dependencies = [
+ "thiserror",
+]
+
+[[package]]
+name = "quote"
+version = "1.0.36"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0fa76aaf39101c457836aec0ce2316dbdc3ab723cdda1c6bd4e6ad4208acaca7"
+dependencies = [
+ "proc-macro2",
+]
+
+[[package]]
+name = "regex"
+version = "1.10.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c117dbdfde9c8308975b6a18d71f3f385c89461f7b3fb054288ecf2a2058ba4c"
+dependencies = [
+ "aho-corasick",
+ "memchr",
+ "regex-automata",
+ "regex-syntax",
+]
+
+[[package]]
+name = "regex-automata"
+version = "0.4.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "86b83b8b9847f9bf95ef68afb0b8e6cdb80f498442f5179a29fad448fcc1eaea"
+dependencies = [
+ "aho-corasick",
+ "memchr",
+ "regex-syntax",
+]
+
+[[package]]
+name = "regex-syntax"
+version = "0.8.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "adad44e29e4c806119491a7f06f03de4d1af22c3a680dd47f1e6e179439d1f56"
+
+[[package]]
+name = "ryu"
+version = "1.0.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f"
+
+[[package]]
+name = "serde"
+version = "1.0.201"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "780f1cebed1629e4753a1a38a3c72d30b97ec044f0aef68cb26650a3c5cf363c"
+dependencies = [
+ "serde_derive",
+]
+
+[[package]]
+name = "serde_derive"
+version = "1.0.201"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c5e405930b9796f1c00bee880d03fc7e0bb4b9a11afc776885ffe84320da2865"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "serde_json"
+version = "1.0.117"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "455182ea6142b14f93f4bc5320a2b31c1f266b66a4a5c858b013302a5d8cbfc3"
+dependencies = [
+ "itoa",
+ "ryu",
+ "serde",
+]
+
+[[package]]
+name = "syn"
+version = "2.0.61"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c993ed8ccba56ae856363b1845da7266a7cb78e1d146c8a32d54b45a8b831fc9"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "unicode-ident",
+]
+
+[[package]]
+name = "thiserror"
+version = "1.0.60"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "579e9083ca58dd9dcf91a9923bb9054071b9ebbd800b342194c9feb0ee89fc18"
+dependencies = [
+ "thiserror-impl",
+]
+
+[[package]]
+name = "thiserror-impl"
+version = "1.0.60"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e2470041c06ec3ac1ab38d0356a6119054dedaea53e12fbefc0de730a1c08524"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "unicode-ident"
+version = "1.0.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b"
+
+[[package]]
+name = "wasm-bindgen"
+version = "0.2.92"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4be2531df63900aeb2bca0daaaddec08491ee64ceecbee5076636a3b026795a8"
+dependencies = [
+ "cfg-if",
+ "wasm-bindgen-macro",
+]
+
+[[package]]
+name = "wasm-bindgen-backend"
+version = "0.2.92"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "614d787b966d3989fa7bb98a654e369c762374fd3213d212cfc0251257e747da"
+dependencies = [
+ "bumpalo",
+ "log",
+ "once_cell",
+ "proc-macro2",
+ "quote",
+ "syn",
+ "wasm-bindgen-shared",
+]
+
+[[package]]
+name = "wasm-bindgen-macro"
+version = "0.2.92"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a1f8823de937b71b9460c0c34e25f3da88250760bec0ebac694b49997550d726"
+dependencies = [
+ "quote",
+ "wasm-bindgen-macro-support",
+]
+
+[[package]]
+name = "wasm-bindgen-macro-support"
+version = "0.2.92"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e94f17b526d0a461a191c78ea52bbce64071ed5c04c9ffe424dcb38f74171bb7"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+ "wasm-bindgen-backend",
+ "wasm-bindgen-shared",
+]
+
+[[package]]
+name = "wasm-bindgen-shared"
+version = "0.2.92"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "af190c94f2773fdb3729c55b007a722abb5384da03bc0986df4c289bf5567e96"
+
+[[package]]
+name = "windows-core"
+version = "0.52.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9"
+dependencies = [
+ "windows-targets",
+]
+
+[[package]]
+name = "windows-targets"
+version = "0.52.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6f0713a46559409d202e70e28227288446bf7841d3211583a4b53e3f6d96e7eb"
+dependencies = [
+ "windows_aarch64_gnullvm",
+ "windows_aarch64_msvc",
+ "windows_i686_gnu",
+ "windows_i686_gnullvm",
+ "windows_i686_msvc",
+ "windows_x86_64_gnu",
+ "windows_x86_64_gnullvm",
+ "windows_x86_64_msvc",
+]
+
+[[package]]
+name = "windows_aarch64_gnullvm"
+version = "0.52.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7088eed71e8b8dda258ecc8bac5fb1153c5cffaf2578fc8ff5d61e23578d3263"
+
+[[package]]
+name = "windows_aarch64_msvc"
+version = "0.52.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9985fd1504e250c615ca5f281c3f7a6da76213ebd5ccc9561496568a2752afb6"
+
+[[package]]
+name = "windows_i686_gnu"
+version = "0.52.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "88ba073cf16d5372720ec942a8ccbf61626074c6d4dd2e745299726ce8b89670"
+
+[[package]]
+name = "windows_i686_gnullvm"
+version = "0.52.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "87f4261229030a858f36b459e748ae97545d6f1ec60e5e0d6a3d32e0dc232ee9"
+
+[[package]]
+name = "windows_i686_msvc"
+version = "0.52.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "db3c2bf3d13d5b658be73463284eaf12830ac9a26a90c717b7f771dfe97487bf"
+
+[[package]]
+name = "windows_x86_64_gnu"
+version = "0.52.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4e4246f76bdeff09eb48875a0fd3e2af6aada79d409d33011886d3e1581517d9"
+
+[[package]]
+name = "windows_x86_64_gnullvm"
+version = "0.52.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "852298e482cd67c356ddd9570386e2862b5673c85bd5f88df9ab6802b334c596"
+
+[[package]]
+name = "windows_x86_64_msvc"
+version = "0.52.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bec47e5bfd1bff0eeaf6d8b485cc1074891a197ab4225d504cb7a1ab88b02bf0"
diff --git a/pkgs/sources/comments/Cargo.toml b/pkgs/sources/comments/Cargo.toml
new file mode 100644
index 00000000..3ae3aa4c
--- /dev/null
+++ b/pkgs/sources/comments/Cargo.toml
@@ -0,0 +1,16 @@
+[package]
+name = "comments"
+version = "0.1.0"
+edition = "2021"
+
+# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
+
+[dependencies]
+anyhow = "1.0.82"
+chrono = "0.4.38"
+chrono-humanize = "0.2.3"
+cli-log = "2.0.0"
+log = "0.4.21"
+regex = "1.10.4"
+serde = { version = "1.0.200", features = ["derive"] }
+serde_json = "1.0.116"
diff --git a/pkgs/sources/comments/comments.nix b/pkgs/sources/comments/comments.nix
new file mode 100644
index 00000000..e8a33bff
--- /dev/null
+++ b/pkgs/sources/comments/comments.nix
@@ -0,0 +1,25 @@
+{
+  lib,
+  rustPlatform,
+  makeWrapper,
+  less,
+  coreutils,
+}:
+rustPlatform.buildRustPackage {
+  pname = "comments";
+  version = "0.1.0";
+
+  src = ./.;
+  cargoLock = {
+    lockFile = ./Cargo.lock;
+  };
+
+  nativeBuildInputs = [
+    makeWrapper
+  ];
+
+  postInstall = ''
+    wrapProgram $out/bin/comments \
+      --set PATH ${lib.makeBinPath [less coreutils]}
+  '';
+}
diff --git a/pkgs/sources/comments/default.nix b/pkgs/sources/comments/default.nix
new file mode 100644
index 00000000..6205dcbe
--- /dev/null
+++ b/pkgs/sources/comments/default.nix
@@ -0,0 +1,18 @@
+[
+  (
+    final: prev: {
+      comments = import ./comments.nix {
+        inherit
+          (prev)
+          lib
+          makeWrapper
+          rustPlatform
+          # dependencies
+          
+          less
+          coreutils
+          ;
+      };
+    }
+  )
+]
diff --git a/pkgs/sources/comments/flake.lock b/pkgs/sources/comments/flake.lock
new file mode 100644
index 00000000..50494465
--- /dev/null
+++ b/pkgs/sources/comments/flake.lock
@@ -0,0 +1,61 @@
+{
+  "nodes": {
+    "flake-utils": {
+      "inputs": {
+        "systems": "systems"
+      },
+      "locked": {
+        "lastModified": 1710146030,
+        "narHash": "sha256-SZ5L6eA7HJ/nmkzGG7/ISclqe6oZdOZTNoesiInkXPQ=",
+        "owner": "numtide",
+        "repo": "flake-utils",
+        "rev": "b1d9ab70662946ef0850d488da1c9019f3a9752a",
+        "type": "github"
+      },
+      "original": {
+        "owner": "numtide",
+        "repo": "flake-utils",
+        "type": "github"
+      }
+    },
+    "nixpkgs": {
+      "locked": {
+        "lastModified": 1715087517,
+        "narHash": "sha256-CLU5Tsg24Ke4+7sH8azHWXKd0CFd4mhLWfhYgUiDBpQ=",
+        "owner": "NixOS",
+        "repo": "nixpkgs",
+        "rev": "b211b392b8486ee79df6cdfb1157ad2133427a29",
+        "type": "github"
+      },
+      "original": {
+        "owner": "NixOS",
+        "ref": "nixos-unstable",
+        "repo": "nixpkgs",
+        "type": "github"
+      }
+    },
+    "root": {
+      "inputs": {
+        "flake-utils": "flake-utils",
+        "nixpkgs": "nixpkgs"
+      }
+    },
+    "systems": {
+      "locked": {
+        "lastModified": 1681028828,
+        "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
+        "owner": "nix-systems",
+        "repo": "default",
+        "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
+        "type": "github"
+      },
+      "original": {
+        "owner": "nix-systems",
+        "repo": "default",
+        "type": "github"
+      }
+    }
+  },
+  "root": "root",
+  "version": 7
+}
diff --git a/pkgs/sources/comments/flake.nix b/pkgs/sources/comments/flake.nix
new file mode 100644
index 00000000..f5e44a65
--- /dev/null
+++ b/pkgs/sources/comments/flake.nix
@@ -0,0 +1,31 @@
+{
+  description = "comments";
+
+  inputs = {
+    nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
+
+    flake-utils.url = "github:numtide/flake-utils";
+  };
+
+  outputs = {
+    self,
+    nixpkgs,
+    flake-utils,
+  }: (flake-utils.lib.eachDefaultSystem (system: let
+    pkgs = nixpkgs.legacyPackages."${system}";
+  in {
+    devShells.default = pkgs.mkShell {
+      packages = with pkgs; [
+        # rust stuff
+        cargo
+        clippy
+        rustc
+        rustfmt
+
+        cargo-edit
+        cargo-expand
+        cargo-audit
+      ];
+    };
+  }));
+}
diff --git a/pkgs/sources/comments/src/info_json.rs b/pkgs/sources/comments/src/info_json.rs
new file mode 100644
index 00000000..eca4fae3
--- /dev/null
+++ b/pkgs/sources/comments/src/info_json.rs
@@ -0,0 +1,223 @@
+use std::collections::HashMap;
+
+use serde::{Deserialize, Deserializer};
+
+#[derive(Debug, Deserialize)]
+pub struct InfoJson {
+    pub id: String,
+    pub title: String,
+    pub formats: Vec<Format>,
+    pub thumbnails: Vec<ThumbNail>,
+    pub thumbnail: String,
+    pub description: String,
+    pub channel_id: String,
+    pub channel_url: String,
+    pub duration: u32,
+    pub view_count: u32,
+    pub age_limit: u32,
+    pub webpage_url: String,
+    pub categories: Vec<String>,
+    pub tags: Vec<String>,
+    pub playable_in_embed: bool,
+    pub live_status: String,
+    _format_sort_fields: Vec<String>,
+    pub automatic_captions: HashMap<String, Vec<Caption>>,
+    pub subtitles: Subtitles,
+    pub comment_count: u32,
+    pub like_count: u32,
+    pub channel: String,
+    pub channel_follower_count: u32,
+    pub channel_is_verified: Option<bool>,
+    pub uploader: String,
+    pub uploader_id: String,
+    pub uploader_url: String,
+    pub upload_date: String,
+    pub availability: String,
+    pub webpage_url_basename: String,
+    pub webpage_url_domain: String,
+    pub extractor: String,
+    pub extractor_key: String,
+    pub display_id: String,
+    pub fulltitle: String,
+    pub duration_string: String,
+    pub is_live: bool,
+    pub was_live: bool,
+    pub epoch: u32,
+    pub comments: Vec<Comment>,
+    pub sponsorblock_chapters: Option<Vec<SponsorblockChapter>>,
+    pub format: String,
+    pub format_id: String,
+    pub ext: String,
+    pub protocol: String,
+    pub language: Option<String>,
+    pub format_note: String,
+    pub filesize_approx: u64,
+    pub tbr: f64,
+    pub width: u32,
+    pub height: u32,
+    pub resolution: String,
+    pub fps: f64,
+    pub dynamic_range: String,
+    pub vcodec: String,
+    pub vbr: f64,
+    pub aspect_ratio: f64,
+    pub acodec: String,
+    pub abr: f64,
+    pub asr: u32,
+    pub audio_channels: u32,
+    _type: String,
+    _version: Version,
+}
+
+#[derive(Debug, Deserialize)]
+pub struct Subtitles {}
+
+#[derive(Debug, Deserialize)]
+pub struct Version {
+    pub version: String,
+    pub release_git_head: String,
+    pub repository: String,
+}
+
+#[derive(Debug, Deserialize)]
+pub struct SponsorblockChapter {}
+
+#[derive(Debug, Deserialize, Clone)]
+#[serde(from = "String")]
+pub enum Parent {
+    Root,
+    Id(String),
+}
+
+impl Parent {
+    pub fn id(&self) -> Option<&str> {
+        if let Self::Id(id) = self {
+            Some(id)
+        } else {
+            None
+        }
+    }
+}
+
+impl From<String> for Parent {
+    fn from(value: String) -> Self {
+        if value == "root" {
+            Self::Root
+        } else {
+            Self::Id(value)
+        }
+    }
+}
+
+#[derive(Debug, Deserialize, Clone)]
+#[serde(from = "String")]
+pub struct Id {
+    pub id: String,
+}
+impl From<String> for Id {
+    fn from(value: String) -> Self {
+        Self {
+            // Take the last element if the string is split with dots, otherwise take the full id
+            id: value.split('.').last().unwrap_or(&value).to_owned(),
+        }
+    }
+}
+
+#[derive(Debug, Deserialize, Clone)]
+pub struct Comment {
+    pub id: Id,
+    pub text: String,
+    #[serde(default = "zero")]
+    pub like_count: u32,
+    pub author_id: String,
+    #[serde(default = "unknown")]
+    pub author: String,
+    pub author_thumbnail: String,
+    pub parent: Parent,
+    #[serde(deserialize_with = "edited_from_time_text", alias = "_time_text")]
+    pub edited: bool,
+    // Can't also be deserialized, as it's already used in 'edited'
+    // _time_text: String,
+    pub timestamp: i64,
+    pub author_url: String,
+    pub author_is_uploader: bool,
+    pub is_favorited: bool,
+}
+fn unknown() -> String {
+    "<Unknown>".to_string()
+}
+fn zero() -> u32 {
+    0
+}
+fn edited_from_time_text<'de, D>(d: D) -> Result<bool, D::Error>
+where
+    D: Deserializer<'de>,
+{
+    let s = String::deserialize(d)?;
+    if s.contains(" (edited)") {
+        Ok(true)
+    } else {
+        Ok(false)
+    }
+}
+
+#[derive(Debug, Deserialize)]
+pub struct Caption {
+    pub ext: String,
+    pub url: String,
+    pub name: Option<String>,
+    pub protocol: Option<String>,
+}
+
+#[derive(Debug, Deserialize)]
+pub struct ThumbNail {
+    pub url: String,
+    pub preference: i32,
+    pub id: String,
+    pub height: Option<u32>,
+    pub width: Option<u32>,
+    pub resolution: Option<String>,
+}
+
+#[derive(Debug, Deserialize)]
+pub struct Format {
+    pub format_id: String,
+    pub format_note: Option<String>,
+    pub ext: String,
+    pub protocol: String,
+    pub acodec: Option<String>,
+    pub vcodec: String,
+    pub url: String,
+    pub width: Option<u32>,
+    pub height: Option<u32>,
+    pub fps: Option<f64>,
+    pub rows: Option<u32>,
+    pub columns: Option<u32>,
+    pub fragments: Option<Vec<Fragment>>,
+    pub resolution: String,
+    pub aspect_ratio: Option<f64>,
+    pub http_headers: HttpHeader,
+    pub audio_ext: String,
+    pub video_ext: String,
+    pub vbr: Option<f64>,
+    pub abr: Option<f64>,
+    pub format: String,
+}
+
+#[derive(Debug, Deserialize)]
+pub struct HttpHeader {
+    #[serde(alias = "User-Agent")]
+    pub user_agent: String,
+    #[serde(alias = "Accept")]
+    pub accept: String,
+    #[serde(alias = "Accept-Language")]
+    pub accept_language: String,
+    #[serde(alias = "Sec-Fetch-Mode")]
+    pub sec_fetch_mode: String,
+}
+
+#[derive(Debug, Deserialize)]
+pub struct Fragment {
+    pub url: String,
+    pub duration: f64,
+}
diff --git a/pkgs/sources/comments/src/main.rs b/pkgs/sources/comments/src/main.rs
new file mode 100644
index 00000000..6e4f72e9
--- /dev/null
+++ b/pkgs/sources/comments/src/main.rs
@@ -0,0 +1,322 @@
+use std::{
+    env,
+    fmt::Display,
+    fs::{self, File},
+    io::{BufReader, Write},
+    mem,
+    path::PathBuf,
+    process::{Command, Stdio},
+};
+
+use anyhow::Context;
+use chrono::{Local, TimeZone};
+use chrono_humanize::{Accuracy, HumanTime, Tense};
+use info_json::{Comment, InfoJson, Parent};
+use regex::Regex;
+
+mod info_json;
+
+fn get_runtime_path(component: &'static str) -> anyhow::Result<PathBuf> {
+    let out: PathBuf = format!(
+        "{}/{}",
+        env::var("XDG_RUNTIME_DIR").expect("This should always exist"),
+        component
+    )
+    .into();
+    fs::create_dir_all(out.parent().expect("Parent should exist"))?;
+    Ok(out)
+}
+
+const STATUS_PATH: &str = "ytcc/running";
+pub fn status_path() -> anyhow::Result<PathBuf> {
+    get_runtime_path(STATUS_PATH)
+}
+
+#[derive(Debug, Clone)]
+pub struct CommentExt {
+    pub value: Comment,
+    pub replies: Vec<CommentExt>,
+}
+
+#[derive(Debug, Default)]
+pub struct Comments {
+    vec: Vec<CommentExt>,
+}
+
+impl Comments {
+    pub fn new() -> Self {
+        Self::default()
+    }
+    pub fn push(&mut self, value: CommentExt) {
+        self.vec.push(value);
+    }
+    pub fn get_mut(&mut self, key: &str) -> Option<&mut CommentExt> {
+        self.vec.iter_mut().filter(|c| c.value.id.id == key).last()
+    }
+    pub fn insert(&mut self, key: &str, value: CommentExt) {
+        let parent = self
+            .vec
+            .iter_mut()
+            .filter(|c| c.value.id.id == key)
+            .last()
+            .expect("One of these should exist");
+        parent.push_reply(value);
+    }
+}
+impl CommentExt {
+    pub fn push_reply(&mut self, value: CommentExt) {
+        self.replies.push(value)
+    }
+    pub fn get_mut_reply(&mut self, key: &str) -> Option<&mut CommentExt> {
+        self.replies
+            .iter_mut()
+            .filter(|c| c.value.id.id == key)
+            .last()
+    }
+}
+
+impl From<Comment> for CommentExt {
+    fn from(value: Comment) -> Self {
+        Self {
+            replies: vec![],
+            value,
+        }
+    }
+}
+
+impl Display for Comments {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        macro_rules! c {
+            ($color:expr, $write:ident) => {
+                $write.write_str(concat!("\x1b[", $color, "m"))?
+            };
+        }
+
+        fn format(
+            comment: &CommentExt,
+            f: &mut std::fmt::Formatter<'_>,
+            ident_count: u32,
+        ) -> std::fmt::Result {
+            let ident = &(0..ident_count).map(|_| " ").collect::<String>();
+            let value = &comment.value;
+
+            f.write_str(ident)?;
+
+            if value.author_is_uploader {
+                c!("91;1", f);
+            } else {
+                c!("35", f);
+            }
+
+            f.write_str(&value.author)?;
+            c!("0", f);
+            if value.edited || value.is_favorited {
+                f.write_str("[")?;
+                if value.edited {
+                    f.write_str("")?;
+                }
+                if value.edited && value.is_favorited {
+                    f.write_str(" ")?;
+                }
+                if value.is_favorited {
+                    f.write_str("")?;
+                }
+                f.write_str("]")?;
+            }
+
+            c!("36;1", f);
+            write!(
+                f,
+                " {}",
+                HumanTime::from(
+                    Local
+                        .timestamp_opt(value.timestamp, 0)
+                        .single()
+                        .expect("This should be valid")
+                )
+                .to_text_en(Accuracy::Rough, Tense::Past)
+            )?;
+            c!("0", f);
+
+            // c!("31;1", f);
+            // f.write_fmt(format_args!(" [{}]", comment.value.like_count))?;
+            // c!("0", f);
+
+            f.write_str(":\n")?;
+            f.write_str(ident)?;
+
+            f.write_str(&value.text.replace('\n', &format!("\n{}", ident)))?;
+            f.write_str("\n")?;
+
+            if !comment.replies.is_empty() {
+                let mut children = comment.replies.clone();
+                children.sort_by(|a, b| a.value.timestamp.cmp(&b.value.timestamp));
+
+                for child in children {
+                    format(&child, f, ident_count + 4)?;
+                }
+            } else {
+                f.write_str("\n")?;
+            }
+
+            Ok(())
+        }
+
+        if !&self.vec.is_empty() {
+            let mut children = self.vec.clone();
+            children.sort_by(|a, b| b.value.like_count.cmp(&a.value.like_count));
+
+            for child in children {
+                format(&child, f, 0)?
+            }
+        }
+        Ok(())
+    }
+}
+
+fn main() -> anyhow::Result<()> {
+    cli_log::init_cli_log!();
+    let args: Option<String> = env::args().skip(1).last();
+    let mut info_json: InfoJson = {
+        let status_path = if let Some(arg) = args {
+            PathBuf::from(arg)
+        } else {
+            status_path().context("Failed to get status path")?
+        };
+
+        let reader =
+            BufReader::new(File::open(&status_path).with_context(|| {
+                format!("Failed to open status file at {}", status_path.display())
+            })?);
+
+        serde_json::from_reader(reader)?
+    };
+
+    let base_comments = mem::take(&mut info_json.comments);
+    drop(info_json);
+
+    let mut comments = Comments::new();
+    base_comments.into_iter().for_each(|c| {
+        if let Parent::Id(id) = &c.parent {
+            comments.insert(&(id.clone()), CommentExt::from(c));
+        } else {
+            comments.push(CommentExt::from(c));
+        }
+    });
+
+    comments.vec.iter_mut().for_each(|comment| {
+        let replies = mem::take(&mut comment.replies);
+        let mut output_replies: Vec<CommentExt>  = vec![];
+
+        let re = Regex::new(r"\u{200b}?(@[^\t\s]+)\u{200b}?").unwrap();
+        for reply in replies {
+            if let Some(replyee_match) =  re.captures(&reply.value.text){
+                let full_match = replyee_match.get(0).expect("This always exists");
+                let text = reply.
+                    value.
+                    text[0..full_match.start()]
+                    .to_owned()
+                    +
+                    &reply
+                    .value
+                    .text[full_match.end()..];
+                let text: &str = text.trim().trim_matches('\u{200b}');
+
+                let replyee = replyee_match.get(1).expect("This should exist").as_str();
+
+
+                if let Some(parent) = output_replies
+                    .iter_mut()
+                    // .rev()
+                    .flat_map(|com| &mut com.replies)
+                    .flat_map(|com| &mut com.replies)
+                    .flat_map(|com| &mut com.replies)
+                    .filter(|com| com.value.author == replyee)
+                    .last()
+                {
+                    parent.replies.push(CommentExt::from(Comment {
+                        text: text.to_owned(),
+                        ..reply.value
+                    }))
+                } else if let Some(parent) = output_replies
+                    .iter_mut()
+                    // .rev()
+                    .flat_map(|com| &mut com.replies)
+                    .flat_map(|com| &mut com.replies)
+                    .filter(|com| com.value.author == replyee)
+                    .last()
+                {
+                    parent.replies.push(CommentExt::from(Comment {
+                        text: text.to_owned(),
+                        ..reply.value
+                    }))
+                } else if let Some(parent) = output_replies
+                    .iter_mut()
+                    // .rev()
+                    .flat_map(|com| &mut com.replies)
+                    .filter(|com| com.value.author == replyee)
+                    .last()
+                {
+                    parent.replies.push(CommentExt::from(Comment {
+                        text: text.to_owned(),
+                        ..reply.value
+                    }))
+                } else if let Some(parent) = output_replies.iter_mut()
+                    // .rev()
+                    .filter(|com| com.value.author == replyee)
+                    .last()
+                {
+                    parent.replies.push(CommentExt::from(Comment {
+                        text: text.to_owned(),
+                        ..reply.value
+                    }))
+                } else {
+                    eprintln!(
+                    "Failed to find a parent for ('{}') both directly and via replies! The reply text was:\n'{}'\n",
+                    replyee,
+                    reply.value.text
+                );
+                    output_replies.push(reply);
+                }
+            } else {
+                output_replies.push(reply);
+            }
+        }
+        comment.replies = output_replies;
+    });
+
+    let mut less = Command::new("less")
+        .args(["--raw-control-chars"])
+        .stdin(Stdio::piped())
+        .stderr(Stdio::inherit())
+        .spawn()
+        .context("Failed to run less")?;
+
+    let mut child = Command::new("fmt")
+        .args(["--uniform-spacing", "--split-only", "--width=90"])
+        .stdin(Stdio::piped())
+        .stderr(Stdio::inherit())
+        .stdout(less.stdin.take().expect("Should be open"))
+        .spawn()
+        .context("Failed to run fmt")?;
+
+    let mut stdin = child.stdin.take().context("Failed to open stdin")?;
+    std::thread::spawn(move || {
+        stdin
+            .write_all(comments.to_string().as_bytes())
+            .expect("Should be able to write to stdin of fmt");
+    });
+
+    let _ = less.wait().context("Failed to await less")?;
+
+    Ok(())
+}
+
+#[cfg(test)]
+mod test {
+    #[test]
+    fn test_string_replacement() {
+        let s = "A \n\nB\n\nC".to_owned();
+        assert_eq!("A \n  \n  B\n  \n  C", s.replace('\n', "\n  "))
+    }
+}
diff --git a/pkgs/sources/comments/update.sh b/pkgs/sources/comments/update.sh
new file mode 100755
index 00000000..e500bb23
--- /dev/null
+++ b/pkgs/sources/comments/update.sh
@@ -0,0 +1,8 @@
+#!/usr/bin/env sh
+
+nix flake update
+
+[ "$1" = "upgrade" ] && cargo upgrade
+cargo update
+
+# vim: ft=sh
diff --git a/pkgs/sources/default.nix b/pkgs/sources/default.nix
new file mode 100644
index 00000000..4668d735
--- /dev/null
+++ b/pkgs/sources/default.nix
@@ -0,0 +1,27 @@
+{
+  homeConfig,
+  nixosConfig,
+  sysLib,
+}: let
+  comments = import ./comments;
+  generate_firefox_extensions = import ./generate_moz_extension;
+  lf_make_map = import ./lf-make-map;
+  nvim_plugs = import ./plgs-pkgs;
+  scripts = import ./scripts {inherit sysLib homeConfig nixosConfig;};
+  snap-sync-forked = (import ./snap-sync-forked) {inherit sysLib;};
+  update_vim_plugins = import ./update_vim_plugins;
+  yt = import ./yt;
+  yts-grammar = import ./tree-sitter-yts;
+
+  overlays =
+    comments
+    ++ generate_firefox_extensions
+    ++ lf_make_map
+    ++ nvim_plugs
+    ++ scripts
+    ++ snap-sync-forked
+    ++ update_vim_plugins
+    ++ yt
+    ++ yts-grammar;
+in
+  overlays
diff --git a/pkgs/sources/generate_moz_extension/.envrc b/pkgs/sources/generate_moz_extension/.envrc
new file mode 100644
index 00000000..3550a30f
--- /dev/null
+++ b/pkgs/sources/generate_moz_extension/.envrc
@@ -0,0 +1 @@
+use flake
diff --git a/pkgs/sources/generate_moz_extension/.gitignore b/pkgs/sources/generate_moz_extension/.gitignore
new file mode 100644
index 00000000..f717ddd7
--- /dev/null
+++ b/pkgs/sources/generate_moz_extension/.gitignore
@@ -0,0 +1,3 @@
+/target
+/result
+.direnv
diff --git a/pkgs/sources/generate_moz_extension/Cargo.lock b/pkgs/sources/generate_moz_extension/Cargo.lock
new file mode 100644
index 00000000..c0a83aa8
--- /dev/null
+++ b/pkgs/sources/generate_moz_extension/Cargo.lock
@@ -0,0 +1,1275 @@
+# This file is automatically @generated by Cargo.
+# It is not intended for manual editing.
+version = 3
+
+[[package]]
+name = "addr2line"
+version = "0.21.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8a30b2e23b9e17a9f90641c7ab1549cd9b44f296d3ccbf309d2863cfe398a0cb"
+dependencies = [
+ "gimli",
+]
+
+[[package]]
+name = "adler"
+version = "1.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe"
+
+[[package]]
+name = "anyhow"
+version = "1.0.83"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "25bdb32cbbdce2b519a9cd7df3a678443100e265d5e25ca763b7572a5104f5f3"
+
+[[package]]
+name = "autocfg"
+version = "1.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0c4b4d0bd25bd0b74681c0ad21497610ce1b7c91b1022cd21c80c6fbdd9476b0"
+
+[[package]]
+name = "backtrace"
+version = "0.3.71"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "26b05800d2e817c8b3b4b54abd461726265fa9789ae34330622f2db9ee696f9d"
+dependencies = [
+ "addr2line",
+ "cc",
+ "cfg-if",
+ "libc",
+ "miniz_oxide",
+ "object",
+ "rustc-demangle",
+]
+
+[[package]]
+name = "base64"
+version = "0.22.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
+
+[[package]]
+name = "bitflags"
+version = "1.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
+
+[[package]]
+name = "bitflags"
+version = "2.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cf4b9d6a944f767f8e5e0db018570623c85f3d925ac718db4e06d0187adb21c1"
+
+[[package]]
+name = "bumpalo"
+version = "3.16.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c"
+
+[[package]]
+name = "bytes"
+version = "1.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "514de17de45fdb8dc022b1a7975556c53c86f9f0aa5f534b98977b171857c2c9"
+
+[[package]]
+name = "cc"
+version = "1.0.97"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "099a5357d84c4c61eb35fc8eafa9a79a902c2f76911e5747ced4e032edd8d9b4"
+
+[[package]]
+name = "cfg-if"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
+
+[[package]]
+name = "core-foundation"
+version = "0.9.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f"
+dependencies = [
+ "core-foundation-sys",
+ "libc",
+]
+
+[[package]]
+name = "core-foundation-sys"
+version = "0.8.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "06ea2b9bc92be3c2baa9334a323ebca2d6f074ff852cd1d7b11064035cd3868f"
+
+[[package]]
+name = "encoding_rs"
+version = "0.8.34"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b45de904aa0b010bce2ab45264d0631681847fa7b6f2eaa7dab7619943bc4f59"
+dependencies = [
+ "cfg-if",
+]
+
+[[package]]
+name = "equivalent"
+version = "1.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5"
+
+[[package]]
+name = "errno"
+version = "0.3.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "534c5cf6194dfab3db3242765c03bbe257cf92f22b38f6bc0c58d59108a820ba"
+dependencies = [
+ "libc",
+ "windows-sys 0.52.0",
+]
+
+[[package]]
+name = "fastrand"
+version = "2.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9fc0510504f03c51ada170672ac806f1f105a88aa97a5281117e1ddc3368e51a"
+
+[[package]]
+name = "fnv"
+version = "1.0.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
+
+[[package]]
+name = "foreign-types"
+version = "0.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1"
+dependencies = [
+ "foreign-types-shared",
+]
+
+[[package]]
+name = "foreign-types-shared"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b"
+
+[[package]]
+name = "form_urlencoded"
+version = "1.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456"
+dependencies = [
+ "percent-encoding",
+]
+
+[[package]]
+name = "futures"
+version = "0.3.30"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "645c6916888f6cb6350d2550b80fb63e734897a8498abe35cfb732b6487804b0"
+dependencies = [
+ "futures-channel",
+ "futures-core",
+ "futures-executor",
+ "futures-io",
+ "futures-sink",
+ "futures-task",
+ "futures-util",
+]
+
+[[package]]
+name = "futures-channel"
+version = "0.3.30"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "eac8f7d7865dcb88bd4373ab671c8cf4508703796caa2b1985a9ca867b3fcb78"
+dependencies = [
+ "futures-core",
+ "futures-sink",
+]
+
+[[package]]
+name = "futures-core"
+version = "0.3.30"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dfc6580bb841c5a68e9ef15c77ccc837b40a7504914d52e47b8b0e9bbda25a1d"
+
+[[package]]
+name = "futures-executor"
+version = "0.3.30"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a576fc72ae164fca6b9db127eaa9a9dda0d61316034f33a0a0d4eda41f02b01d"
+dependencies = [
+ "futures-core",
+ "futures-task",
+ "futures-util",
+]
+
+[[package]]
+name = "futures-io"
+version = "0.3.30"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a44623e20b9681a318efdd71c299b6b222ed6f231972bfe2f224ebad6311f0c1"
+
+[[package]]
+name = "futures-macro"
+version = "0.3.30"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "futures-sink"
+version = "0.3.30"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9fb8e00e87438d937621c1c6269e53f536c14d3fbd6a042bb24879e57d474fb5"
+
+[[package]]
+name = "futures-task"
+version = "0.3.30"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "38d84fa142264698cdce1a9f9172cf383a0c82de1bddcf3092901442c4097004"
+
+[[package]]
+name = "futures-util"
+version = "0.3.30"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3d6401deb83407ab3da39eba7e33987a73c3df0c82b4bb5813ee871c19c41d48"
+dependencies = [
+ "futures-channel",
+ "futures-core",
+ "futures-io",
+ "futures-macro",
+ "futures-sink",
+ "futures-task",
+ "memchr",
+ "pin-project-lite",
+ "pin-utils",
+ "slab",
+]
+
+[[package]]
+name = "generate_extensions"
+version = "0.1.0"
+dependencies = [
+ "anyhow",
+ "futures",
+ "reqwest",
+ "serde",
+ "serde_json",
+ "tokio",
+]
+
+[[package]]
+name = "gimli"
+version = "0.28.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4271d37baee1b8c7e4b708028c57d816cf9d2434acb33a549475f78c181f6253"
+
+[[package]]
+name = "h2"
+version = "0.4.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "816ec7294445779408f36fe57bc5b7fc1cf59664059096c65f905c1c61f58069"
+dependencies = [
+ "bytes",
+ "fnv",
+ "futures-core",
+ "futures-sink",
+ "futures-util",
+ "http",
+ "indexmap",
+ "slab",
+ "tokio",
+ "tokio-util",
+ "tracing",
+]
+
+[[package]]
+name = "hashbrown"
+version = "0.14.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1"
+
+[[package]]
+name = "hermit-abi"
+version = "0.3.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024"
+
+[[package]]
+name = "http"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "21b9ddb458710bc376481b842f5da65cdf31522de232c1ca8146abce2a358258"
+dependencies = [
+ "bytes",
+ "fnv",
+ "itoa",
+]
+
+[[package]]
+name = "http-body"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1cac85db508abc24a2e48553ba12a996e87244a0395ce011e62b37158745d643"
+dependencies = [
+ "bytes",
+ "http",
+]
+
+[[package]]
+name = "http-body-util"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0475f8b2ac86659c21b64320d5d653f9efe42acd2a4e560073ec61a155a34f1d"
+dependencies = [
+ "bytes",
+ "futures-core",
+ "http",
+ "http-body",
+ "pin-project-lite",
+]
+
+[[package]]
+name = "httparse"
+version = "1.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d897f394bad6a705d5f4104762e116a75639e470d80901eed05a860a95cb1904"
+
+[[package]]
+name = "hyper"
+version = "1.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fe575dd17d0862a9a33781c8c4696a55c320909004a67a00fb286ba8b1bc496d"
+dependencies = [
+ "bytes",
+ "futures-channel",
+ "futures-util",
+ "h2",
+ "http",
+ "http-body",
+ "httparse",
+ "itoa",
+ "pin-project-lite",
+ "smallvec",
+ "tokio",
+ "want",
+]
+
+[[package]]
+name = "hyper-tls"
+version = "0.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "70206fc6890eaca9fde8a0bf71caa2ddfc9fe045ac9e5c70df101a7dbde866e0"
+dependencies = [
+ "bytes",
+ "http-body-util",
+ "hyper",
+ "hyper-util",
+ "native-tls",
+ "tokio",
+ "tokio-native-tls",
+ "tower-service",
+]
+
+[[package]]
+name = "hyper-util"
+version = "0.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ca38ef113da30126bbff9cd1705f9273e15d45498615d138b0c20279ac7a76aa"
+dependencies = [
+ "bytes",
+ "futures-channel",
+ "futures-util",
+ "http",
+ "http-body",
+ "hyper",
+ "pin-project-lite",
+ "socket2",
+ "tokio",
+ "tower",
+ "tower-service",
+ "tracing",
+]
+
+[[package]]
+name = "idna"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "634d9b1461af396cad843f47fdba5597a4f9e6ddd4bfb6ff5d85028c25cb12f6"
+dependencies = [
+ "unicode-bidi",
+ "unicode-normalization",
+]
+
+[[package]]
+name = "indexmap"
+version = "2.2.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "168fb715dda47215e360912c096649d23d58bf392ac62f73919e831745e40f26"
+dependencies = [
+ "equivalent",
+ "hashbrown",
+]
+
+[[package]]
+name = "ipnet"
+version = "2.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8f518f335dce6725a761382244631d86cf0ccb2863413590b31338feb467f9c3"
+
+[[package]]
+name = "itoa"
+version = "1.0.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b"
+
+[[package]]
+name = "js-sys"
+version = "0.3.69"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "29c15563dc2726973df627357ce0c9ddddbea194836909d655df6a75d2cf296d"
+dependencies = [
+ "wasm-bindgen",
+]
+
+[[package]]
+name = "lazy_static"
+version = "1.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"
+
+[[package]]
+name = "libc"
+version = "0.2.154"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ae743338b92ff9146ce83992f766a31066a91a8c84a45e0e9f21e7cf6de6d346"
+
+[[package]]
+name = "linux-raw-sys"
+version = "0.4.13"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "01cda141df6706de531b6c46c3a33ecca755538219bd484262fa09410c13539c"
+
+[[package]]
+name = "log"
+version = "0.4.21"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "90ed8c1e510134f979dbc4f070f87d4313098b704861a105fe34231c70a3901c"
+
+[[package]]
+name = "memchr"
+version = "2.7.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6c8640c5d730cb13ebd907d8d04b52f55ac9a2eec55b440c8892f40d56c76c1d"
+
+[[package]]
+name = "mime"
+version = "0.3.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a"
+
+[[package]]
+name = "miniz_oxide"
+version = "0.7.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9d811f3e15f28568be3407c8e7fdb6514c1cda3cb30683f15b6a1a1dc4ea14a7"
+dependencies = [
+ "adler",
+]
+
+[[package]]
+name = "mio"
+version = "0.8.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a4a650543ca06a924e8b371db273b2756685faae30f8487da1b56505a8f78b0c"
+dependencies = [
+ "libc",
+ "wasi",
+ "windows-sys 0.48.0",
+]
+
+[[package]]
+name = "native-tls"
+version = "0.2.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "07226173c32f2926027b63cce4bcd8076c3552846cbe7925f3aaffeac0a3b92e"
+dependencies = [
+ "lazy_static",
+ "libc",
+ "log",
+ "openssl",
+ "openssl-probe",
+ "openssl-sys",
+ "schannel",
+ "security-framework",
+ "security-framework-sys",
+ "tempfile",
+]
+
+[[package]]
+name = "num_cpus"
+version = "1.16.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4161fcb6d602d4d2081af7c3a45852d875a03dd337a6bfdd6e06407b61342a43"
+dependencies = [
+ "hermit-abi",
+ "libc",
+]
+
+[[package]]
+name = "object"
+version = "0.32.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a6a622008b6e321afc04970976f62ee297fdbaa6f95318ca343e3eebb9648441"
+dependencies = [
+ "memchr",
+]
+
+[[package]]
+name = "once_cell"
+version = "1.19.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92"
+
+[[package]]
+name = "openssl"
+version = "0.10.64"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "95a0481286a310808298130d22dd1fef0fa571e05a8f44ec801801e84b216b1f"
+dependencies = [
+ "bitflags 2.5.0",
+ "cfg-if",
+ "foreign-types",
+ "libc",
+ "once_cell",
+ "openssl-macros",
+ "openssl-sys",
+]
+
+[[package]]
+name = "openssl-macros"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "openssl-probe"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf"
+
+[[package]]
+name = "openssl-sys"
+version = "0.9.102"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c597637d56fbc83893a35eb0dd04b2b8e7a50c91e64e9493e398b5df4fb45fa2"
+dependencies = [
+ "cc",
+ "libc",
+ "pkg-config",
+ "vcpkg",
+]
+
+[[package]]
+name = "percent-encoding"
+version = "2.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e"
+
+[[package]]
+name = "pin-project"
+version = "1.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b6bf43b791c5b9e34c3d182969b4abb522f9343702850a2e57f460d00d09b4b3"
+dependencies = [
+ "pin-project-internal",
+]
+
+[[package]]
+name = "pin-project-internal"
+version = "1.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2f38a4412a78282e09a2cf38d195ea5420d15ba0602cb375210efbc877243965"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "pin-project-lite"
+version = "0.2.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bda66fc9667c18cb2758a2ac84d1167245054bcf85d5d1aaa6923f45801bdd02"
+
+[[package]]
+name = "pin-utils"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"
+
+[[package]]
+name = "pkg-config"
+version = "0.3.30"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d231b230927b5e4ad203db57bbcbee2802f6bce620b1e4a9024a07d94e2907ec"
+
+[[package]]
+name = "proc-macro2"
+version = "1.0.82"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8ad3d49ab951a01fbaafe34f2ec74122942fe18a3f9814c3268f1bb72042131b"
+dependencies = [
+ "unicode-ident",
+]
+
+[[package]]
+name = "quote"
+version = "1.0.36"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0fa76aaf39101c457836aec0ce2316dbdc3ab723cdda1c6bd4e6ad4208acaca7"
+dependencies = [
+ "proc-macro2",
+]
+
+[[package]]
+name = "reqwest"
+version = "0.12.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "566cafdd92868e0939d3fb961bd0dc25fcfaaed179291093b3d43e6b3150ea10"
+dependencies = [
+ "base64",
+ "bytes",
+ "encoding_rs",
+ "futures-core",
+ "futures-util",
+ "h2",
+ "http",
+ "http-body",
+ "http-body-util",
+ "hyper",
+ "hyper-tls",
+ "hyper-util",
+ "ipnet",
+ "js-sys",
+ "log",
+ "mime",
+ "native-tls",
+ "once_cell",
+ "percent-encoding",
+ "pin-project-lite",
+ "rustls-pemfile",
+ "serde",
+ "serde_json",
+ "serde_urlencoded",
+ "sync_wrapper",
+ "system-configuration",
+ "tokio",
+ "tokio-native-tls",
+ "tower-service",
+ "url",
+ "wasm-bindgen",
+ "wasm-bindgen-futures",
+ "web-sys",
+ "winreg",
+]
+
+[[package]]
+name = "rustc-demangle"
+version = "0.1.24"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f"
+
+[[package]]
+name = "rustix"
+version = "0.38.34"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "70dc5ec042f7a43c4a73241207cecc9873a06d45debb38b329f8541d85c2730f"
+dependencies = [
+ "bitflags 2.5.0",
+ "errno",
+ "libc",
+ "linux-raw-sys",
+ "windows-sys 0.52.0",
+]
+
+[[package]]
+name = "rustls-pemfile"
+version = "2.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "29993a25686778eb88d4189742cd713c9bce943bc54251a33509dc63cbacf73d"
+dependencies = [
+ "base64",
+ "rustls-pki-types",
+]
+
+[[package]]
+name = "rustls-pki-types"
+version = "1.7.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "976295e77ce332211c0d24d92c0e83e50f5c5f046d11082cea19f3df13a3562d"
+
+[[package]]
+name = "ryu"
+version = "1.0.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f"
+
+[[package]]
+name = "schannel"
+version = "0.1.23"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fbc91545643bcf3a0bbb6569265615222618bdf33ce4ffbbd13c4bbd4c093534"
+dependencies = [
+ "windows-sys 0.52.0",
+]
+
+[[package]]
+name = "security-framework"
+version = "2.11.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c627723fd09706bacdb5cf41499e95098555af3c3c29d014dc3c458ef6be11c0"
+dependencies = [
+ "bitflags 2.5.0",
+ "core-foundation",
+ "core-foundation-sys",
+ "libc",
+ "security-framework-sys",
+]
+
+[[package]]
+name = "security-framework-sys"
+version = "2.11.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "317936bbbd05227752583946b9e66d7ce3b489f84e11a94a510b4437fef407d7"
+dependencies = [
+ "core-foundation-sys",
+ "libc",
+]
+
+[[package]]
+name = "serde"
+version = "1.0.201"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "780f1cebed1629e4753a1a38a3c72d30b97ec044f0aef68cb26650a3c5cf363c"
+dependencies = [
+ "serde_derive",
+]
+
+[[package]]
+name = "serde_derive"
+version = "1.0.201"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c5e405930b9796f1c00bee880d03fc7e0bb4b9a11afc776885ffe84320da2865"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "serde_json"
+version = "1.0.117"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "455182ea6142b14f93f4bc5320a2b31c1f266b66a4a5c858b013302a5d8cbfc3"
+dependencies = [
+ "itoa",
+ "ryu",
+ "serde",
+]
+
+[[package]]
+name = "serde_urlencoded"
+version = "0.7.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd"
+dependencies = [
+ "form_urlencoded",
+ "itoa",
+ "ryu",
+ "serde",
+]
+
+[[package]]
+name = "slab"
+version = "0.4.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67"
+dependencies = [
+ "autocfg",
+]
+
+[[package]]
+name = "smallvec"
+version = "1.13.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67"
+
+[[package]]
+name = "socket2"
+version = "0.5.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ce305eb0b4296696835b71df73eb912e0f1ffd2556a501fcede6e0c50349191c"
+dependencies = [
+ "libc",
+ "windows-sys 0.52.0",
+]
+
+[[package]]
+name = "syn"
+version = "2.0.61"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c993ed8ccba56ae856363b1845da7266a7cb78e1d146c8a32d54b45a8b831fc9"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "unicode-ident",
+]
+
+[[package]]
+name = "sync_wrapper"
+version = "0.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2047c6ded9c721764247e62cd3b03c09ffc529b2ba5b10ec482ae507a4a70160"
+
+[[package]]
+name = "system-configuration"
+version = "0.5.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ba3a3adc5c275d719af8cb4272ea1c4a6d668a777f37e115f6d11ddbc1c8e0e7"
+dependencies = [
+ "bitflags 1.3.2",
+ "core-foundation",
+ "system-configuration-sys",
+]
+
+[[package]]
+name = "system-configuration-sys"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a75fb188eb626b924683e3b95e3a48e63551fcfb51949de2f06a9d91dbee93c9"
+dependencies = [
+ "core-foundation-sys",
+ "libc",
+]
+
+[[package]]
+name = "tempfile"
+version = "3.10.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "85b77fafb263dd9d05cbeac119526425676db3784113aa9295c88498cbf8bff1"
+dependencies = [
+ "cfg-if",
+ "fastrand",
+ "rustix",
+ "windows-sys 0.52.0",
+]
+
+[[package]]
+name = "tinyvec"
+version = "1.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "87cc5ceb3875bb20c2890005a4e226a4651264a5c75edb2421b52861a0a0cb50"
+dependencies = [
+ "tinyvec_macros",
+]
+
+[[package]]
+name = "tinyvec_macros"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
+
+[[package]]
+name = "tokio"
+version = "1.37.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1adbebffeca75fcfd058afa480fb6c0b81e165a0323f9c9d39c9697e37c46787"
+dependencies = [
+ "backtrace",
+ "bytes",
+ "libc",
+ "mio",
+ "num_cpus",
+ "pin-project-lite",
+ "socket2",
+ "tokio-macros",
+ "windows-sys 0.48.0",
+]
+
+[[package]]
+name = "tokio-macros"
+version = "2.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5b8a1e28f2deaa14e508979454cb3a223b10b938b45af148bc0986de36f1923b"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "tokio-native-tls"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2"
+dependencies = [
+ "native-tls",
+ "tokio",
+]
+
+[[package]]
+name = "tokio-util"
+version = "0.7.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9cf6b47b3771c49ac75ad09a6162f53ad4b8088b76ac60e8ec1455b31a189fe1"
+dependencies = [
+ "bytes",
+ "futures-core",
+ "futures-sink",
+ "pin-project-lite",
+ "tokio",
+]
+
+[[package]]
+name = "tower"
+version = "0.4.13"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b8fa9be0de6cf49e536ce1851f987bd21a43b771b09473c3549a6c853db37c1c"
+dependencies = [
+ "futures-core",
+ "futures-util",
+ "pin-project",
+ "pin-project-lite",
+ "tokio",
+ "tower-layer",
+ "tower-service",
+ "tracing",
+]
+
+[[package]]
+name = "tower-layer"
+version = "0.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c20c8dbed6283a09604c3e69b4b7eeb54e298b8a600d4d5ecb5ad39de609f1d0"
+
+[[package]]
+name = "tower-service"
+version = "0.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b6bc1c9ce2b5135ac7f93c72918fc37feb872bdc6a5533a8b85eb4b86bfdae52"
+
+[[package]]
+name = "tracing"
+version = "0.1.40"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c3523ab5a71916ccf420eebdf5521fcef02141234bbc0b8a49f2fdc4544364ef"
+dependencies = [
+ "log",
+ "pin-project-lite",
+ "tracing-core",
+]
+
+[[package]]
+name = "tracing-core"
+version = "0.1.32"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c06d3da6113f116aaee68e4d601191614c9053067f9ab7f6edbcb161237daa54"
+dependencies = [
+ "once_cell",
+]
+
+[[package]]
+name = "try-lock"
+version = "0.2.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b"
+
+[[package]]
+name = "unicode-bidi"
+version = "0.3.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "08f95100a766bf4f8f28f90d77e0a5461bbdb219042e7679bebe79004fed8d75"
+
+[[package]]
+name = "unicode-ident"
+version = "1.0.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b"
+
+[[package]]
+name = "unicode-normalization"
+version = "0.1.23"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a56d1686db2308d901306f92a263857ef59ea39678a5458e7cb17f01415101f5"
+dependencies = [
+ "tinyvec",
+]
+
+[[package]]
+name = "url"
+version = "2.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "31e6302e3bb753d46e83516cae55ae196fc0c309407cf11ab35cc51a4c2a4633"
+dependencies = [
+ "form_urlencoded",
+ "idna",
+ "percent-encoding",
+]
+
+[[package]]
+name = "vcpkg"
+version = "0.2.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426"
+
+[[package]]
+name = "want"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e"
+dependencies = [
+ "try-lock",
+]
+
+[[package]]
+name = "wasi"
+version = "0.11.0+wasi-snapshot-preview1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423"
+
+[[package]]
+name = "wasm-bindgen"
+version = "0.2.92"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4be2531df63900aeb2bca0daaaddec08491ee64ceecbee5076636a3b026795a8"
+dependencies = [
+ "cfg-if",
+ "wasm-bindgen-macro",
+]
+
+[[package]]
+name = "wasm-bindgen-backend"
+version = "0.2.92"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "614d787b966d3989fa7bb98a654e369c762374fd3213d212cfc0251257e747da"
+dependencies = [
+ "bumpalo",
+ "log",
+ "once_cell",
+ "proc-macro2",
+ "quote",
+ "syn",
+ "wasm-bindgen-shared",
+]
+
+[[package]]
+name = "wasm-bindgen-futures"
+version = "0.4.42"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "76bc14366121efc8dbb487ab05bcc9d346b3b5ec0eaa76e46594cabbe51762c0"
+dependencies = [
+ "cfg-if",
+ "js-sys",
+ "wasm-bindgen",
+ "web-sys",
+]
+
+[[package]]
+name = "wasm-bindgen-macro"
+version = "0.2.92"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a1f8823de937b71b9460c0c34e25f3da88250760bec0ebac694b49997550d726"
+dependencies = [
+ "quote",
+ "wasm-bindgen-macro-support",
+]
+
+[[package]]
+name = "wasm-bindgen-macro-support"
+version = "0.2.92"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e94f17b526d0a461a191c78ea52bbce64071ed5c04c9ffe424dcb38f74171bb7"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+ "wasm-bindgen-backend",
+ "wasm-bindgen-shared",
+]
+
+[[package]]
+name = "wasm-bindgen-shared"
+version = "0.2.92"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "af190c94f2773fdb3729c55b007a722abb5384da03bc0986df4c289bf5567e96"
+
+[[package]]
+name = "web-sys"
+version = "0.3.69"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "77afa9a11836342370f4817622a2f0f418b134426d91a82dfb48f532d2ec13ef"
+dependencies = [
+ "js-sys",
+ "wasm-bindgen",
+]
+
+[[package]]
+name = "windows-sys"
+version = "0.48.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9"
+dependencies = [
+ "windows-targets 0.48.5",
+]
+
+[[package]]
+name = "windows-sys"
+version = "0.52.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d"
+dependencies = [
+ "windows-targets 0.52.5",
+]
+
+[[package]]
+name = "windows-targets"
+version = "0.48.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c"
+dependencies = [
+ "windows_aarch64_gnullvm 0.48.5",
+ "windows_aarch64_msvc 0.48.5",
+ "windows_i686_gnu 0.48.5",
+ "windows_i686_msvc 0.48.5",
+ "windows_x86_64_gnu 0.48.5",
+ "windows_x86_64_gnullvm 0.48.5",
+ "windows_x86_64_msvc 0.48.5",
+]
+
+[[package]]
+name = "windows-targets"
+version = "0.52.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6f0713a46559409d202e70e28227288446bf7841d3211583a4b53e3f6d96e7eb"
+dependencies = [
+ "windows_aarch64_gnullvm 0.52.5",
+ "windows_aarch64_msvc 0.52.5",
+ "windows_i686_gnu 0.52.5",
+ "windows_i686_gnullvm",
+ "windows_i686_msvc 0.52.5",
+ "windows_x86_64_gnu 0.52.5",
+ "windows_x86_64_gnullvm 0.52.5",
+ "windows_x86_64_msvc 0.52.5",
+]
+
+[[package]]
+name = "windows_aarch64_gnullvm"
+version = "0.48.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8"
+
+[[package]]
+name = "windows_aarch64_gnullvm"
+version = "0.52.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7088eed71e8b8dda258ecc8bac5fb1153c5cffaf2578fc8ff5d61e23578d3263"
+
+[[package]]
+name = "windows_aarch64_msvc"
+version = "0.48.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc"
+
+[[package]]
+name = "windows_aarch64_msvc"
+version = "0.52.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9985fd1504e250c615ca5f281c3f7a6da76213ebd5ccc9561496568a2752afb6"
+
+[[package]]
+name = "windows_i686_gnu"
+version = "0.48.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e"
+
+[[package]]
+name = "windows_i686_gnu"
+version = "0.52.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "88ba073cf16d5372720ec942a8ccbf61626074c6d4dd2e745299726ce8b89670"
+
+[[package]]
+name = "windows_i686_gnullvm"
+version = "0.52.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "87f4261229030a858f36b459e748ae97545d6f1ec60e5e0d6a3d32e0dc232ee9"
+
+[[package]]
+name = "windows_i686_msvc"
+version = "0.48.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406"
+
+[[package]]
+name = "windows_i686_msvc"
+version = "0.52.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "db3c2bf3d13d5b658be73463284eaf12830ac9a26a90c717b7f771dfe97487bf"
+
+[[package]]
+name = "windows_x86_64_gnu"
+version = "0.48.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e"
+
+[[package]]
+name = "windows_x86_64_gnu"
+version = "0.52.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4e4246f76bdeff09eb48875a0fd3e2af6aada79d409d33011886d3e1581517d9"
+
+[[package]]
+name = "windows_x86_64_gnullvm"
+version = "0.48.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc"
+
+[[package]]
+name = "windows_x86_64_gnullvm"
+version = "0.52.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "852298e482cd67c356ddd9570386e2862b5673c85bd5f88df9ab6802b334c596"
+
+[[package]]
+name = "windows_x86_64_msvc"
+version = "0.48.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538"
+
+[[package]]
+name = "windows_x86_64_msvc"
+version = "0.52.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bec47e5bfd1bff0eeaf6d8b485cc1074891a197ab4225d504cb7a1ab88b02bf0"
+
+[[package]]
+name = "winreg"
+version = "0.52.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a277a57398d4bfa075df44f501a17cfdf8542d224f0d36095a2adc7aee4ef0a5"
+dependencies = [
+ "cfg-if",
+ "windows-sys 0.48.0",
+]
diff --git a/pkgs/sources/generate_moz_extension/Cargo.toml b/pkgs/sources/generate_moz_extension/Cargo.toml
new file mode 100644
index 00000000..e7d44db4
--- /dev/null
+++ b/pkgs/sources/generate_moz_extension/Cargo.toml
@@ -0,0 +1,14 @@
+[package]
+name = "generate_extensions"
+version = "0.1.0"
+edition = "2021"
+
+# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
+
+[dependencies]
+anyhow = "1.0.83"
+futures = "0.3.30"
+reqwest = "0.12.4"
+serde = { version = "1.0.201", features = ["derive"] }
+serde_json = "1.0.117"
+tokio = { version = "1.37.0", features = ["macros", "rt-multi-thread"] }
diff --git a/pkgs/sources/generate_moz_extension/default.nix b/pkgs/sources/generate_moz_extension/default.nix
new file mode 100644
index 00000000..be734eee
--- /dev/null
+++ b/pkgs/sources/generate_moz_extension/default.nix
@@ -0,0 +1,16 @@
+[
+  (
+    final: prev: {
+      generate_firefox_extensions = import ./generate_firefox_extensions.nix {
+        inherit
+          (prev)
+          rustPlatform
+          # Dependencies
+          
+          openssl
+          pkg-config
+          ;
+      };
+    }
+  )
+]
diff --git a/pkgs/sources/generate_moz_extension/examples/generate_extensions.sh b/pkgs/sources/generate_moz_extension/examples/generate_extensions.sh
new file mode 100755
index 00000000..96802992
--- /dev/null
+++ b/pkgs/sources/generate_moz_extension/examples/generate_extensions.sh
@@ -0,0 +1,17 @@
+#!/bin/sh
+
+tmp=$(mktemp)
+cat <<EOF | awk '!/^\s*#/' >"$tmp"
+    darkreader:navbar
+    keepassxc-browser:navbar
+    vhack-libredirect:navbar
+    # torproject-snowflake:navbar
+    tridactyl-vim:menupanel
+    ublock-origin:menupanel
+EOF
+
+# The cat execution should be unquoted;
+# shellcheck disable=SC2046
+cargo run -- $(cat "$tmp")
+
+rm "$tmp"
diff --git a/pkgs/sources/generate_moz_extension/flake.lock b/pkgs/sources/generate_moz_extension/flake.lock
new file mode 100644
index 00000000..741a8ad1
--- /dev/null
+++ b/pkgs/sources/generate_moz_extension/flake.lock
@@ -0,0 +1,106 @@
+{
+  "nodes": {
+    "crane": {
+      "inputs": {
+        "nixpkgs": [
+          "nixpkgs"
+        ]
+      },
+      "locked": {
+        "lastModified": 1714864355,
+        "narHash": "sha256-uXNW6bapWFfkYIkK1EagydSrFMqycOYEDSq75GmUpjk=",
+        "owner": "ipetkov",
+        "repo": "crane",
+        "rev": "442a7a6152f49b907e73206dc8e1f46a61e8e873",
+        "type": "github"
+      },
+      "original": {
+        "owner": "ipetkov",
+        "repo": "crane",
+        "type": "github"
+      }
+    },
+    "flake-utils": {
+      "inputs": {
+        "systems": "systems"
+      },
+      "locked": {
+        "lastModified": 1710146030,
+        "narHash": "sha256-SZ5L6eA7HJ/nmkzGG7/ISclqe6oZdOZTNoesiInkXPQ=",
+        "owner": "numtide",
+        "repo": "flake-utils",
+        "rev": "b1d9ab70662946ef0850d488da1c9019f3a9752a",
+        "type": "github"
+      },
+      "original": {
+        "owner": "numtide",
+        "repo": "flake-utils",
+        "type": "github"
+      }
+    },
+    "nixpkgs": {
+      "locked": {
+        "lastModified": 1715037484,
+        "narHash": "sha256-OUt8xQFmBU96Hmm4T9tOWTu4oCswCzoVl+pxSq/kiFc=",
+        "owner": "NixOS",
+        "repo": "nixpkgs",
+        "rev": "ad7efee13e0d216bf29992311536fce1d3eefbef",
+        "type": "github"
+      },
+      "original": {
+        "owner": "NixOS",
+        "ref": "nixpkgs-unstable",
+        "repo": "nixpkgs",
+        "type": "github"
+      }
+    },
+    "root": {
+      "inputs": {
+        "crane": "crane",
+        "flake-utils": "flake-utils",
+        "nixpkgs": "nixpkgs",
+        "rust-overlay": "rust-overlay"
+      }
+    },
+    "rust-overlay": {
+      "inputs": {
+        "flake-utils": [
+          "flake-utils"
+        ],
+        "nixpkgs": [
+          "nixpkgs"
+        ]
+      },
+      "locked": {
+        "lastModified": 1715221036,
+        "narHash": "sha256-81EKOdlmT/4hZpImRlvMVPgmCcJYZjwlWbJese/XqUw=",
+        "owner": "oxalica",
+        "repo": "rust-overlay",
+        "rev": "5c4bc8a0a70093a31a12509c5653c147f2310bd2",
+        "type": "github"
+      },
+      "original": {
+        "owner": "oxalica",
+        "repo": "rust-overlay",
+        "type": "github"
+      }
+    },
+    "systems": {
+      "locked": {
+        "lastModified": 1681028828,
+        "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
+        "owner": "nix-systems",
+        "repo": "default",
+        "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
+        "type": "github"
+      },
+      "original": {
+        "owner": "nix-systems",
+        "repo": "default",
+        "type": "github"
+      }
+    }
+  },
+  "root": "root",
+  "version": 7
+}
diff --git a/pkgs/sources/generate_moz_extension/flake.nix b/pkgs/sources/generate_moz_extension/flake.nix
new file mode 100644
index 00000000..5575f90b
--- /dev/null
+++ b/pkgs/sources/generate_moz_extension/flake.nix
@@ -0,0 +1,75 @@
+{
+  description = "A simple way to query the mozialla api for extension data";
+
+  inputs = {
+    nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable";
+
+    crane = {
+      url = "github:ipetkov/crane";
+      inputs = {
+        nixpkgs.follows = "nixpkgs";
+      };
+    };
+
+    flake-utils.url = "github:numtide/flake-utils";
+
+    rust-overlay = {
+      url = "github:oxalica/rust-overlay";
+      inputs = {
+        nixpkgs.follows = "nixpkgs";
+        flake-utils.follows = "flake-utils";
+      };
+    };
+  };
+
+  outputs = {
+    self,
+    nixpkgs,
+    crane,
+    flake-utils,
+    rust-overlay,
+    ...
+  }:
+    flake-utils.lib.eachDefaultSystem (system: let
+      pkgs = import nixpkgs {
+        inherit system;
+        overlays = [(import rust-overlay)];
+      };
+
+      rust-stable = pkgs.rust-bin.stable.latest.default;
+      rust-minimal = pkgs.rust-bin.stable.latest.minimal;
+
+      craneLib = (crane.mkLib pkgs).overrideToolchain rust-minimal;
+
+      buildInputs = [
+        pkgs.openssl # needed for openssl
+      ];
+      nativeBuildInputs = [
+        pkgs.pkg-config # needed for openssl
+      ];
+
+      craneBuild = craneLib.buildPackage {
+        src = craneLib.cleanCargoSource ./.;
+        inherit buildInputs nativeBuildInputs;
+
+        doCheck = true;
+      };
+    in {
+      packages.default = craneBuild;
+      app.default = {
+        type = "app";
+        program = "${self.packages.${system}.default}/bin/generate_extensions";
+      };
+      devShells.default = pkgs.mkShell {
+        packages = with pkgs; [
+          cocogitto
+
+          rust-stable
+          cargo-edit
+        ];
+        inherit buildInputs nativeBuildInputs;
+      };
+    });
+}
+# vim: ts=2
+
diff --git a/pkgs/sources/generate_moz_extension/generate_firefox_extensions.nix b/pkgs/sources/generate_moz_extension/generate_firefox_extensions.nix
new file mode 100644
index 00000000..abd95c77
--- /dev/null
+++ b/pkgs/sources/generate_moz_extension/generate_firefox_extensions.nix
@@ -0,0 +1,20 @@
+{
+  rustPlatform,
+  openssl,
+  pkg-config,
+}:
+rustPlatform.buildRustPackage {
+  pname = "generate_firefox_extensions";
+  version = "0.1.0";
+
+  src = ./.;
+  cargoLock = {
+    lockFile = ./Cargo.lock;
+  };
+  buildInputs = [
+    openssl # needed for openssl-sys crate
+  ];
+  nativeBuildInputs = [
+    pkg-config # needed for openssl dependency
+  ];
+}
diff --git a/pkgs/sources/generate_moz_extension/res/generate_extensions.py b/pkgs/sources/generate_moz_extension/res/generate_extensions.py
new file mode 100644
index 00000000..ee8cc966
--- /dev/null
+++ b/pkgs/sources/generate_moz_extension/res/generate_extensions.py
@@ -0,0 +1,44 @@
+#!/usr/bin/env python
+# source: https://github.com/etu/nixconfig/blob/ba47d577c8bfb4a1c06927c34ece34118f4a0460/modules/graphical/firefox/generate.py
+
+from concurrent.futures import ThreadPoolExecutor
+import json
+import requests
+
+EXTENSIONS = sorted(
+    [
+        "darkreader",
+        "firenvim",
+        "keepassxc-browser",
+        "simple-tab-groups",
+    ]
+)
+
+
+def index_ext(ext: str):
+    # print(f"Indexing {ext}...")
+
+    resp = requests.get(f"https://addons.mozilla.org/api/v5/addons/addon/{ext}/").json()
+    rel = resp["current_version"]
+
+    if not rel["file"]["hash"].startswith("sha256:"):
+        raise ValueError("Unhandled hash type")
+
+    return {
+        "pname": ext,
+        "version": rel["version"],
+        "addonId": resp["guid"],
+        "url": rel["file"]["url"],
+        "sha256": rel["file"]["hash"],
+    }
+
+
+if __name__ == "__main__":
+    # outfile = os.path.dirname(os.path.realpath(__file__)) + "/extensions.json"
+
+    with ThreadPoolExecutor() as e:
+        extensions = {ext: e.submit(index_ext, ext) for ext in EXTENSIONS}
+        extensions = {k: v.result() for k, v in extensions.items()}
+
+    # with open(outfile, "w") as f:
+    print(json.dumps(extensions, indent=2))
diff --git a/pkgs/sources/generate_moz_extension/res/reference.json b/pkgs/sources/generate_moz_extension/res/reference.json
new file mode 100644
index 00000000..f46ea8ec
--- /dev/null
+++ b/pkgs/sources/generate_moz_extension/res/reference.json
@@ -0,0 +1,30 @@
+{
+  "darkreader": {
+    "pname": "darkreader",
+    "version": "4.9.62",
+    "addonId": "addon@darkreader.org",
+    "url": "https://addons.mozilla.org/firefox/downloads/file/4053589/darkreader-4.9.62.xpi",
+    "sha256": "sha256:e537a2cee45ed7c26f79ecd3ed362620e3f00d24c158532a58e163a63a3d60cc"
+  },
+  "firenvim": {
+    "pname": "firenvim",
+    "version": "0.2.14",
+    "addonId": "firenvim@lacamb.re",
+    "url": "https://addons.mozilla.org/firefox/downloads/file/4026386/firenvim-0.2.14.xpi",
+    "sha256": "sha256:a8c495a59e30eaabbb3fcd188db9b5e28b40bffefe41a3f0fa22ecc58c80c2b6"
+  },
+  "keepassxc-browser": {
+    "pname": "keepassxc-browser",
+    "version": "1.8.4",
+    "addonId": "keepassxc-browser@keepassxc.org",
+    "url": "https://addons.mozilla.org/firefox/downloads/file/4045866/keepassxc_browser-1.8.4.xpi",
+    "sha256": "sha256:cc39aa058cb8915cfc88424e2e1cebe3ccfc3f95d7bddb2abd0c4905d2b17719"
+  },
+  "simple-tab-groups": {
+    "pname": "simple-tab-groups",
+    "version": "4.7.2.1",
+    "addonId": "simple-tab-groups@drive4ik",
+    "url": "https://addons.mozilla.org/firefox/downloads/file/3873608/simple_tab_groups-4.7.2.1.xpi",
+    "sha256": "sha256:75077589098ca62c00b86cf9554c6120bf8dc04c5f916fe26f84915f5147b2a4"
+  }
+}
diff --git a/pkgs/sources/generate_moz_extension/res/test.json b/pkgs/sources/generate_moz_extension/res/test.json
new file mode 100644
index 00000000..daa1d19a
--- /dev/null
+++ b/pkgs/sources/generate_moz_extension/res/test.json
@@ -0,0 +1,30 @@
+{
+  "darkreader": {
+    "addon_id": "addon@darkreader.org",
+    "pname": "darkreader",
+    "sha256": "sha256:e537a2cee45ed7c26f79ecd3ed362620e3f00d24c158532a58e163a63a3d60cc",
+    "url": "https://addons.mozilla.org/firefox/downloads/file/4053589/darkreader-4.9.62.xpi",
+    "version": "4.9.62"
+  },
+  "firenvim": {
+    "addon_id": "firenvim@lacamb.re",
+    "pname": "firenvim",
+    "sha256": "sha256:a8c495a59e30eaabbb3fcd188db9b5e28b40bffefe41a3f0fa22ecc58c80c2b6",
+    "url": "https://addons.mozilla.org/firefox/downloads/file/4026386/firenvim-0.2.14.xpi",
+    "version": "0.2.14"
+  },
+  "keepassxc-browser": {
+    "addon_id": "keepassxc-browser@keepassxc.org",
+    "pname": "keepassxc-browser",
+    "sha256": "sha256:cc39aa058cb8915cfc88424e2e1cebe3ccfc3f95d7bddb2abd0c4905d2b17719",
+    "url": "https://addons.mozilla.org/firefox/downloads/file/4045866/keepassxc_browser-1.8.4.xpi",
+    "version": "1.8.4"
+  },
+  "simple-tab-groups": {
+    "addon_id": "simple-tab-groups@drive4ik",
+    "pname": "simple-tab-groups",
+    "sha256": "sha256:75077589098ca62c00b86cf9554c6120bf8dc04c5f916fe26f84915f5147b2a4",
+    "url": "https://addons.mozilla.org/firefox/downloads/file/3873608/simple_tab_groups-4.7.2.1.xpi",
+    "version": "4.7.2.1"
+  }
+}
diff --git a/pkgs/sources/generate_moz_extension/src/main.rs b/pkgs/sources/generate_moz_extension/src/main.rs
new file mode 100644
index 00000000..bde986a3
--- /dev/null
+++ b/pkgs/sources/generate_moz_extension/src/main.rs
@@ -0,0 +1,138 @@
+use std::env::args;
+
+use anyhow::{bail, Context};
+use futures::StreamExt;
+use reqwest::Client;
+use serde_json::{json, Map, Value};
+
+pub mod types;
+
+macro_rules! get_json_value {
+    ($key:expr, $json_value:ident, $type:ident, $get:ident) => {
+        match $json_value.get($key) {
+            Some(resp) => {
+                let resp = resp.to_owned();
+                if resp.$type() {
+                    resp.$get().expect(
+                        "The should have been checked in the if guard, so unpacking here is fine",
+                    ).to_owned()
+                } else {
+                    bail!(
+                        "Value {} => \n{}\n is not of type: {}",
+                        $key,
+                        resp,
+                        stringify!($type)
+                    );
+                }
+            }
+            None => {
+                bail!(
+                    "There seems to be no '{}' in your json data (json value: '{}')\n Has the api changend?",
+                    $key, serde_json::to_string_pretty(&$json_value).expect("Will always work")
+                );
+            }
+        }
+    };
+}
+
+use futures::stream::futures_unordered::FuturesUnordered;
+use types::{Extension, InputExtension};
+
+#[tokio::main]
+async fn main() -> anyhow::Result<()> {
+    let mut extensions: Vec<InputExtension> = vec![];
+    for input_extension in args()
+        .skip(1)
+        .map(|str| InputExtension::try_from(str))
+        .collect::<Vec<anyhow::Result<InputExtension>>>()
+    {
+        extensions.push(input_extension?);
+    }
+
+    let resulting_extensions = process_extensions(extensions).await?;
+
+    let mut output = Map::new();
+    for extension in resulting_extensions {
+        output.insert(extension.pname.clone(), json!(extension));
+    }
+
+    println!(
+        "{}",
+        serde_json::to_string_pretty(&serde_json::Value::Object(output)).expect(
+            "This is constructed from json, it should also be possible to serialize it again"
+        )
+    );
+    Ok(())
+}
+
+async fn process_extensions(extensions: Vec<InputExtension>) -> anyhow::Result<Vec<Extension>> {
+    let mut output = Vec::with_capacity(extensions.len());
+
+    let client = Client::new();
+    for extension in extensions
+        .iter()
+        .map(|ext| {
+            let local_client = &client;
+            index_extension(ext, local_client)
+        })
+        .collect::<FuturesUnordered<_>>()
+        .collect::<Vec<_>>()
+        .await
+    {
+        output.push(extension?);
+    }
+    Ok(output)
+}
+
+async fn index_extension(extension: &InputExtension, client: &Client) -> anyhow::Result<Extension> {
+    let response = client
+        .get(format!(
+            "https://addons.mozilla.org/api/v5/addons/addon/{}",
+            extension,
+        ))
+        .send()
+        .await
+        .context("Accessing the mozzila extenios api failed with error: {e}")?;
+
+    eprintln!("Indexing {} ({})...", extension, response.status());
+    let response: Value = serde_json::from_str(
+        &response
+            .text()
+            .await
+            .context("Turning the response to text fail with error: {e}")?,
+    )
+    .context("Deserializing the response failed! Error: {e}")?;
+
+    if let Some(detail) = response.get("detail") {
+        if detail == "Not found." {
+            bail!("Your extension ('{}') was not found!", extension);
+        }
+    };
+
+    let release = { get_json_value!("current_version", response, is_object, as_object) };
+
+    #[allow(non_snake_case)]
+    let addonId = { get_json_value!("guid", response, is_string, as_str) };
+
+    let version = { get_json_value!("version", release, is_string, as_str) };
+    let file = { get_json_value!("file", release, is_object, as_object) };
+
+    let url = { get_json_value!("url", file, is_string, as_str) };
+    let sha256 = {
+        let hash = get_json_value!("hash", file, is_string, as_str);
+        if hash.starts_with("sha256:") {
+            hash
+        } else {
+            bail!("This hash type is unhandled: {}", hash);
+        }
+    };
+
+    Ok(Extension {
+        pname: extension.moz_name.clone(),
+        default_area: extension.default_area,
+        version,
+        addonId,
+        url,
+        sha256,
+    })
+}
diff --git a/pkgs/sources/generate_moz_extension/src/types.rs b/pkgs/sources/generate_moz_extension/src/types.rs
new file mode 100644
index 00000000..b830fe0d
--- /dev/null
+++ b/pkgs/sources/generate_moz_extension/src/types.rs
@@ -0,0 +1,71 @@
+use std::fmt::Display;
+
+use anyhow::anyhow;
+use serde::{Deserialize, Serialize};
+
+#[derive(Debug, Serialize, Deserialize)]
+#[allow(non_snake_case)]
+pub struct Extension {
+    pub pname: String,
+    pub default_area: DefaultArea,
+    pub version: String,
+    pub addonId: String,
+    pub url: String,
+    pub sha256: String,
+}
+
+#[derive(Debug, Clone)]
+pub struct InputExtension {
+    pub moz_name: String,
+    pub default_area: DefaultArea,
+}
+#[derive(Debug, Clone, Copy, Deserialize, Serialize)]
+#[allow(non_camel_case_types)]
+pub enum DefaultArea {
+    navbar,
+    menupanel,
+}
+
+impl Display for DefaultArea {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        match self {
+            DefaultArea::navbar => f.write_str("navbar"),
+            DefaultArea::menupanel => f.write_str("menupanel"),
+        }
+    }
+}
+
+impl TryFrom<&str> for DefaultArea {
+    type Error = anyhow::Error;
+
+    fn try_from(value: &str) -> Result<Self, Self::Error> {
+        match value {
+            "navbar" => Ok(Self::navbar),
+            "menupanel" => Ok(Self::menupanel),
+            _ => Err(anyhow!(
+                "Your <default_area> needs to be one of 'navbar' or 'menupanel', but is: '{}'",
+                value
+            )),
+        }
+    }
+}
+
+impl Display for InputExtension {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        f.write_str(&self.moz_name)
+    }
+}
+impl TryFrom<String> for InputExtension {
+    type Error = anyhow::Error;
+
+    fn try_from(value: String) -> Result<Self, Self::Error> {
+        if let Some((moz_name, default_area)) = value.split_once(':') {
+            Ok(Self {
+                moz_name: moz_name.to_owned(),
+                default_area: default_area.try_into()?,
+            })
+        } else {
+            Err(anyhow!("Can't parse the input string as a InputExtension!\n Needs to be: '<moz_name>:<default_area>'"))
+        }
+    }
+}
diff --git a/pkgs/sources/generate_moz_extension/update.sh b/pkgs/sources/generate_moz_extension/update.sh
new file mode 100755
index 00000000..e500bb23
--- /dev/null
+++ b/pkgs/sources/generate_moz_extension/update.sh
@@ -0,0 +1,8 @@
+#!/usr/bin/env sh
+
+nix flake update
+
+[ "$1" = "upgrade" ] && cargo upgrade
+cargo update
+
+# vim: ft=sh
diff --git a/pkgs/sources/lf-make-map/.envrc b/pkgs/sources/lf-make-map/.envrc
new file mode 100644
index 00000000..c8c56659
--- /dev/null
+++ b/pkgs/sources/lf-make-map/.envrc
@@ -0,0 +1,11 @@
+use flake || use nix
+watch_file flake.nix
+
+PATH_add ./target/debug
+PATH_add ./target/release
+PATH_add ./scripts
+
+if on_git_branch; then
+  echo && git status --short --branch &&
+  echo && git fetch --verbose
+fi
diff --git a/pkgs/sources/lf-make-map/.gitignore b/pkgs/sources/lf-make-map/.gitignore
new file mode 100644
index 00000000..cb87f36f
--- /dev/null
+++ b/pkgs/sources/lf-make-map/.gitignore
@@ -0,0 +1,6 @@
+# build
+/target
+/result
+
+# dev env
+.direnv
diff --git a/pkgs/sources/lf-make-map/Cargo.lock b/pkgs/sources/lf-make-map/Cargo.lock
new file mode 100644
index 00000000..16af6e03
--- /dev/null
+++ b/pkgs/sources/lf-make-map/Cargo.lock
@@ -0,0 +1,505 @@
+# This file is automatically @generated by Cargo.
+# It is not intended for manual editing.
+version = 3
+
+[[package]]
+name = "android-tzdata"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0"
+
+[[package]]
+name = "android_system_properties"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311"
+dependencies = [
+ "libc",
+]
+
+[[package]]
+name = "anstream"
+version = "0.6.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "418c75fa768af9c03be99d17643f93f79bbba589895012a80e3452a19ddda15b"
+dependencies = [
+ "anstyle",
+ "anstyle-parse",
+ "anstyle-query",
+ "anstyle-wincon",
+ "colorchoice",
+ "is_terminal_polyfill",
+ "utf8parse",
+]
+
+[[package]]
+name = "anstyle"
+version = "1.0.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "038dfcf04a5feb68e9c60b21c9625a54c2c0616e79b72b0fd87075a056ae1d1b"
+
+[[package]]
+name = "anstyle-parse"
+version = "0.2.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c03a11a9034d92058ceb6ee011ce58af4a9bf61491aa7e1e59ecd24bd40d22d4"
+dependencies = [
+ "utf8parse",
+]
+
+[[package]]
+name = "anstyle-query"
+version = "1.0.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a64c907d4e79225ac72e2a354c9ce84d50ebb4586dee56c82b3ee73004f537f5"
+dependencies = [
+ "windows-sys",
+]
+
+[[package]]
+name = "anstyle-wincon"
+version = "3.0.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "61a38449feb7068f52bb06c12759005cf459ee52bb4adc1d5a7c4322d716fb19"
+dependencies = [
+ "anstyle",
+ "windows-sys",
+]
+
+[[package]]
+name = "anyhow"
+version = "1.0.83"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "25bdb32cbbdce2b519a9cd7df3a678443100e265d5e25ca763b7572a5104f5f3"
+
+[[package]]
+name = "autocfg"
+version = "1.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0c4b4d0bd25bd0b74681c0ad21497610ce1b7c91b1022cd21c80c6fbdd9476b0"
+
+[[package]]
+name = "bumpalo"
+version = "3.16.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c"
+
+[[package]]
+name = "cc"
+version = "1.0.97"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "099a5357d84c4c61eb35fc8eafa9a79a902c2f76911e5747ced4e032edd8d9b4"
+
+[[package]]
+name = "cfg-if"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
+
+[[package]]
+name = "chrono"
+version = "0.4.38"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a21f936df1771bf62b77f047b726c4625ff2e8aa607c01ec06e5a05bd8463401"
+dependencies = [
+ "android-tzdata",
+ "iana-time-zone",
+ "js-sys",
+ "num-traits",
+ "wasm-bindgen",
+ "windows-targets",
+]
+
+[[package]]
+name = "clap"
+version = "4.5.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "90bc066a67923782aa8515dbaea16946c5bcc5addbd668bb80af688e53e548a0"
+dependencies = [
+ "clap_builder",
+ "clap_derive",
+]
+
+[[package]]
+name = "clap_builder"
+version = "4.5.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ae129e2e766ae0ec03484e609954119f123cc1fe650337e155d03b022f24f7b4"
+dependencies = [
+ "anstream",
+ "anstyle",
+ "clap_lex",
+ "strsim",
+]
+
+[[package]]
+name = "clap_derive"
+version = "4.5.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "528131438037fd55894f62d6e9f068b8f45ac57ffa77517819645d10aed04f64"
+dependencies = [
+ "heck",
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "clap_lex"
+version = "0.7.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "98cc8fbded0c607b7ba9dd60cd98df59af97e84d24e49c8557331cfc26d301ce"
+
+[[package]]
+name = "colorchoice"
+version = "1.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0b6a852b24ab71dffc585bcb46eaf7959d175cb865a7152e35b348d1b2960422"
+
+[[package]]
+name = "core-foundation-sys"
+version = "0.8.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "06ea2b9bc92be3c2baa9334a323ebca2d6f074ff852cd1d7b11064035cd3868f"
+
+[[package]]
+name = "heck"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
+
+[[package]]
+name = "hermit-abi"
+version = "0.3.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024"
+
+[[package]]
+name = "iana-time-zone"
+version = "0.1.60"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e7ffbb5a1b541ea2561f8c41c087286cc091e21e556a4f09a8f6cbf17b69b141"
+dependencies = [
+ "android_system_properties",
+ "core-foundation-sys",
+ "iana-time-zone-haiku",
+ "js-sys",
+ "wasm-bindgen",
+ "windows-core",
+]
+
+[[package]]
+name = "iana-time-zone-haiku"
+version = "0.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f"
+dependencies = [
+ "cc",
+]
+
+[[package]]
+name = "is-terminal"
+version = "0.4.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f23ff5ef2b80d608d61efee834934d862cd92461afc0560dedf493e4c033738b"
+dependencies = [
+ "hermit-abi",
+ "libc",
+ "windows-sys",
+]
+
+[[package]]
+name = "is_terminal_polyfill"
+version = "1.70.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f8478577c03552c21db0e2724ffb8986a5ce7af88107e6be5d2ee6e158c12800"
+
+[[package]]
+name = "js-sys"
+version = "0.3.69"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "29c15563dc2726973df627357ce0c9ddddbea194836909d655df6a75d2cf296d"
+dependencies = [
+ "wasm-bindgen",
+]
+
+[[package]]
+name = "lf-make-map"
+version = "0.1.0"
+dependencies = [
+ "anyhow",
+ "clap",
+ "log",
+ "stderrlog",
+ "walkdir",
+]
+
+[[package]]
+name = "libc"
+version = "0.2.154"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ae743338b92ff9146ce83992f766a31066a91a8c84a45e0e9f21e7cf6de6d346"
+
+[[package]]
+name = "log"
+version = "0.4.21"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "90ed8c1e510134f979dbc4f070f87d4313098b704861a105fe34231c70a3901c"
+
+[[package]]
+name = "num-traits"
+version = "0.2.19"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841"
+dependencies = [
+ "autocfg",
+]
+
+[[package]]
+name = "once_cell"
+version = "1.19.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92"
+
+[[package]]
+name = "proc-macro2"
+version = "1.0.82"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8ad3d49ab951a01fbaafe34f2ec74122942fe18a3f9814c3268f1bb72042131b"
+dependencies = [
+ "unicode-ident",
+]
+
+[[package]]
+name = "quote"
+version = "1.0.36"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0fa76aaf39101c457836aec0ce2316dbdc3ab723cdda1c6bd4e6ad4208acaca7"
+dependencies = [
+ "proc-macro2",
+]
+
+[[package]]
+name = "same-file"
+version = "1.0.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502"
+dependencies = [
+ "winapi-util",
+]
+
+[[package]]
+name = "stderrlog"
+version = "0.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "61c910772f992ab17d32d6760e167d2353f4130ed50e796752689556af07dc6b"
+dependencies = [
+ "chrono",
+ "is-terminal",
+ "log",
+ "termcolor",
+ "thread_local",
+]
+
+[[package]]
+name = "strsim"
+version = "0.11.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
+
+[[package]]
+name = "syn"
+version = "2.0.61"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c993ed8ccba56ae856363b1845da7266a7cb78e1d146c8a32d54b45a8b831fc9"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "unicode-ident",
+]
+
+[[package]]
+name = "termcolor"
+version = "1.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bab24d30b911b2376f3a13cc2cd443142f0c81dda04c118693e35b3835757755"
+dependencies = [
+ "winapi-util",
+]
+
+[[package]]
+name = "thread_local"
+version = "1.1.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8b9ef9bad013ada3808854ceac7b46812a6465ba368859a37e2100283d2d719c"
+dependencies = [
+ "cfg-if",
+ "once_cell",
+]
+
+[[package]]
+name = "unicode-ident"
+version = "1.0.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b"
+
+[[package]]
+name = "utf8parse"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a"
+
+[[package]]
+name = "walkdir"
+version = "2.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b"
+dependencies = [
+ "same-file",
+ "winapi-util",
+]
+
+[[package]]
+name = "wasm-bindgen"
+version = "0.2.92"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4be2531df63900aeb2bca0daaaddec08491ee64ceecbee5076636a3b026795a8"
+dependencies = [
+ "cfg-if",
+ "wasm-bindgen-macro",
+]
+
+[[package]]
+name = "wasm-bindgen-backend"
+version = "0.2.92"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "614d787b966d3989fa7bb98a654e369c762374fd3213d212cfc0251257e747da"
+dependencies = [
+ "bumpalo",
+ "log",
+ "once_cell",
+ "proc-macro2",
+ "quote",
+ "syn",
+ "wasm-bindgen-shared",
+]
+
+[[package]]
+name = "wasm-bindgen-macro"
+version = "0.2.92"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a1f8823de937b71b9460c0c34e25f3da88250760bec0ebac694b49997550d726"
+dependencies = [
+ "quote",
+ "wasm-bindgen-macro-support",
+]
+
+[[package]]
+name = "wasm-bindgen-macro-support"
+version = "0.2.92"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e94f17b526d0a461a191c78ea52bbce64071ed5c04c9ffe424dcb38f74171bb7"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+ "wasm-bindgen-backend",
+ "wasm-bindgen-shared",
+]
+
+[[package]]
+name = "wasm-bindgen-shared"
+version = "0.2.92"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "af190c94f2773fdb3729c55b007a722abb5384da03bc0986df4c289bf5567e96"
+
+[[package]]
+name = "winapi-util"
+version = "0.1.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4d4cc384e1e73b93bafa6fb4f1df8c41695c8a91cf9c4c64358067d15a7b6c6b"
+dependencies = [
+ "windows-sys",
+]
+
+[[package]]
+name = "windows-core"
+version = "0.52.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9"
+dependencies = [
+ "windows-targets",
+]
+
+[[package]]
+name = "windows-sys"
+version = "0.52.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d"
+dependencies = [
+ "windows-targets",
+]
+
+[[package]]
+name = "windows-targets"
+version = "0.52.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6f0713a46559409d202e70e28227288446bf7841d3211583a4b53e3f6d96e7eb"
+dependencies = [
+ "windows_aarch64_gnullvm",
+ "windows_aarch64_msvc",
+ "windows_i686_gnu",
+ "windows_i686_gnullvm",
+ "windows_i686_msvc",
+ "windows_x86_64_gnu",
+ "windows_x86_64_gnullvm",
+ "windows_x86_64_msvc",
+]
+
+[[package]]
+name = "windows_aarch64_gnullvm"
+version = "0.52.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7088eed71e8b8dda258ecc8bac5fb1153c5cffaf2578fc8ff5d61e23578d3263"
+
+[[package]]
+name = "windows_aarch64_msvc"
+version = "0.52.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9985fd1504e250c615ca5f281c3f7a6da76213ebd5ccc9561496568a2752afb6"
+
+[[package]]
+name = "windows_i686_gnu"
+version = "0.52.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "88ba073cf16d5372720ec942a8ccbf61626074c6d4dd2e745299726ce8b89670"
+
+[[package]]
+name = "windows_i686_gnullvm"
+version = "0.52.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "87f4261229030a858f36b459e748ae97545d6f1ec60e5e0d6a3d32e0dc232ee9"
+
+[[package]]
+name = "windows_i686_msvc"
+version = "0.52.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "db3c2bf3d13d5b658be73463284eaf12830ac9a26a90c717b7f771dfe97487bf"
+
+[[package]]
+name = "windows_x86_64_gnu"
+version = "0.52.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4e4246f76bdeff09eb48875a0fd3e2af6aada79d409d33011886d3e1581517d9"
+
+[[package]]
+name = "windows_x86_64_gnullvm"
+version = "0.52.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "852298e482cd67c356ddd9570386e2862b5673c85bd5f88df9ab6802b334c596"
+
+[[package]]
+name = "windows_x86_64_msvc"
+version = "0.52.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bec47e5bfd1bff0eeaf6d8b485cc1074891a197ab4225d504cb7a1ab88b02bf0"
diff --git a/pkgs/sources/lf-make-map/Cargo.toml b/pkgs/sources/lf-make-map/Cargo.toml
new file mode 100644
index 00000000..da9881fd
--- /dev/null
+++ b/pkgs/sources/lf-make-map/Cargo.toml
@@ -0,0 +1,14 @@
+[package]
+name = "lf-make-map"
+description = "An automatic lf cd mapping generator"
+version = "0.1.0"
+edition = "2021"
+
+# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
+
+[dependencies]
+anyhow = "1.0.83"
+clap = { version = "4.5.4", features = ["derive", "env"] }
+log = "0.4.21"
+stderrlog = "0.6.0"
+walkdir = "2.5.0"
diff --git a/pkgs/sources/lf-make-map/README.md b/pkgs/sources/lf-make-map/README.md
new file mode 100644
index 00000000..0c57cede
--- /dev/null
+++ b/pkgs/sources/lf-make-map/README.md
@@ -0,0 +1,12 @@
+# Lf make map
+
+> An automatic lf cd mapping generator
+
+Some text about the project.
+
+## Licence
+
+This program is free software: you can redistribute it and/or modify
+it under the terms of the GNU General Public License as published by
+the Free Software Foundation, either version 3 of the License, or
+(at your option) any later version.
diff --git a/pkgs/sources/lf-make-map/default.nix b/pkgs/sources/lf-make-map/default.nix
new file mode 100644
index 00000000..8ff4c624
--- /dev/null
+++ b/pkgs/sources/lf-make-map/default.nix
@@ -0,0 +1,12 @@
+[
+  (
+    final: prev: {
+      lf-make-map = import ./lf_make_map.nix {
+        inherit
+          (prev)
+          rustPlatform
+          ;
+      };
+    }
+  )
+]
diff --git a/pkgs/sources/lf-make-map/flake.lock b/pkgs/sources/lf-make-map/flake.lock
new file mode 100644
index 00000000..611392df
--- /dev/null
+++ b/pkgs/sources/lf-make-map/flake.lock
@@ -0,0 +1,147 @@
+{
+  "nodes": {
+    "crane": {
+      "inputs": {
+        "nixpkgs": [
+          "nixpkgs"
+        ]
+      },
+      "locked": {
+        "lastModified": 1714864355,
+        "narHash": "sha256-uXNW6bapWFfkYIkK1EagydSrFMqycOYEDSq75GmUpjk=",
+        "owner": "ipetkov",
+        "repo": "crane",
+        "rev": "442a7a6152f49b907e73206dc8e1f46a61e8e873",
+        "type": "github"
+      },
+      "original": {
+        "owner": "ipetkov",
+        "repo": "crane",
+        "type": "github"
+      }
+    },
+    "flake-compat": {
+      "flake": false,
+      "locked": {
+        "lastModified": 1696426674,
+        "narHash": "sha256-kvjfFW7WAETZlt09AgDn1MrtKzP7t90Vf7vypd3OL1U=",
+        "owner": "edolstra",
+        "repo": "flake-compat",
+        "rev": "0f9255e01c2351cc7d116c072cb317785dd33b33",
+        "type": "github"
+      },
+      "original": {
+        "owner": "edolstra",
+        "repo": "flake-compat",
+        "type": "github"
+      }
+    },
+    "flake-utils": {
+      "inputs": {
+        "systems": [
+          "systems"
+        ]
+      },
+      "locked": {
+        "lastModified": 1710146030,
+        "narHash": "sha256-SZ5L6eA7HJ/nmkzGG7/ISclqe6oZdOZTNoesiInkXPQ=",
+        "owner": "numtide",
+        "repo": "flake-utils",
+        "rev": "b1d9ab70662946ef0850d488da1c9019f3a9752a",
+        "type": "github"
+      },
+      "original": {
+        "owner": "numtide",
+        "repo": "flake-utils",
+        "type": "github"
+      }
+    },
+    "nixpkgs": {
+      "locked": {
+        "lastModified": 1715037484,
+        "narHash": "sha256-OUt8xQFmBU96Hmm4T9tOWTu4oCswCzoVl+pxSq/kiFc=",
+        "owner": "NixOS",
+        "repo": "nixpkgs",
+        "rev": "ad7efee13e0d216bf29992311536fce1d3eefbef",
+        "type": "github"
+      },
+      "original": {
+        "owner": "NixOS",
+        "ref": "nixpkgs-unstable",
+        "repo": "nixpkgs",
+        "type": "github"
+      }
+    },
+    "root": {
+      "inputs": {
+        "crane": "crane",
+        "flake-compat": "flake-compat",
+        "flake-utils": "flake-utils",
+        "nixpkgs": "nixpkgs",
+        "rust-overlay": "rust-overlay",
+        "systems": "systems",
+        "treefmt-nix": "treefmt-nix"
+      }
+    },
+    "rust-overlay": {
+      "inputs": {
+        "flake-utils": [
+          "flake-utils"
+        ],
+        "nixpkgs": [
+          "nixpkgs"
+        ]
+      },
+      "locked": {
+        "lastModified": 1715221036,
+        "narHash": "sha256-81EKOdlmT/4hZpImRlvMVPgmCcJYZjwlWbJese/XqUw=",
+        "owner": "oxalica",
+        "repo": "rust-overlay",
+        "rev": "5c4bc8a0a70093a31a12509c5653c147f2310bd2",
+        "type": "github"
+      },
+      "original": {
+        "owner": "oxalica",
+        "repo": "rust-overlay",
+        "type": "github"
+      }
+    },
+    "systems": {
+      "locked": {
+        "lastModified": 1680978846,
+        "narHash": "sha256-Gtqg8b/v49BFDpDetjclCYXm8mAnTrUzR0JnE2nv5aw=",
+        "owner": "nix-systems",
+        "repo": "x86_64-linux",
+        "rev": "2ecfcac5e15790ba6ce360ceccddb15ad16d08a8",
+        "type": "github"
+      },
+      "original": {
+        "owner": "nix-systems",
+        "repo": "x86_64-linux",
+        "type": "github"
+      }
+    },
+    "treefmt-nix": {
+      "inputs": {
+        "nixpkgs": [
+          "nixpkgs"
+        ]
+      },
+      "locked": {
+        "lastModified": 1714058656,
+        "narHash": "sha256-Qv4RBm4LKuO4fNOfx9wl40W2rBbv5u5m+whxRYUMiaA=",
+        "owner": "numtide",
+        "repo": "treefmt-nix",
+        "rev": "c6aaf729f34a36c445618580a9f95a48f5e4e03f",
+        "type": "github"
+      },
+      "original": {
+        "owner": "numtide",
+        "repo": "treefmt-nix",
+        "type": "github"
+      }
+    }
+  },
+  "root": "root",
+  "version": 7
+}
diff --git a/pkgs/sources/lf-make-map/flake.nix b/pkgs/sources/lf-make-map/flake.nix
new file mode 100644
index 00000000..dc8c24cc
--- /dev/null
+++ b/pkgs/sources/lf-make-map/flake.nix
@@ -0,0 +1,125 @@
+{
+  description = "An automatic lf cd mapping generator";
+
+  inputs = {
+    nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable";
+
+    treefmt-nix = {
+      url = "github:numtide/treefmt-nix";
+      inputs = {
+        nixpkgs.follows = "nixpkgs";
+      };
+    };
+
+    crane = {
+      url = "github:ipetkov/crane";
+      inputs = {
+        nixpkgs.follows = "nixpkgs";
+      };
+    };
+    rust-overlay = {
+      url = "github:oxalica/rust-overlay";
+      inputs = {
+        nixpkgs.follows = "nixpkgs";
+        flake-utils.follows = "flake-utils";
+      };
+    };
+
+    # inputs for following
+    systems = {
+      url = "github:nix-systems/x86_64-linux"; # only evaluate for this system
+    };
+    flake-compat = {
+      url = "github:edolstra/flake-compat";
+      flake = false;
+    };
+    flake-utils = {
+      url = "github:numtide/flake-utils";
+      inputs = {
+        systems.follows = "systems";
+      };
+    };
+  };
+
+  outputs = {
+    self,
+    nixpkgs,
+    flake-utils,
+    treefmt-nix,
+    crane,
+    rust-overlay,
+    ...
+  }:
+    flake-utils.lib.eachDefaultSystem (system: let
+      pkgs = import nixpkgs {
+        inherit system;
+        overlays = [(import rust-overlay)];
+      };
+
+      nightly = false;
+      rust_minimal =
+        if nightly
+        then pkgs.rust-bin.selectLatestNightlyWith (toolchain: toolchain.minimal)
+        else pkgs.rust-bin.stable.latest.minimal;
+      rust_default =
+        if nightly
+        then pkgs.rust-bin.selectLatestNightlyWith (toolchain: toolchain.default)
+        else pkgs.rust-bin.stable.latest.default;
+
+      cargo_toml = craneLib.cleanCargoToml {cargoToml = ./Cargo.toml;};
+      pname = cargo_toml.package.name;
+
+      craneLib = (crane.mkLib pkgs).overrideToolchain rust_minimal;
+      craneBuild = craneLib.buildPackage {
+        src = craneLib.cleanCargoSource ./.;
+
+        doCheck = true;
+      };
+
+      manual = pkgs.stdenv.mkDerivation {
+        name = "${pname}-manual";
+        inherit (cargo_toml.package) version;
+
+        src = ./docs;
+        nativeBuildInputs = with pkgs; [pandoc];
+
+        buildPhase = ''
+          mkdir --parents $out/docs;
+
+          pandoc "./${pname}.1.md" -s -t man > $out/docs/${pname}.1
+        '';
+
+        installPhase = ''
+          install -D $out/docs/${pname}.1  $out/share/man/man1/${pname};
+        '';
+      };
+
+      treefmtEval = import ./treefmt.nix {inherit treefmt-nix pkgs;};
+    in {
+      packages.default = pkgs.symlinkJoin {
+        inherit (cargo_toml.package) name;
+
+        paths = [manual craneBuild];
+      };
+
+      checks = {
+        inherit craneBuild;
+        formatting = treefmtEval.config.build.check self;
+      };
+
+      formatter = treefmtEval.config.build.wrapper;
+
+      devShells.default = pkgs.mkShell {
+        packages = with pkgs; [
+          cocogitto
+
+          rust_default
+          cargo-edit
+
+          licensure
+        ];
+      };
+    });
+}
+# vim: ts=2
+
diff --git a/pkgs/sources/lf-make-map/lf_make_map.nix b/pkgs/sources/lf-make-map/lf_make_map.nix
new file mode 100644
index 00000000..afb067b8
--- /dev/null
+++ b/pkgs/sources/lf-make-map/lf_make_map.nix
@@ -0,0 +1,10 @@
+{rustPlatform}:
+rustPlatform.buildRustPackage {
+  pname = "lf-make-map";
+  version = "0.1.0";
+
+  src = ./.;
+  cargoLock = {
+    lockFile = ./Cargo.lock;
+  };
+}
diff --git a/pkgs/sources/lf-make-map/src/cli.rs b/pkgs/sources/lf-make-map/src/cli.rs
new file mode 100644
index 00000000..a398e451
--- /dev/null
+++ b/pkgs/sources/lf-make-map/src/cli.rs
@@ -0,0 +1,49 @@
+use std::path::PathBuf;
+
+use clap::{ArgAction, Parser, Subcommand};
+
+/// An automatic lf cd mapping generator
+#[derive(Parser, Debug)]
+#[clap(author, version, about, long_about = None)]
+#[command(next_line_help = true)]
+pub struct Args {
+    /// The directory to treat as home
+    #[arg(long, short = 'n', env = "HOME")]
+    pub home_name: PathBuf,
+
+    /// The number of directories to generate mappings for, starting from each `relevant_directory`
+    #[arg(long, short, default_value = "2")]
+    pub depth: usize,
+
+    /// Increase message verbosity
+    #[arg(long="verbose", short = 'v', action = ArgAction::Count)]
+    pub verbosity: u8,
+
+    /// Silence all output
+    #[arg(long, short = 'q')]
+    pub quiet: bool,
+
+    #[command(subcommand)]
+    pub command: Command,
+}
+
+#[derive(Subcommand, Debug)]
+pub enum Command {
+    /// Visualize the generated mappings in a tree
+    Visualize {
+        #[command(flatten)]
+        options: CommandOptions,
+    },
+
+    /// Output the generated mappings in a format suitable for the lf config file
+    Generate {
+        #[command(flatten)]
+        options: CommandOptions,
+    },
+}
+
+#[derive(Debug, Parser)]
+pub struct CommandOptions {
+    /// The directories to generate mappings for
+    pub relevant_directories: Vec<PathBuf>,
+}
diff --git a/pkgs/sources/lf-make-map/src/main.rs b/pkgs/sources/lf-make-map/src/main.rs
new file mode 100644
index 00000000..aaf79b20
--- /dev/null
+++ b/pkgs/sources/lf-make-map/src/main.rs
@@ -0,0 +1,229 @@
+use std::path::{Path, PathBuf};
+
+use anyhow::{Context, Result};
+use clap::Parser;
+use cli::{Args, Command};
+use log::trace;
+use mapping::map_tree::MappingTree;
+use walkdir::{DirEntry, WalkDir};
+
+use crate::mapping::MapKey;
+
+mod cli;
+mod mapping;
+
+fn main() -> anyhow::Result<()> {
+    let args = Args::parse();
+
+    stderrlog::new()
+        .module(module_path!())
+        .quiet(args.quiet)
+        .show_module_names(false)
+        .color(stderrlog::ColorChoice::Auto)
+        .verbosity(args.verbosity as usize)
+        .timestamp(stderrlog::Timestamp::Off)
+        .init()?;
+
+    let mut mappings = MappingTree::new();
+
+    let relevant_directories = match &args.command {
+        Command::Visualize { options } => &options.relevant_directories,
+        Command::Generate { options } => &options.relevant_directories,
+    };
+
+    for dir in relevant_directories {
+        trace!("Processing '{}'..", dir.display());
+        let path = strip_path(&dir, &args.home_name)?;
+
+        mappings
+            .include(path_to_str(path)?)
+            .with_context(|| format!("Failed to include path: '{}'", path.display()))?;
+    }
+
+    let home = path_to_str(&args.home_name)?.to_owned();
+
+    let mut current_depth = 1;
+    while current_depth != args.depth {
+        for (key, value) in mappings.iter(false) {
+            trace!(
+                "Adding to child ('{}' -> '{}')",
+                MapKey::display(&key),
+                value
+            );
+
+            let mut local_mappings = MappingTree::new();
+            for dir in WalkDir::new(extend(&home, &value)?)
+                .min_depth(1)
+                .max_depth(1)
+                .into_iter()
+                .filter_entry(|e| is_dir(e) && !is_hidden(e))
+            {
+                let directory = dir
+                    .with_context(|| format!("Failed to read dir ('{}')", home.clone() + &value))?;
+                let path_to_strip = &PathBuf::from(extend(&home, &value)?);
+                let path = strip_path(&directory.path(), &path_to_strip)?;
+                trace!(
+                    "Including: '{}' (after stripping '{}' from '{}' -> '{}' + '/' + '{}')",
+                    path.display(),
+                    directory.path().display(),
+                    path_to_strip.display(),
+                    home,
+                    value
+                );
+
+                let gen_key = MapKey::new_ones_from_path(path_to_str(path)?, 1);
+                local_mappings
+                    .insert(
+                        &gen_key,
+                        path_to_str(strip_path(&directory.path(), &PathBuf::from(&home))?)?,
+                    )
+                    .with_context(|| format!("Failed to include path: '{}'", path.display()))?;
+            }
+
+            trace!("{}", local_mappings);
+
+            trace!(
+                "'{}' -> '{:#?}'",
+                MapKey::display(&key),
+                local_mappings.root_node()
+            );
+            mappings.interleave(&key, local_mappings.root_node().to_owned())?;
+        }
+        current_depth += 1;
+    }
+
+    match args.command {
+        Command::Visualize { .. } => println!("{}", mappings),
+        Command::Generate { .. } => println!("{}", mappings.to_lf_mappings(args.home_name)),
+    }
+
+    Ok(())
+}
+
+fn extend(base: &str, value: &str) -> Result<String> {
+    let base_path = PathBuf::from(base);
+    let value_path = PathBuf::from(value);
+
+    Ok(path_to_str(&base_path.join(&value_path))?.to_owned())
+}
+
+fn is_hidden(entry: &DirEntry) -> bool {
+    entry
+        .file_name()
+        .to_str()
+        .map(|s| s.starts_with("."))
+        .unwrap_or(false)
+}
+
+fn is_dir(entry: &DirEntry) -> bool {
+    entry.file_type().is_dir()
+}
+
+fn strip_path<'a>(path: &'a Path, to_strip: &Path) -> Result<&'a Path> {
+    path.strip_prefix(&to_strip).with_context(|| {
+        format!(
+            "'{}' is not under the specified home path ('{}')!",
+            path.display(),
+            to_strip.display()
+        )
+    })
+}
+
+fn path_to_str(path: &Path) -> Result<&str> {
+    path.to_str().with_context(|| {
+        format!(
+            "\
+Can't derive a keymapping from path: '{}' \
+because it can't be turned to a string
+",
+            path.display()
+        )
+    })
+}
+
+// fn gen_lf_mappings(home_name: PathBuf, char_num: usize, rel_dirs: Vec<PathBuf>) {
+//     let mut mappings_vec = vec![];
+//     let mut index_counter = 0;
+//     rel_dirs.iter().for_each(|rel_dir| {
+//         mappings_vec.push(vec![Mapping::new(
+//             &gen_hot_key(rel_dir, rel_dir, char_num),
+//             rel_dir,
+//             rel_dir,
+//             None,
+//         )]);
+//         get_dir(rel_dir.to_owned()).iter().for_each(|path| {
+//             mappings_vec[index_counter].push(Mapping::new(
+//                 &gen_hot_key(
+//                     path,
+//                     path.parent().expect("All paths here should have parents"),
+//                     char_num,
+//                 ),
+//                 path,
+//                 &path
+//                     .parent()
+//                     .expect("All paths here should have parents")
+//                     .to_owned(),
+//                 None,
+//             ));
+//         });
+//         index_counter += 1;
+//     });
+//     print_mappings(&mappings_vec, home_name);
+//     mappings_vec
+//         .into_iter()
+//         .for_each(|rel_dir_mapping: Vec<Mapping>| {
+//             let mut hash_map = sort_mapping_by_hot_key(rel_dir_mapping.clone());
+//             //dbg!(hash_map);
+//             hash_map.insert("gsi".to_owned(), vec![rel_dir_mapping[0].clone()]);
+//         });
+// }
+//
+// fn sort_mapping_by_hot_key(mut mappings: Vec<Mapping>) -> HashMap<String, Vec<Mapping>> {
+//     mappings.sort_by_key(|mapping| mapping.hot_key.clone());
+//
+//     let mut filtered_mappings: HashMap<String, Vec<Mapping>> = HashMap::new();
+//     mappings.iter().for_each(|mapping| {
+//         filtered_mappings.insert(mapping.hot_key.clone(), vec![]);
+//     });
+//     //dbg!(&mappings);
+//
+//     let mut index_counter = 1;
+//     mappings.iter().for_each(|mapping| {
+//         if mappings.len() > index_counter {
+//             let next_mapping = &mappings[index_counter];
+//             let vec = filtered_mappings
+//                 .get_mut(&mapping.hot_key)
+//                 .expect("This existst as it has been initialized");
+//
+//             if &next_mapping.hot_key == &mapping.hot_key {
+//                 vec.push(mapping.clone());
+//                 vec.push(next_mapping.clone());
+//             } else {
+//                 vec.push(mapping.clone());
+//             }
+//
+//             let new_vec = vec.to_owned();
+//             filtered_mappings.insert(mapping.hot_key.to_owned(), new_vec);
+//         }
+//
+//         index_counter += 1;
+//     });
+//     filtered_mappings
+// }
+//
+// fn print_mappings(mappings: &Vec<Vec<Mapping>>, home_name: PathBuf) {
+//     for mapping in mappings {
+//         mapping.iter().for_each(|map| {
+//             println!(
+//                 "{} = \"cd {}\";",
+//                 map.hot_key,
+//                 map.path
+//                     .display()
+//                     .to_string()
+//                     .replace(home_name.to_str().expect("This should be UTF-8"), "~")
+//             );
+//         });
+//
+//         println!("# -------------");
+//     }
+// }
diff --git a/pkgs/sources/lf-make-map/src/mapping/map_tree/display.rs b/pkgs/sources/lf-make-map/src/mapping/map_tree/display.rs
new file mode 100644
index 00000000..65302e1e
--- /dev/null
+++ b/pkgs/sources/lf-make-map/src/mapping/map_tree/display.rs
@@ -0,0 +1,91 @@
+use std::fmt::Display;
+
+use crate::mapping::{
+    map_tree::{Node, NodeValue},
+    MapKey,
+};
+
+use super::MappingTree;
+
+impl Display for MappingTree {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        fn write_node(
+            f: &mut std::fmt::Formatter<'_>,
+            node: &Node,
+            indention: String,
+            location: Vec<MapKey>,
+            is_last: bool,
+            is_root: bool,
+        ) -> std::fmt::Result {
+            let node_value = match &node.value {
+                NodeValue::Parent { children: _ } => "<Parent>".to_owned(),
+                NodeValue::Child { path, extandable } => {
+                    path.to_owned() + if *extandable { " [exten.]" } else { " [stop]" }
+                }
+            };
+
+            let new_idention = indention.clone()
+                + if is_root {
+                    ""
+                } else {
+                    match is_last {
+                        true => "    ",
+                        false => "│   ",
+                    }
+                };
+
+            let bullet = match is_last {
+                true => String::from("└── "),
+                false => String::from("├── "),
+            };
+
+            if is_root {
+                write!(f, ": {}\n", node_value)?;
+            } else {
+                write!(
+                    f,
+                    "{}{}\x1b[1;33m{}\x1b[0m: {}\n",
+                    indention,
+                    bullet,
+                    MapKey::display(&location),
+                    node_value,
+                )?;
+            };
+
+            match &node.value {
+                NodeValue::Parent { children } => {
+                    let mut children_vec: Vec<(&MapKey, &Node)> = children.iter().collect();
+                    children_vec.sort_by(|(a, _), (b, _)| a.key.cmp(&b.key));
+
+                    let mut counter = 1;
+                    for (key, child) in &children_vec {
+                        let mut new_location = location.clone();
+                        new_location.push((*key).to_owned());
+
+                        write_node(
+                            f,
+                            child,
+                            new_idention.clone(),
+                            new_location.clone(),
+                            counter == children_vec.len(),
+                            false,
+                        )?;
+                        counter += 1;
+                    }
+                }
+                NodeValue::Child {
+                    path: _,
+                    extandable: _,
+                } => {
+                    // Do nothing and stop the recursion
+                }
+            }
+
+            Ok(())
+        }
+
+        write_node(f, &self.root, String::new(), vec![], false, true)?;
+
+        Ok(())
+    }
+}
diff --git a/pkgs/sources/lf-make-map/src/mapping/map_tree/iterator.rs b/pkgs/sources/lf-make-map/src/mapping/map_tree/iterator.rs
new file mode 100644
index 00000000..4364bb2b
--- /dev/null
+++ b/pkgs/sources/lf-make-map/src/mapping/map_tree/iterator.rs
@@ -0,0 +1,53 @@
+use crate::mapping::MapKey;
+
+use super::{MappingTree, Node, NodeValue};
+
+pub struct MappingTreeIterator {
+    children: Vec<(Vec<MapKey>, String)>,
+}
+
+impl MappingTreeIterator {
+    pub fn new(tree: &MappingTree, ignore_extendable: bool) -> Self {
+        let children = extract_child(vec![], &tree.root, ignore_extendable);
+
+        Self { children }
+    }
+}
+
+fn extract_child(
+    current_key: Vec<MapKey>,
+    node: &Node,
+    ignore_extendable: bool,
+) -> Vec<(Vec<MapKey>, String)> {
+    match &node.value {
+        NodeValue::Parent { children } => children
+            .iter()
+            .map(|(key, value)| {
+                let mut new_key = current_key.clone();
+                new_key.push(key.to_owned());
+
+                extract_child(new_key, value, ignore_extendable)
+            })
+            .flatten()
+            .collect(),
+        NodeValue::Child { path, extandable } => {
+            if ignore_extendable {
+                vec![(current_key, path.to_string())]
+            } else {
+                if *extandable {
+                    vec![(current_key, path.to_string())]
+                } else {
+                    vec![]
+                }
+            }
+        }
+    }
+}
+
+impl Iterator for MappingTreeIterator {
+    type Item = (Vec<MapKey>, String);
+
+    fn next(&mut self) -> Option<Self::Item> {
+        self.children.pop()
+    }
+}
diff --git a/pkgs/sources/lf-make-map/src/mapping/map_tree/lf_mapping.rs b/pkgs/sources/lf-make-map/src/mapping/map_tree/lf_mapping.rs
new file mode 100644
index 00000000..6d9c7a0d
--- /dev/null
+++ b/pkgs/sources/lf-make-map/src/mapping/map_tree/lf_mapping.rs
@@ -0,0 +1,19 @@
+use std::path::PathBuf;
+
+use crate::mapping::MapKey;
+
+use super::MappingTree;
+
+impl MappingTree {
+    pub fn to_lf_mappings(self, home_path: PathBuf) -> String {
+        self.iter(true)
+            .map(|(key, value)| {
+                format!(
+                    "map g{} cd \"{}\"\n",
+                    MapKey::display(&key),
+                    home_path.join(&value).display()
+                )
+            })
+            .collect()
+    }
+}
diff --git a/pkgs/sources/lf-make-map/src/mapping/map_tree/mod.rs b/pkgs/sources/lf-make-map/src/mapping/map_tree/mod.rs
new file mode 100644
index 00000000..35e6d91d
--- /dev/null
+++ b/pkgs/sources/lf-make-map/src/mapping/map_tree/mod.rs
@@ -0,0 +1,402 @@
+use std::{collections::HashMap, mem};
+
+use anyhow::{bail, Result};
+use log::debug;
+
+use self::iterator::MappingTreeIterator;
+
+use super::MapKey;
+
+pub mod display;
+pub mod iterator;
+pub mod lf_mapping;
+
+/// A prefix tree
+#[derive(Debug)]
+pub struct MappingTree {
+    root: Node,
+}
+
+#[derive(Clone, Debug, PartialEq, Eq)]
+pub enum NodeValue {
+    Parent { children: HashMap<MapKey, Node> },
+    Child { path: String, extandable: bool },
+}
+
+#[derive(Clone, Debug, PartialEq, Eq)]
+pub struct Node {
+    value: NodeValue,
+}
+
+impl MappingTree {
+    pub fn new() -> Self {
+        Self {
+            root: Node::new_parent(),
+        }
+    }
+
+    pub fn root_node(&self) -> &Node {
+        &self.root
+    }
+
+    pub fn iter(&self, ignore_extendable: bool) -> MappingTreeIterator {
+        MappingTreeIterator::new(&self, ignore_extendable)
+    }
+
+    /// Returns the node at the key, otherwise None. The node can be changed
+    pub fn get_mut(&mut self, key: &[MapKey]) -> Option<&mut Node> {
+        let mut current_node = &mut self.root;
+        for ch in key.iter() {
+            if let NodeValue::Parent { children } = &mut current_node.value {
+                current_node = children.get_mut(&ch)?
+            } else {
+                return None;
+            }
+        }
+
+        Some(current_node)
+    }
+
+    /// Returns the node at the key, otherwise the last node that matched.
+    pub fn try_get(&self, key: &[MapKey]) -> (&Node, Vec<MapKey>) {
+        let mut current_node = &self.root;
+        let mut current_key = vec![];
+
+        for ch in key.iter() {
+            if let NodeValue::Parent { children } = &current_node.value {
+                current_node = if let Some(node) = children.get(&ch) {
+                    let (key, _value) = children
+                        .get_key_value(&ch)
+                        .expect("This exists, we checked");
+                    current_key.push(key.clone());
+
+                    node
+                } else {
+                    return (current_node, current_key);
+                };
+            } else {
+                return (current_node, current_key);
+            }
+        }
+
+        (current_node, current_key)
+    }
+
+    pub fn include(&mut self, path: &str) -> Result<()> {
+        let associated_key = MapKey::new_ones_from_path(path, 1);
+        self.insert(&associated_key, path)
+    }
+
+    pub fn insert(&mut self, key: &[MapKey], path: &str) -> Result<()> {
+        self.insert_node(key, Node::new_child(path.to_owned()))
+    }
+
+    pub fn interleave(&mut self, key: &[MapKey], node: Node) -> Result<()> {
+        let want_to_be_parent = self.get_mut(&key).expect("This value exists");
+        let (parent_value, _parent_children) = if let NodeValue::Parent { children } = node.value {
+            (
+                NodeValue::Parent {
+                    children: children.clone(),
+                },
+                children,
+            )
+        } else {
+            unreachable!("This value will be a parent")
+        };
+
+        let child_value = mem::replace(&mut want_to_be_parent.value, parent_value);
+        assert!(matches!(
+            child_value,
+            NodeValue::Child {
+                path: _,
+                extandable: _
+            }
+        ));
+
+        let child_value = if let NodeValue::Child {
+            path,
+            extandable: _,
+        } = child_value
+        {
+            NodeValue::Child {
+                path,
+                extandable: false,
+            }
+        } else {
+            unreachable!("This is only a child value")
+        };
+
+        let child = Node { value: child_value };
+
+        let mut new_key = key.to_vec();
+        new_key.push(MapKey {
+            key: '.',
+            part_path: ".".to_owned(),
+            resolution: 1,
+        });
+        self.insert_node(&new_key, child)?;
+        Ok(())
+    }
+
+    pub fn insert_node(&mut self, key: &[MapKey], node: Node) -> Result<()> {
+        let (_node, found_key) = self.try_get(key).clone();
+
+        if found_key != key {
+            let needed_nodes_key = key
+                .strip_prefix(&found_key[..])
+                .expect("The node's location is a prefix");
+
+            let needed_nodes_length = needed_nodes_key.iter().count();
+
+            let mut current_node = self
+                .get_mut(&found_key[..])
+                .expect("This should always exists");
+            let mut current_location = found_key.clone();
+            let mut counter = 1;
+
+            for ch in needed_nodes_key.iter() {
+                current_location.push(ch.to_owned());
+
+                let next_node = if counter == needed_nodes_length {
+                    node.clone()
+                } else {
+                    Node::new_parent()
+                };
+
+                current_node = match &current_node.value {
+                    NodeValue::Parent { children } => {
+                        assert_eq!(children.get(&ch), None);
+
+                        let children =
+                            if let NodeValue::Parent { children } = &mut current_node.value {
+                                children
+                            } else {
+                                unreachable!("This is a parent, we cheched")
+                            };
+
+                        children.insert(ch.to_owned(), next_node);
+                        children.get_mut(&ch).expect("Was just inserted")
+                    }
+                    NodeValue::Child {
+                        path,
+                        extandable: _,
+                    } => {
+                        // A node that should be a parent was classified
+                        // as child before:
+                        //
+                        //  1. Remove the child node and replace it with a parent one.
+                        //  2. Add the child node to the parent node as child, but with a '.' as MapKey.
+                        //  3. Add the original node also as child to the parent node.
+
+                        let mut children = HashMap::new();
+                        let move_child_node = Node::new_child(path.to_owned());
+
+                        children.insert(
+                            MapKey {
+                                key: '.',
+                                part_path: ".".to_owned(),
+                                resolution: 1,
+                            },
+                            move_child_node,
+                        );
+                        children.insert(ch.to_owned(), next_node);
+
+                        current_node.value = NodeValue::Parent { children };
+
+                        let children =
+                            if let NodeValue::Parent { children } = &mut current_node.value {
+                                children
+                            } else {
+                                unreachable!("We just inserted the parent value.")
+                            };
+
+                        children.get_mut(&ch).expect("Was just inserted")
+                    }
+                };
+
+                counter += 1;
+            }
+        } else {
+            fn reduce_string(a: &str) -> Option<char> {
+                let first_char = a.chars().take(1).last().expect("Should contain one char");
+
+                if a.chars().all(|ch| ch == first_char) {
+                    return Some(first_char);
+                } else {
+                    return None;
+                }
+            }
+            fn check_subset(a: &str, b: &str) -> bool {
+                if a.len() > b.len() {
+                    let a_prefix: String = a.chars().take(b.len()).collect();
+                    let a_suffix: String = a.chars().skip(b.len()).collect();
+
+                    if a_prefix == b {
+                        let clean_suffix = reduce_string(&a_suffix);
+                        if let Some(ch) = clean_suffix {
+                            ch == b.chars().last().expect("Will match")
+                        } else {
+                            false
+                        }
+                    } else {
+                        false
+                    }
+                } else if b.len() > a.len() {
+                    let b_prefix: String = b.chars().take(a.len()).collect();
+                    let b_suffix: String = b.chars().skip(a.len()).collect();
+
+                    if b_prefix == a {
+                        let clean_suffix = reduce_string(&b_suffix);
+                        if let Some(ch) = clean_suffix {
+                            ch == a.chars().last().expect("Will match")
+                        } else {
+                            false
+                        }
+                    } else {
+                        false
+                    }
+                } else {
+                    a == b
+                }
+            }
+
+            // Another node was already inserted with the same key!
+            // So we simple increase the resolution of the other node and this node, until their
+            // keys are not the same anymore.
+            // This only includes the last segment of the `MapKey`
+            //
+            // 1. Change both keys, until they are not equal any more
+            // 2. Move the wrongly placed node to the new place.
+            // 3. Insert our node.
+            let mut foreign_key = vec![found_key.last().expect("This will exist").clone()];
+            let mut our_key = vec![key.last().expect("This will exist").clone()];
+
+            debug!(
+                "'{}' ('{}') and '{}' ('{}') are the same, try to find a better combination!",
+                MapKey::display(&our_key),
+                our_key[0].part_path,
+                MapKey::display(&foreign_key),
+                foreign_key[0].part_path,
+            );
+
+            // The 'a' and 'b' stuff is here, to ensure that both returning None will not match
+            // this condition.
+            if reduce_string(&foreign_key[0].part_path).unwrap_or('a')
+                == reduce_string(&our_key[0].part_path).unwrap_or('b')
+            {
+                bail!(
+                    "\
+The foreign_key ('{}', path_part: '{}' -> '{}') and our_key ('{}', path_part: '{}' -> '{}') \
+have an identical path_part (when duplicated chars are removed)!
+I cannot extended them via incrementation.
+Please rename the paths to fix this.
+                        ",
+                    MapKey::display(&foreign_key),
+                    &foreign_key[0].part_path,
+                    reduce_string(&foreign_key[0].part_path).expect("Is some here"),
+                    MapKey::display(&our_key),
+                    &our_key[0].part_path,
+                    reduce_string(&our_key[0].part_path).expect("Is some here"),
+                );
+            }
+
+            if check_subset(&foreign_key[0].part_path, &our_key[0].part_path) {
+                bail!(
+                    "\
+The foreign_key ('{}', path_part: '{}') and our_key ('{}', path_part: '{}') \
+are subsets of one another!
+A discrimination through incrementation will not work!
+Please rename the paths to fix this.
+                        ",
+                    MapKey::display(&foreign_key),
+                    &foreign_key[0].part_path,
+                    MapKey::display(&our_key),
+                    &our_key[0].part_path,
+                );
+            }
+
+            while our_key == foreign_key {
+                our_key = our_key[0].increment(our_key[our_key.len() - 1].resolution + 1);
+                foreign_key =
+                    foreign_key[0].increment(foreign_key[foreign_key.len() - 1].resolution + 1);
+                debug!(
+                    "Now its: '{}' ('{}') and '{}' ('{}')",
+                    MapKey::display(&our_key),
+                    our_key[0].part_path,
+                    MapKey::display(&foreign_key),
+                    foreign_key[0].part_path,
+                );
+            }
+
+            debug!(
+                "Found a better one: '{}' ('{}') and '{}' ('{}')",
+                MapKey::display(&our_key),
+                our_key[0].part_path,
+                MapKey::display(&foreign_key),
+                foreign_key[0].part_path,
+            );
+
+            let parent = self
+                .get_mut(&found_key[..&found_key.len() - 1])
+                .expect("This will exist");
+
+            if let NodeValue::Parent { children } = &mut parent.value {
+                if let NodeValue::Child {
+                    path: _,
+                    extandable: _,
+                } = children
+                    .get(found_key.last().expect("Exists"))
+                    .expect("This node also exists")
+                    .value
+                {
+                    let old = children
+                        .remove(found_key.last().expect("This will exist"))
+                        .expect("This will be there");
+
+                    let full_foreign_key: Vec<_> = found_key
+                        .clone()
+                        .into_iter()
+                        .rev()
+                        .skip(1)
+                        .rev()
+                        .chain(foreign_key.clone().into_iter())
+                        .collect();
+                    self.insert_node(&full_foreign_key, old.clone())?;
+                }
+
+                let full_our_key: Vec<_> = key
+                    .to_vec()
+                    .into_iter()
+                    .rev()
+                    .skip(1)
+                    .rev()
+                    .chain(our_key.clone().into_iter())
+                    .collect();
+
+                self.insert_node(&full_our_key, node.clone())?;
+            } else {
+                unreachable!("This node will be a parent");
+            }
+        }
+
+        Ok(())
+    }
+}
+
+impl Node {
+    pub fn new_child(path: String) -> Self {
+        Self {
+            value: NodeValue::Child {
+                path,
+                extandable: true,
+            },
+        }
+    }
+    pub fn new_parent() -> Self {
+        Self {
+            value: NodeValue::Parent {
+                children: HashMap::new(),
+            },
+        }
+    }
+}
diff --git a/pkgs/sources/lf-make-map/src/mapping/mod.rs b/pkgs/sources/lf-make-map/src/mapping/mod.rs
new file mode 100644
index 00000000..114fdca0
--- /dev/null
+++ b/pkgs/sources/lf-make-map/src/mapping/mod.rs
@@ -0,0 +1,156 @@
+use std::{
+    fmt::{Display, Write},
+    hash::Hash,
+};
+
+use log::debug;
+
+pub mod map_tree;
+
+#[derive(Clone, Debug, Eq)]
+pub struct MapKey {
+    pub key: char,
+
+    resolution: usize,
+
+    /// Part of the path, used to derive the key
+    part_path: String,
+}
+
+impl Hash for MapKey {
+    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
+        self.key.hash(state)
+    }
+}
+
+impl PartialEq for MapKey {
+    fn eq(&self, other: &Self) -> bool {
+        self.key == other.key
+    }
+}
+
+impl MapKey {
+    pub fn new_from_part_path(part_path: &str, resolution: usize) -> Vec<Self> {
+        let key = Self::part_path_to_key(&part_path, resolution);
+
+        key.chars()
+            .map(|ch| Self {
+                key: ch,
+                resolution,
+                part_path: part_path.to_owned(),
+            })
+            .collect()
+    }
+
+    pub fn new_ones_from_path(path: &str, number_of_chars: usize) -> Vec<Self> {
+        let key: Vec<MapKey> = path
+            .split('/')
+            .map(|part| Self::new_from_part_path(part, number_of_chars))
+            .flatten()
+            .collect();
+
+        debug!(
+            "Generated full MapKeys: '{}' -> '{}'",
+            path,
+            MapKey::display(&key)
+        );
+        key
+    }
+
+    pub fn increment(&self, target_resolution: usize) -> Vec<Self> {
+        let new_resolution = target_resolution;
+
+        // debug!("Incrementing: '{}' ('{}')", &self, &self.part_path);
+
+        let added_chars = if new_resolution < self.part_path.len() {
+            MapKey::part_path_to_key(&self.part_path, new_resolution)
+        } else {
+            let mut generated_chars =
+                MapKey::part_path_to_key(&self.part_path, self.part_path.len());
+
+            generated_chars.extend(
+                (0..(new_resolution - self.part_path.len()))
+                    .into_iter()
+                    .map(|_| self.part_path.chars().last().expect("This will exists")),
+            );
+
+            generated_chars
+        };
+
+        let part_path = self.part_path.clone();
+        let output: Vec<Self> = added_chars
+            .chars()
+            .enumerate()
+            .map(|(res, ch)| MapKey {
+                key: ch,
+                resolution: res + 1,
+                part_path: part_path.clone(),
+            })
+            .collect();
+
+        // debug!("Finished increment: '{}' ('{}')", MapKey::display(&output), output[0].part_path);
+        output
+    }
+
+    pub fn display(values: &[Self]) -> String {
+        values.iter().map(|value| value.key.clone()).collect()
+    }
+    fn part_path_to_key(part: &str, number_of_chars: usize) -> String {
+        fn make(pat: char, part: &str, number_of_chars: usize) -> String {
+            let mut acc = String::new();
+
+            if !part.split(pat).all(|part| part.len() > 0) {
+                panic!(
+                    "\
+Can't turn this path '{}' to a mapping.
+This should not happen, please report the bug!",
+                    part
+                )
+            }
+
+            let mut last_working = None;
+            for i in 0..number_of_chars {
+                for str in part.split(pat) {
+                    if acc.len() != number_of_chars {
+                        acc.push(match str.chars().nth(i) {
+                            Some(ch) => ch,
+                            None => {
+                                if let Some(last) = last_working {
+                                    str.chars().nth(last).expect("This should always exist")
+                                } else {
+                                    last_working = Some(i - 1);
+                                    str.chars().nth(i - 1).expect("This should always exist")
+                                }
+                            }
+                        })
+                    }
+                }
+            }
+
+            acc
+        }
+
+        let value = if part.contains('_') && !part.starts_with('_') && !part.ends_with('_') {
+            make('_', part, number_of_chars)
+        } else if part.contains('-') && !part.starts_with('-') && !part.ends_with('-') {
+            make('-', part, number_of_chars)
+        } else {
+            part.chars().take(number_of_chars).collect::<String>()
+        };
+
+        assert_eq!(
+            value.len(),
+            number_of_chars,
+            "'{}' does not have expected length of: {}",
+            value,
+            number_of_chars
+        );
+        value
+    }
+}
+
+impl Display for MapKey {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        f.write_char(self.key)
+    }
+}
diff --git a/pkgs/sources/lf-make-map/update.sh b/pkgs/sources/lf-make-map/update.sh
new file mode 100755
index 00000000..a0a029f4
--- /dev/null
+++ b/pkgs/sources/lf-make-map/update.sh
@@ -0,0 +1,6 @@
+#!/usr/bin/env sh
+
+nix flake update
+
+[ "$1" = "upgrade" ] && cargo upgrade
+cargo update
diff --git a/pkgs/sources/plgs-pkgs/README.md b/pkgs/sources/plgs-pkgs/README.md
new file mode 100644
index 00000000..e8169951
--- /dev/null
+++ b/pkgs/sources/plgs-pkgs/README.md
@@ -0,0 +1,92 @@
+# Fork
+
+All files in this repository where forked form [here](https://github.com/NixNeovim/NixNeovimPlugins) on commit `5010b91eb03696574c3c293f072a090618227e87`.
+Below the original README. They were licensed under the MIT license.
+
+# All vim plugins, ready to go
+
+This repo auto generates nix packages for vim/neovim plugins.
+Packages are automatically updated twice per week using a GitHub Actions.
+Plugins are fetched from the `manifest.txt` and [awesome-neovim][0] repo.
+
+This is a fork of [this repo](https://github.com/m15a/nixpkgs-vim-extra-plugins); however, we fetch all additions from the original repo, so we will never have less plugins.
+Further, the original deletes plugins that are available in the nixpkgs. We, instead, try to assemble a list of all available plugins.
+Therefore, to access plugins you will never have to search in two places.
+
+This repo can be used as a stand-alone, by adding it to your inputs.
+However, we recommend to use [NixNeovim](https://github.com/NixNeovim/NixNeovim) modules instead, and use this only when you need a plugins, which does not have a module, yet.
+
+## Available plugins
+
+The [plugins.md](plugins.md) contains an auto-generated list of all available plugins.
+
+## Usage
+
+- We recommend using [NixNeovim](https://github.com/NixNeovim/NixNeovim), and only access the plugins directly when they do not have a module in NixNeovim.
+
+However, you can also use this repo without NixNeovim:
+To access the plugins, you need to add the overlay.
+The overlay adds extra Vim plugins to `pkgs.vimExtraPlugins`.
+First, add this repo to your inputs:
+
+```
+inputs.nixneovimplugins.url = github:jooooscha/nixpkgs-vim-extra-plugins
+```
+
+Next, apply the provided overlay:
+
+```
+nixpkgs.overlays = [
+  inputs.nixneovimplugins.overlays.default
+];
+```
+
+Finally, you can add the packages to your vim/neovim config. For example you can use [NixNeovim](https://github.com/NixNeovim/Nixneovim) or you can add the plugins directly:
+
+```
+ programs.neovim = {
+   plugins = [
+     pkgs.vimExtraPlugins.nvim-colorizer-lua
+   ];
+ }
+```
+
+More info on using neovim with nix can be found here: [NixOS Neovim](https://nixos.wiki/wiki/Neovim)
+
+## Contribution
+
+### How to add a new plugin
+
+#### 1. Add the plugin to manifest.txt:
+
+```
+# Examples
+
+haringsrob/nvim_context_vt
+sourcehut:henriquehbr/ataraxis.lua
+gitlab:yorickpeterse/nvim-pqf
+williamboman/mason.nvim:45b9a4da776d9fb017960b3ac7241161fb7bc578
+foo/bar::baz                   --> renamed to baz
+foo/bar:dev                    --> using dev branch
+```
+
+Supported are Github (default), SourceHut, and GitLab.
+
+#### 2. Create a Pull Request
+
+- Create a pull request with the changed manifest.txt (and blacklist.txt if neccessary).
+- A GitHub action will check your contribution and generate all neccessary nix code for your new plugin. It will also take care of sorting and cleaning the manifest.txt
+- After all checks have passed, I will merge your change.
+
+I am happy for any contribution. :)
+
+### How to remove a new plugin
+
+Copy the entry from manifest.txt to blacklist.txt and create a PR.
+The GitHub Actions will do the rest, including removing the entry from manifest.txt
+
+## Credits
+
+This is originally based on work by [m15a](https://github.com/m15a/nixpkgs-vim-extra-plugins)
+
+[0]: https://github.com/rockerBOO/awesome-neovim
diff --git a/pkgs/sources/plgs-pkgs/check.nix b/pkgs/sources/plgs-pkgs/check.nix
new file mode 100644
index 00000000..ad23e2c7
--- /dev/null
+++ b/pkgs/sources/plgs-pkgs/check.nix
@@ -0,0 +1,37 @@
+{
+  pkgs,
+  lib,
+  ...
+}: let
+  # checks if a plugin has a license
+  hasLicense = _: pkg: let
+    warn = x: lib.warn x x;
+
+    msg =
+      if builtins.hasAttr "license" pkg.meta
+      then "${pkg.name} has license"
+      else warn "${pkg.name} has no license";
+
+    msg' = lib.replaceStrings [" "] ["-"] msg;
+  in
+    pkgs.runCommandNoCC msg' {} "echo : > $out ";
+
+  # function to check License for all packages
+  check-missing-licenses = let
+    buildInputs =
+      lib.mapAttrsToList
+      hasLicense
+      pkgs.vimExtraPlugins;
+  in
+    pkgs.runCommandNoCC
+    "check-missing-licenses"
+    {inherit buildInputs;}
+    "echo : > $out";
+in {
+  checks =
+    pkgs.vimExtraPlugins
+    // {
+      inherit check-missing-licenses;
+      inherit (pkgs) update-vim-plugins;
+    };
+}
diff --git a/pkgs/sources/plgs-pkgs/default.nix b/pkgs/sources/plgs-pkgs/default.nix
new file mode 100644
index 00000000..0f7cd485
--- /dev/null
+++ b/pkgs/sources/plgs-pkgs/default.nix
@@ -0,0 +1,15 @@
+[
+  (final: prev:
+    prev.lib.composeManyExtensions [
+      (self: super: let
+        origin = import ./plugins {
+          inherit (super.vimUtils) buildVimPlugin;
+          inherit (super) lib fetchurl fetchgit;
+        };
+      in {
+        vimExtraPlugins = super.lib.makeExtensible (_: super.lib.recurseIntoAttrs origin);
+      })
+    ]
+    final
+    prev)
+]
diff --git a/pkgs/sources/plgs-pkgs/overrides.nix b/pkgs/sources/plgs-pkgs/overrides.nix
new file mode 100644
index 00000000..e03a78b1
--- /dev/null
+++ b/pkgs/sources/plgs-pkgs/overrides.nix
@@ -0,0 +1,34 @@
+final: prev: let
+  inherit (final) lib;
+
+  /*
+  * Mark broken packages here.
+  */
+  markBrokenPackages = self: super:
+    lib.mapAttrs (attrName: broken:
+      super.${attrName}.overrideAttrs (old: {
+        meta = old.meta // {inherit broken;};
+      }))
+    {
+      # <name> = true;
+    };
+
+  /*
+  * Add licenses if missing or incorrect in generated ./pkgs/vim-plugins.nix.
+  */
+  fixLicenses = self: super:
+    lib.mapAttrs (attrName: license:
+      super.${attrName}.overrideAttrs (old: {
+        meta = old.meta // {inherit license;};
+      })) (with lib.licenses; {
+      /*
+      * Example:
+      * plugin-name = [<licenses>]
+      */
+    });
+in {
+  vimExtraPlugins = prev.vimExtraPlugins.extend (lib.composeManyExtensions [
+    markBrokenPackages
+    fixLicenses
+  ]);
+}
diff --git a/pkgs/sources/plgs-pkgs/plugins/.plugins.json b/pkgs/sources/plgs-pkgs/plugins/.plugins.json
new file mode 100644
index 00000000..9331bc8f
--- /dev/null
+++ b/pkgs/sources/plgs-pkgs/plugins/.plugins.json
@@ -0,0 +1,7 @@
+{
+  "ThePrimeagen/harpoon:master": "{\"description\": \"\", \"homepage\": \"https://github.com/ThePrimeagen/harpoon\", \"license\": {\"py/reduce\": [{\"py/type\": \"update_vim_plugins.nix.License\"}, {\"py/tuple\": [\"mit\"]}]}, \"name\": \"harpoon\", \"owner\": \"ThePrimeagen\", \"py/object\": \"update_vim_plugins.plugin.GitHubPlugin\", \"source\": {\"py/object\": \"update_vim_plugins.nix.UrlSource\", \"sha256\": \"1w4hi9hbdjwdhb4vwa0x08a25vbcxqg1d5cskm2qvjy5fdlqils0\", \"url\": \"https://github.com/ThePrimeagen/harpoon/archive/ccae1b9bec717ae284906b0bf83d720e59d12b91.tar.gz\"}, \"source_line\": \"ThePrimeagen/harpoon:master\", \"version\": {\"__reduce__\": [{\"py/type\": \"datetime.date\"}, [\"B+cMGg==\"]], \"py/object\": \"datetime.date\"}}",
+  "akinsho/toggleterm.nvim": "{\"description\": \"A neovim lua plugin to help easily manage multiple terminal windows\", \"homepage\": \"https://github.com/akinsho/toggleterm.nvim\", \"license\": {\"py/reduce\": [{\"py/type\": \"update_vim_plugins.nix.License\"}, {\"py/tuple\": [\"gpl3Only\"]}]}, \"name\": \"toggleterm-nvim\", \"owner\": \"akinsho\", \"py/object\": \"update_vim_plugins.plugin.GitHubPlugin\", \"source\": {\"py/object\": \"update_vim_plugins.nix.UrlSource\", \"sha256\": \"0nx69q9597vy7lzvvh58fnjyin23ns6apmyp532sgf547bw7mld6\", \"url\": \"https://github.com/akinsho/toggleterm.nvim/archive/cbd041d91b90cd3c02df03fe6133208888f8e008.tar.gz\"}, \"source_line\": \"akinsho/toggleterm.nvim\", \"version\": {\"__reduce__\": [{\"py/type\": \"datetime.date\"}, [\"B+cMBg==\"]], \"py/object\": \"datetime.date\"}}",
+  "andrewferrier/debugprint.nvim": "{\"description\": \"Debugging in NeoVim the print() way!\", \"homepage\": \"https://github.com/andrewferrier/debugprint.nvim\", \"license\": {\"py/reduce\": [{\"py/type\": \"update_vim_plugins.nix.License\"}, {\"py/tuple\": [\"mit\"]}]}, \"name\": \"debugprint-nvim\", \"owner\": \"andrewferrier\", \"py/object\": \"update_vim_plugins.plugin.GitHubPlugin\", \"source\": {\"py/object\": \"update_vim_plugins.nix.UrlSource\", \"sha256\": \"06r1jhx7jd15q8wvnw0xqwk3bkx39pm4pbv70hf9ggd6zsnmsrmn\", \"url\": \"https://github.com/andrewferrier/debugprint.nvim/archive/54297dd0a4f318b279a1cb954e7714f3942df123.tar.gz\"}, \"source_line\": \"andrewferrier/debugprint.nvim\", \"version\": {\"__reduce__\": [{\"py/type\": \"datetime.date\"}, [\"B+gDHQ==\"]], \"py/object\": \"datetime.date\"}}",
+  "lmburns/lf.nvim": "{\"description\": \"Lf file manager for Neovim (in Lua)\", \"homepage\": \"https://github.com/lmburns/lf.nvim\", \"license\": {\"py/reduce\": [{\"py/type\": \"update_vim_plugins.nix.License\"}, {\"py/tuple\": [\"mit\"]}]}, \"name\": \"lf-nvim\", \"owner\": \"lmburns\", \"py/object\": \"update_vim_plugins.plugin.GitHubPlugin\", \"source\": {\"py/object\": \"update_vim_plugins.nix.UrlSource\", \"sha256\": \"1nwf90bnzqhlgs007gg6xpx0vf4r1d19586nld78ipi1ch7nz4px\", \"url\": \"https://github.com/lmburns/lf.nvim/archive/69ab1efcffee6928bf68ac9bd0c016464d9b2c8b.tar.gz\"}, \"source_line\": \"lmburns/lf.nvim\", \"version\": {\"__reduce__\": [{\"py/type\": \"datetime.date\"}, [\"B+cKAw==\"]], \"py/object\": \"datetime.date\"}}",
+  "nvim-telescope/telescope-bibtex.nvim": "{\"description\": \"A telescope.nvim extension to search and paste bibtex entries into your TeX files.\", \"homepage\": \"https://github.com/nvim-telescope/telescope-bibtex.nvim\", \"license\": {\"py/reduce\": [{\"py/type\": \"update_vim_plugins.nix.License\"}, {\"py/tuple\": [\"mit\"]}]}, \"name\": \"telescope-bibtex-nvim\", \"owner\": \"nvim-telescope\", \"py/object\": \"update_vim_plugins.plugin.GitHubPlugin\", \"source\": {\"py/object\": \"update_vim_plugins.nix.UrlSource\", \"sha256\": \"1sd6p8cvv3dckgrhc7grlyfcibjxhxbfyh0w7p5m4mdcazhy1kqs\", \"url\": \"https://github.com/nvim-telescope/telescope-bibtex.nvim/archive/289a6f86ebec06e8ae1590533b732b9981d84900.tar.gz\"}, \"source_line\": \"nvim-telescope/telescope-bibtex.nvim\", \"version\": {\"__reduce__\": [{\"py/type\": \"datetime.date\"}, [\"B+gDHA==\"]], \"py/object\": \"datetime.date\"}}"
+}
\ No newline at end of file
diff --git a/pkgs/sources/plgs-pkgs/plugins/blacklist.txt b/pkgs/sources/plgs-pkgs/plugins/blacklist.txt
new file mode 100644
index 00000000..8b137891
--- /dev/null
+++ b/pkgs/sources/plgs-pkgs/plugins/blacklist.txt
@@ -0,0 +1 @@
+
diff --git a/pkgs/sources/plgs-pkgs/plugins/default.nix b/pkgs/sources/plgs-pkgs/plugins/default.nix
new file mode 100644
index 00000000..df09e446
--- /dev/null
+++ b/pkgs/sources/plgs-pkgs/plugins/default.nix
@@ -0,0 +1,55 @@
+{
+  lib,
+  buildVimPlugin,
+  fetchurl,
+  fetchgit,
+}: {
+  /*
+  Generated from: ThePrimeagen/harpoon:master
+  */
+  harpoon = buildVimPlugin {
+    pname = "harpoon";
+    version = "2023-12-26";
+    src = fetchurl {
+      url = "https://github.com/ThePrimeagen/harpoon/archive/ccae1b9bec717ae284906b0bf83d720e59d12b91.tar.gz";
+      sha256 = "1w4hi9hbdjwdhb4vwa0x08a25vbcxqg1d5cskm2qvjy5fdlqils0";
+    };
+    meta = with lib; {
+      description = "";
+      homepage = "https://github.com/ThePrimeagen/harpoon";
+      license = with licenses; [mit];
+    };
+  };
+  /*
+  Generated from: lmburns/lf.nvim
+  */
+  lf-nvim = buildVimPlugin {
+    pname = "lf-nvim";
+    version = "2023-10-03";
+    src = fetchurl {
+      url = "https://github.com/lmburns/lf.nvim/archive/69ab1efcffee6928bf68ac9bd0c016464d9b2c8b.tar.gz";
+      sha256 = "1nwf90bnzqhlgs007gg6xpx0vf4r1d19586nld78ipi1ch7nz4px";
+    };
+    meta = with lib; {
+      description = "Lf file manager for Neovim (in Lua)";
+      homepage = "https://github.com/lmburns/lf.nvim";
+      license = with licenses; [mit];
+    };
+  };
+  /*
+  Generated from: nvim-telescope/telescope-bibtex.nvim
+  */
+  telescope-bibtex-nvim = buildVimPlugin {
+    pname = "telescope-bibtex-nvim";
+    version = "2024-03-28";
+    src = fetchurl {
+      url = "https://github.com/nvim-telescope/telescope-bibtex.nvim/archive/289a6f86ebec06e8ae1590533b732b9981d84900.tar.gz";
+      sha256 = "1sd6p8cvv3dckgrhc7grlyfcibjxhxbfyh0w7p5m4mdcazhy1kqs";
+    };
+    meta = with lib; {
+      description = "A telescope.nvim extension to search and paste bibtex entries into your TeX files.";
+      homepage = "https://github.com/nvim-telescope/telescope-bibtex.nvim";
+      license = with licenses; [mit];
+    };
+  };
+}
diff --git a/pkgs/sources/plgs-pkgs/plugins/manifest.txt b/pkgs/sources/plgs-pkgs/plugins/manifest.txt
new file mode 100644
index 00000000..615083c8
--- /dev/null
+++ b/pkgs/sources/plgs-pkgs/plugins/manifest.txt
@@ -0,0 +1,3 @@
+lmburns/lf.nvim
+nvim-telescope/telescope-bibtex.nvim
+ThePrimeagen/harpoon:master
diff --git a/pkgs/sources/plgs-pkgs/plugins/plugins.md b/pkgs/sources/plgs-pkgs/plugins/plugins.md
new file mode 100644
index 00000000..4f73f811
--- /dev/null
+++ b/pkgs/sources/plgs-pkgs/plugins/plugins.md
@@ -0,0 +1,7 @@
+- Plugin count: 3
+
+| Repo | Last Update | Nix package name | Last checked |
+|:---|:---|:---|:---|
+| [ThePrimeagen/harpoon:master](https://github.com/ThePrimeagen/harpoon) | 2023-12-26 | `harpoon` | 2024-05-09 |
+| [lmburns/lf.nvim](https://github.com/lmburns/lf.nvim) | 2023-10-03 | `lf-nvim` | 2024-05-09 |
+| [nvim-telescope/telescope-bibtex.nvim](https://github.com/nvim-telescope/telescope-bibtex.nvim) | 2024-03-28 | `telescope-bibtex-nvim` | 2024-05-09 |
diff --git a/pkgs/sources/plgs-pkgs/plugins/whitelist.txt b/pkgs/sources/plgs-pkgs/plugins/whitelist.txt
new file mode 100644
index 00000000..e69de29b
--- /dev/null
+++ b/pkgs/sources/plgs-pkgs/plugins/whitelist.txt
diff --git a/pkgs/sources/plgs-pkgs/update.sh b/pkgs/sources/plgs-pkgs/update.sh
new file mode 100755
index 00000000..6a0d3452
--- /dev/null
+++ b/pkgs/sources/plgs-pkgs/update.sh
@@ -0,0 +1,27 @@
+#!/usr/bin/env sh
+
+BASE_DIR="$(readlink -f "$(dirname "$0")/plugins")"
+
+# Fetch plugins
+cd "$BASE_DIR" || (echo "BUG: No '$BASE_DIR'" && exit 1)
+
+# Cleanup manifest
+sort -o "$BASE_DIR/manifest.txt" "$BASE_DIR/manifest.txt"
+sort -o "$BASE_DIR/blacklist.txt" "$BASE_DIR/blacklist.txt"
+## Remove all plugins, which are on the blacklist
+# The same file is read and written to
+# shellcheck disable=SC2005
+echo "$(comm -23 "$BASE_DIR/manifest.txt" "$BASE_DIR/blacklist.txt")" >"$BASE_DIR/manifest.txt"
+
+# Backup vim-plugins.nix
+mv "$BASE_DIR/default.nix" "$BASE_DIR/default.nix.bak"
+echo "{...} : {}" >"$BASE_DIR/default.nix"
+
+# Generate derivations for new plugins (this binary is provided by the dev-environment)
+update-vim-plugins cleanup "$BASE_DIR"
+
+# Restore vim-plugins.nix
+mv "$BASE_DIR/default.nix.bak" "$BASE_DIR/default.nix"
+
+# Update new plugins
+update-vim-plugins update "$BASE_DIR" --all
diff --git a/pkgs/sources/scripts/default.nix b/pkgs/sources/scripts/default.nix
new file mode 100644
index 00000000..09c8d411
--- /dev/null
+++ b/pkgs/sources/scripts/default.nix
@@ -0,0 +1,412 @@
+{
+  sysLib,
+  homeConfig,
+  nixosConfig,
+}: [
+  (
+    final: prev: let
+      inherit (prev) lib;
+
+      write_shell = {
+        name,
+        path,
+        dependencies ? [],
+        keepPath ? false,
+        completions ? false,
+      }:
+        sysLib.writeShellScript {
+          inherit name keepPath;
+          src = ./source/${path}/${name}.sh;
+          dependencies = dependencies ++ [prev.dash];
+          generateCompletions = completions;
+        };
+      write_python = {
+        name,
+        path,
+        dependencies_system ? [],
+        dependencies_python ? _: [],
+        keepPath ? false,
+      }: let
+        src = ./source/${path}/${name}.py;
+        dependencies =
+          [(prev.python3.withPackages dependencies_python)]
+          ++ dependencies_system;
+        path_setting =
+          if keepPath
+          then "--prefix PATH :"
+          else "--set PATH";
+      in
+        prev.runCommandLocal name {
+          nativeBuildInputs = [prev.makeWrapper] ++ dependencies;
+        }
+        ''
+          install -m755 ${src} -D "$out/bin/${name}"
+          patchShebangs "$out/bin/${name}"
+          wrapProgram "$out/bin/${name}" ${path_setting} ${prev.lib.makeBinPath dependencies};
+        '';
+
+      ## Begin of shell scripts
+      aumo-scr = write_shell {
+        name = "aumo";
+        path = "apps";
+        dependencies = builtins.attrValues {
+          inherit
+            (prev)
+            udisks
+            findutils
+            rofi
+            ;
+        };
+      };
+
+      battery-scr = write_shell {
+        name = "battery";
+        path = "wrappers";
+        dependencies = [];
+      };
+
+      brightness-scr = lib.mkIf nixosConfig.soispha.laptop.enable (write_shell {
+        name = "brightness";
+        path = "small_functions";
+        generateCompletions = true;
+        dependencies = [];
+        replacementStrings = {BACKLIGHT_NAME = nixosConfig.soispha.laptop.backlight;};
+      });
+
+      con2pdf-scr = sysLib.writeShellScript {
+        name = "con2pdf";
+        src = ./source/apps/con2pdf.sh;
+        dependencies = builtins.attrValues {inherit (prev) sane-backends imagemagick coreutils fd;};
+        generateCompletions = true;
+        replacementStrings = {
+          DEVICE_FUNCTION =
+            # This is here, because escaping the whole function, to use it in the shell script
+            # directly just isn't possible
+            prev.writeText "DEVICE_FUNCTION"
+            /*
+            bash
+            */
+            ''
+              scanimage -L | awk 'BEGIN { FS = "`" } { gsub(/'.*/, "", $2); print $2 }'
+            '';
+        };
+      };
+
+      description-scr = write_shell {
+        name = "description";
+        path = "specific/ytcc";
+        dependencies = builtins.attrValues {
+          inherit (prev) jq fmt less locale;
+        };
+      };
+
+      fupdate-scr = write_shell {
+        name = "fupdate";
+        path = "apps";
+        keepPath = true;
+        completions = true;
+        dependencies = builtins.attrValues {
+          inherit
+            (prev)
+            dash
+            nix
+            gnugrep
+            fd
+            coreutils
+            bat # used by batgrep
+            gnused # required by batgrep
+            git # needed to fetch through git
+            ;
+          inherit (prev.bat-extras) batgrep;
+        };
+      };
+
+      git-edit-index-scr = write_shell {
+        name = "git-edit-index";
+        path = "apps";
+        completions = true;
+        # This starts neovim, wich might want to shell out
+        keepPath = true;
+        dependencies = builtins.attrValues {
+          inherit
+            (prev)
+            git
+            gnused
+            # $EDITOR
+
+            ;
+        };
+      };
+
+      hibernate-scr = write_shell {
+        name = "hibernate";
+        path = "wrappers";
+        dependencies = builtins.attrValues {
+          inherit
+            (prev)
+            systemd
+            taskwarrior
+            ;
+        };
+      };
+
+      ll-scr = sysLib.writeShellScript {
+        name = "ll";
+        src = ./source/wrappers/ll.sh;
+        wrap = false;
+      };
+
+      # TODO: this need to be replaced with a wayland alternative
+      #  llp-scr = write_shell {
+      #     name = "llp";
+      #     path = "wrappers";
+      #     dependencies = builtins.attrValues {inherit (prev) lf ueberzug;};
+      #   };
+
+      lock-scr = write_shell {
+        name = "lock";
+        path = "wrappers";
+        dependencies = builtins.attrValues {
+          inherit
+            (prev)
+            taskwarrior
+            swaylock
+            ;
+        };
+      };
+
+      lyrics-scr = write_shell {
+        name = "lyrics";
+        path = "wrappers";
+        dependencies = builtins.attrValues {
+          inherit
+            (prev)
+            exiftool
+            mpc-cli
+            jq
+            less
+            locale # dependency of less
+            ;
+        };
+      };
+
+      mpc-fav-scr = write_shell {
+        name = "mpc-fav";
+        path = "wrappers";
+        dependencies = builtins.attrValues {
+          inherit
+            (prev)
+            mpc-cli
+            ;
+        };
+      };
+
+      mpc-rm-scr = write_shell {
+        name = "mpc-rm";
+        path = "wrappers";
+        dependencies = builtins.attrValues {
+          inherit
+            (prev)
+            mpc-cli
+            trash-cli
+            ;
+        };
+      };
+
+      mpc-scr = write_shell {
+        name = "mpc";
+        path = "wrappers";
+        dependencies = [
+          mpc-fav-scr
+          mpc-rm-scr
+          prev.mpc-cli
+        ];
+      };
+
+      nato-scr = write_python {
+        name = "nato";
+        path = "small_functions";
+        dependencies_python = ps: [];
+      };
+
+      neorg-scr = sysLib.writeShellScriptMultiPart {
+        name = "neorg";
+        keepPath = true;
+        src = ./source/specific/neorg/sh;
+        baseName = "main.sh";
+        cmdPrefix = "functions";
+        cmdNames = [
+          "add.sh"
+          "context.sh"
+          "dmenu.sh"
+          "f_start.sh"
+          "f_stop.sh"
+          "list.sh"
+          "project.sh"
+          "review.sh"
+          "utils.sh"
+          "workspace.sh"
+        ];
+        dependencies = with prev; [
+          cocogitto
+          rofi
+          libnotify
+        ];
+        generateCompletions = true;
+        replacementStrings = {
+          DEFAULT_NEORG_PROJECT_DIR =
+            homeConfig.programs.nixvim.plugins.neorg.modules."core.dirman".config.workspaces.projects;
+          HOME_TASKRC = "${homeConfig.xdg.configHome}/task/home-manager-taskrc";
+          NEORG_REVIEW_PATH = "${homeConfig.xdg.dataHome}/neorg/review";
+          ALL_PROJECTS_NEWLINE = "${homeConfig.soispha.taskwarrior.projects.projects_newline}";
+          ALL_PROJECTS_COMMA = "${homeConfig.soispha.taskwarrior.projects.projects_comma}";
+          ALL_PROJECTS_PIPE = "${homeConfig.soispha.taskwarrior.projects.projects_pipe}";
+          ALL_WORKSPACES = "${lib.strings.concatStringsSep "|" (builtins.attrNames homeConfig.programs.nixvim.plugins.neorg.modules."core.dirman".config.workspaces)}";
+          ID_GENERATION_FUNCTION = "${sysLib.writeShellScript {
+            name = "neorg_id_function";
+            src = ./source/specific/neorg/neorg_id_function.sh;
+            dependencies = with prev; [
+              taskwarrior
+              gawk
+              findutils # xargs
+            ];
+          }}/bin/neorg_id_function";
+
+          # TODO: Replace the hard-coded path here with some reference <2023-10-20>
+          TASK_PROJECT_FILE = "/home/soispha/repos/nix/nixos-config/hm/soispha/conf/taskwarrior/projects/default.nix";
+        };
+      };
+
+      screenshot_persistent-scr = write_shell {
+        name = "screenshot_persistent";
+        path = "small_functions";
+        keepPath = true;
+        dependencies = builtins.attrValues {
+          inherit
+            (prev)
+            grim
+            slurp
+            alacritty
+            rofi
+            libnotify
+            lf # TODO: add llp
+            ;
+        };
+      };
+
+      screenshot_temporary-scr = write_shell {
+        name = "screenshot_temporary";
+        path = "small_functions";
+        dependencies = builtins.attrValues {inherit (prev) grim slurp wl-clipboard;};
+      };
+
+      show-scr = write_shell {
+        name = "show";
+        path = "wrappers";
+        keepPath = true; # I might want to use nvim in less (and shell escapes)
+        dependencies = builtins.attrValues {inherit (prev) less locale;};
+      };
+
+      sort_song-scr = write_shell {
+        name = "sort_song";
+        path = "wrappers";
+        dependencies = builtins.attrValues {inherit (prev) mediainfo jq gawk;};
+      };
+
+      spodi-scr = sysLib.writeShellScriptMultiPart {
+        name = "spodi";
+        keepPath = false;
+        src = ./source/specific/spodi;
+        baseName = "spodi.sh";
+        cmdPrefix = "sh";
+        cmdNames = [
+          "download.sh"
+          "update.sh"
+        ];
+        dependencies = with prev; [
+          gawk
+          expect
+          spotdl
+          fd
+          coreutils
+        ];
+        generateCompletions = true;
+        replacementStrings = {
+          XDG_CACHE_HOME = homeConfig.xdg.cacheHome;
+          XDG_MUSIC_DIR = homeConfig.xdg.userDirs.music;
+        };
+      };
+
+      update-sys-scr = write_shell {
+        name = "update-sys";
+        path = "small_functions";
+        completions = true;
+        dependencies = builtins.attrValues {
+          inherit
+            (prev)
+            git
+            nixos-rebuild
+            sudo
+            openssh
+            coreutils
+            mktemp
+            gnugrep
+            gnused
+            systemd
+            ;
+        };
+      };
+
+      virsh-del-scr = write_shell {
+        name = "virsh-del";
+        path = "wrappers";
+        dependencies = builtins.attrValues {inherit (prev) libvirt;};
+      };
+
+      yti-scr = write_shell {
+        name = "yti";
+        path = "wrappers";
+        dependencies = builtins.attrValues {inherit (prev) gawk expect yt-dlp;};
+      };
+    in {
+      scripts = {
+        # llp = llp-scr; # TODO: see above
+        aumo = aumo-scr;
+        battery = battery-scr;
+        brightness = brightness-scr;
+        con2pdf = con2pdf-scr;
+        description = description-scr;
+        fupdate = fupdate-scr;
+        git-edit-index = git-edit-index-scr;
+        hibernate = hibernate-scr;
+        ll = ll-scr;
+        lock = lock-scr;
+        lyrics = lyrics-scr;
+        mpc = mpc-scr;
+        mpc-fav = mpc-fav-scr;
+        mpc-rm = mpc-rm-scr;
+        nato = nato-scr;
+        neorg = neorg-scr;
+        screenshot_persistent = screenshot_persistent-scr;
+        screenshot_temporary = screenshot_temporary-scr;
+        show = show-scr;
+        sort_song = sort_song-scr;
+        spodi = spodi-scr;
+        update-sys = update-sys-scr;
+        virsh-del = virsh-del-scr;
+        yti = yti-scr;
+      };
+    }
+  )
+]
+
+
+    pkgs = import nixpkgs (import ./sys/nixpkgs {
+      inherit (nixpkgs) lib;
+      inherit system sysLib;
+
+      # FIXME: Don't unconditionally use tiamat here <2024-02-24>
+      homeConfig = self.nixosConfigurations.tiamat.config.home-manager.users.soispha;
+      nixosConfig = self.nixosConfigurations.tiamat.config;
+      overlays = [];
+    });
diff --git a/pkgs/sources/scripts/source/apps/aumo.sh b/pkgs/sources/scripts/source/apps/aumo.sh
new file mode 100755
index 00000000..84d39deb
--- /dev/null
+++ b/pkgs/sources/scripts/source/apps/aumo.sh
@@ -0,0 +1,28 @@
+#! /usr/bin/env dash
+
+# shellcheck source=/dev/null
+SHELL_LIBRARY_VERSION="2.1.2" . %SHELL_LIBRARY_PATH
+
+unmounting() {
+    disk_name="$(find /dev/disk/by-label -type l -printf "%P|" | rofi -sep "|" -dmenu -p "Select disk to mount")"
+
+    udisksctl unmount --block-device "/dev/disk/by-label/$disk_name"
+}
+
+mounting() {
+    disk_name="$(find /dev/disk/by-label -type l -printf "%P|" | rofi -sep "|" -dmenu -p "Select disk to mount")"
+
+    udisksctl mount --block-device "/dev/disk/by-label/$disk_name"
+}
+
+case "$1" in
+"mount")
+    mounting
+    ;;
+"unmount" | "umount")
+    unmounting
+    ;;
+*)
+    die "Usage: $NAME mount|unmount"
+    ;;
+esac
diff --git a/pkgs/sources/scripts/source/apps/con2pdf.sh b/pkgs/sources/scripts/source/apps/con2pdf.sh
new file mode 100755
index 00000000..08bf8998
--- /dev/null
+++ b/pkgs/sources/scripts/source/apps/con2pdf.sh
@@ -0,0 +1,234 @@
+#! /usr/bin/env dash
+
+# shellcheck source=/dev/null
+SHELL_LIBRARY_VERSION="2.1.2" . %SHELL_LIBRARY_PATH
+
+# needed for help() and version
+# shellcheck disable=2034
+AUTHORS="Soispha"
+# shellcheck disable=2034
+YEARS="2023"
+# shellcheck disable=2034
+VERSION="1.0.0"
+
+# NAME is from the wrapper
+# shellcheck disable=SC2269
+NAME="$NAME"
+help() {
+    cat <<EOF
+Scan images and turn them into a pdf.
+
+Usage:
+    $NAME [OPTIONS] --name --device
+
+OPTIONS:
+    --out-dir | -o [FILE]
+                            Path to place the generated pdf files (default: ./pdf).
+
+    --name | -n NAME
+                            Name for the pdf files (e.g. <NAME>_1.pdf).
+
+    --num-pages | -p NUM
+                            Number of pages to merge into one pdf (default: 1).
+
+    --device | -d DEVICE
+                            Device used for scanning.
+
+    --method | -m METHOD
+                            Method to use for scanning (default: ADF).
+
+    --help | -h
+                            Display this help and exit.
+
+    --version | -v
+                            Display version and copyright information and exit.
+ARGUMENTS:
+    FILE := [[fd . --max-depth 3]]
+                            A name of a file to store, default is: ./pdf
+
+    NAME | * := [[fd . --max-depth 3]]
+                            The basename of the generated files
+
+    NUM | *([0-9]) := 0 | 1 | 2 | 3 | 4
+                            Possible numbers of pages, can be more than 4
+
+    DEVICE := [[$(cat %DEVICE_FUNCTION)]]
+                            Possible scanner names
+
+    METHOD := ADF | Flatbed
+                            The scanning method to use, not all scanners support both of
+                            these. The default is ADF
+EOF
+}
+
+scan_adf() {
+    device="$1"
+    sides_per_page="$2"
+    method="ADF"
+    for i in $(seq "$sides_per_page"); do
+        do_until_success \
+            "scanimage --format=tiff --progress --source='$method' --device='$device' --batch=%d.tif --batch-increment='$sides_per_page' --batch-start='$i'" \
+            "warn 'Retrying scan, as we assume a network error!'"
+
+        if [ "$sides_per_page" -ne 1 ]; then
+            msg "Finished turn, please change side!"
+            readp "Press enter to continue" noop
+        fi
+    done
+}
+process_images_adf() {
+    tiff_temp_path="$1"
+    output_directory="$2"
+    name="$3"
+
+    counter=0
+    pdf_counter=0
+    image_cache="$(mktmp)"
+    while read -r scanned_image; do
+        dbg "$scanned_image (scanned_image) at $counter (counter)"
+        echo "$scanned_image" >>"$image_cache"
+        : $((counter += 1))
+        if [ "$counter" = "$number_of_pages" ]; then
+            dbg "$counter == $number_of_pages"
+            counter=0
+            convert_images "$image_cache" "${name}_$pdf_counter" "$output_directory"
+            : $((pdf_counter += 1))
+            printf "" >"$image_cache"
+        fi
+    done <"$(tmp_pipe fd . "$tiff_temp_path" "|" sort -V)"
+}
+
+scan_flatbed() {
+    device="$1"
+    number_of_pages"$2"
+    method="Flatbed"
+    for i in $(seq "$number_of_pages"); do
+        do_until_success \
+            "scanimage --format=tiff --progress --source='$method' --device='$device' --output-file=$i.tiff" \
+            "warn 'Retrying scan, as we assume a network error!'"
+        if [ "$number_of_pages" -ne 1 ]; then
+            msg "Finished turn, please change side!"
+            readp "Press enter to continue" noop
+        fi
+    done
+}
+process_images_flatbed() {
+    tiff_temp_path="$1"
+    output_directory="$2"
+    name="$3"
+
+    counter=0
+    image_cache="$(mktmp)"
+    while read -r scanned_image; do
+        echo "$scanned_image" >>"$image_cache"
+        : $((counter += 1))
+        if [ "$counter" = "$number_of_pages" ]; then
+            counter=0
+            convert_images "$image_cache" "$name" "$output_directory"
+            printf "" >"$image_cache"
+        fi
+    done <"$(tmp_pipe fd . "$tiff_temp_path" "|" sort -V)"
+}
+convert_images() {
+    image_cache="$1"
+    pdf_name="$2"
+    output_dir="$3"
+
+    set --
+    while read -r image; do
+        dbg "setting image: $image"
+        set -- "$@" "$image"
+    done <"$image_cache"
+
+    while [ -e "$output_dir/${pdf_name}.pdf" ]; do
+        pdf_name="${pdf_name}_$(tr -dc 'A-Za-z0-9' </dev/urandom | head -c 25)"
+    done
+    dbg "using pdf_name: $pdf_name"
+    convert "$@" -compress jpeg -quality 100 "$output_dir/${pdf_name}.pdf"
+}
+
+scan() {
+    number_of_pages="$1"
+    device="$2"
+    output_directory="$(readlink -f "$3")"
+    name="$4"
+    method="$5"
+
+    [ -z "$number_of_pages" ] && die "Parameter 'number_of_pages' is not set!"
+    [ -z "$device" ] && die "Parameter 'device' is not set!"
+    [ -z "$output_directory" ] && die "Parameter 'output_directory' is not set!"
+    [ -z "$name" ] && die "Parameter 'name' is not set!"
+    [ -z "$method" ] && die "Parameter 'method' is not set!"
+
+    tiff_temp_path="$(mktmp -d)"
+    cd "$tiff_temp_path" || die "Bug"
+
+    msg "Started scanning..."
+    if [ "$method" = "Flatbed" ]; then
+        scan_flatbed "$device" "$number_of_pages"
+    else
+        scan_adf "$device" "$number_of_pages"
+    fi
+
+    msg "Creating output directory..."
+    mkdir "$output_directory"
+    cd "$output_directory" || die "Bug"
+
+    msg "Converting images to pdfs..."
+    if [ "$method" = "Flatbed" ]; then
+        process_images_flatbed "$tiff_temp_path" "$output_directory" "$name"
+    else
+        process_images_adf "$tiff_temp_path" "$output_directory" "$name"
+    fi
+}
+
+for input in "$@"; do
+    case "$input" in
+    "--help" | "-h")
+        help
+        exit 0
+        ;;
+    "--version" | "-v")
+        version
+        exit 0
+        ;;
+    esac
+done
+
+number_of_pages="1"
+unset device
+output_directory="$(pwd)/pdf"
+unset name
+method="ADF"
+
+while [ "$#" -ne 0 ]; do
+    case "$1" in
+    "--help" | "-h") ;;
+    "--version" | "-v") ;;
+    "--out-dir" | "-o")
+        shift 1
+        output_directory="$1"
+        ;;
+    "--name" | "-n")
+        shift 1
+        name="$1"
+        ;;
+    "--num-pages" | "-p")
+        shift 1
+        number_of_pages="$1"
+        ;;
+    "--device" | "-d")
+        shift 1
+        device="$1"
+        ;;
+    "--method" | "-m")
+        shift 1
+        method="$1"
+        ;;
+    *)
+        die "Command line arg $1 does not exist. See --help for a list."
+        ;;
+    esac
+    shift 1
+done
+scan "$number_of_pages" "$device" "$output_directory" "$name" "$method"
diff --git a/pkgs/sources/scripts/source/apps/fupdate.1.md b/pkgs/sources/scripts/source/apps/fupdate.1.md
new file mode 100644
index 00000000..710e8fb7
--- /dev/null
+++ b/pkgs/sources/scripts/source/apps/fupdate.1.md
@@ -0,0 +1,70 @@
+% FUPDATE(1) fupdate 1.0.0
+% Soispha
+% May 2023
+
+# NAME
+
+fupdate - updates your flake, while checking for common mistakes
+
+# SYNOPSIS
+
+**fupdate** list of \[*flake*|*\<some word>*|*--help*|*-h*\]
+
+# DESCRIPTION
+
+Argument can be stacked, this makes it possible to specify multiple targets to be updated in succession. See the Examples section for further details.
+
+No argument or *flake*
+: **fupdate**, when executed without arguments or with *flake*, will update your *flake.lock*, check for duplicate flake inputs, i.e., an input has an input declared, which you have also declared as input, and will run a script called *update.sh*, if you allow it.
+The allowance for the script is asked, when you run **fupdate** and the found script is not yet allowed. Furthermore, the allowance is based on the concrete sha256 hash of the script, so any changes will require another allowance.
+
+**\<some word>** as argument
+: If the executable **update-\<some word>** is reachable thought the PATH variable, than this is run. Otherwise, the program will exit.
+
+# OPTIONS
+
+**--help**, **-h**
+: Displays a help message and exit.
+
+**--version**, **-v**
+: Displays the software version and exit.
+
+# EXAMPLES
+
+**fupdate** or **fupdate flake**
+: Updates your *flake.lock*. See the Description section for further details.
+
+**fupdate sys**
+: Run the executable **update-sys**, if it exists. See the Description section for further details.
+
+**fupdate flake sys docs**
+: First updates your flake, then, if the command succeeded, runs **update-sys**, afterweich **update-docs** is run.
+
+# FILES
+
+*update.sh*
+: This is supposed to be a shell script located in your flake base directory, i.e., the directory which contains both a *flake.nix* and a *flake.lock* file.
+
+*~/.local/share/flake-update/*
+: **fupdate** will store the hashes to the allowed *update.sh* files here.
+
+# BUGS
+
+Report bugs to <https://codeberg.org/soispha/flake_update/issues>.
+
+# COPYRIGHT
+
+Copyright (C) 2023  Soispha
+
+This program is free software: you can redistribute it and/or modify
+it under the terms of the GNU General Public License as published by
+the Free Software Foundation, either version 3 of the License, or
+(at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program.  If not, see <https://www.gnu.org/licenses/>.
diff --git a/pkgs/sources/scripts/source/apps/fupdate.sh b/pkgs/sources/scripts/source/apps/fupdate.sh
new file mode 100755
index 00000000..4322610a
--- /dev/null
+++ b/pkgs/sources/scripts/source/apps/fupdate.sh
@@ -0,0 +1,197 @@
+#! /usr/bin/env dash
+
+# shellcheck source=/dev/null
+SHELL_LIBRARY_VERSION="2.1.2" . %SHELL_LIBRARY_PATH
+
+UPDATE_SCRIPT_NAME="update.sh"
+CONFIG_DIRECTORY_PATH="$HOME/.local/share/flake-update"
+
+# Both are used in version()
+# shellcheck disable=SC2034
+AUTHORS="Soispha"
+# shellcheck disable=SC2034
+YEARS="2023"
+
+UPDATE_SCRIPT_NOT_WANTED=false
+
+# Searches upward for a `UPDATE_SCRIPT_NAME` script
+# Returns a path to the script if it exists, otherwise nothing is returned
+check_for_update_script() {
+    dirname="$(search_upward_files "$UPDATE_SCRIPT_NAME")"
+    if [ "$dirname" ]; then
+        printf "%s/%s" "$dirname" "$UPDATE_SCRIPT_NAME"
+    fi
+}
+
+# Checks if a given path to the update script is allowed.
+# Takes the path as input
+# Return 0, if allowed, 1 if not.
+check_for_allowed_update_script() {
+    update_script="$1"
+    config_path="${CONFIG_DIRECTORY_PATH}${update_script}"
+    update_script_hash="$(sha256sum "$update_script")"
+    if [ -f "$config_path" ]; then
+        if [ "$(cat "$config_path")" = "$update_script_hash" ]; then
+            dbg "Recorded hash matches"
+            return 0
+        else
+            dbg "Recorded hash \'$(cat "$config_path")\' does not match real hash \'$update_script_hash\', assuming not allowed"
+            return 1
+        fi
+    else
+        dbg "Path \'$config_path\' does not exist, assuming not allowed"
+        return 1
+    fi
+}
+
+# Asks the user if they want to allow a given script.
+# Takes the path as input
+ask_to_allow_update_script() {
+    update_script="$1"
+    config_path="${CONFIG_DIRECTORY_PATH}${update_script}"
+    update_script_hash="$(sha256sum "$update_script")"
+    println "\033[2J" # clear the screen
+    cat "$update_script"
+    readp "Do you want to allow this script?[N/y]: " allow
+    # shellcheck disable=SC2154
+    dbg "allow is: $allow"
+    case "$allow" in
+    [yY])
+        dbg "allowed script"
+        dbg "storing contents in: $config_path"
+        mkdir --parents "$(dirname "$config_path")"
+        print "$update_script_hash" >"$config_path"
+        ;;
+    *)
+        UPDATE_SCRIPT_NOT_ALLOWED=true
+        ;;
+    esac
+}
+
+# Runs the provided script and continues to update the nix flake
+# Takes the path to the script and the directory to the flake as arguments
+# If the path to the update script is empty, it will be ignored
+update() {
+    update_script="$1"
+    flake_base_dir="$2"
+    shift 2
+    dbg "Provided following args to update script: '$*'"
+
+    cd "$flake_base_dir" || die "Provided dir \'$flake_base_dir\' can not be accessed"
+    dbg "changed directory to: $flake_base_dir"
+
+    nix flake update
+
+    if ! [ "$update_script" = "" ] && ! [ "$UPDATE_SCRIPT_NOT_WANTED" = "true" ]; then
+        "$update_script" "$@"
+    fi
+
+    if grep '[^0-9]_[0-9]' flake.lock >/dev/null; then
+        batgrep '[^0-9]_[0-9]' flake.lock
+        die "Your flake.nix contains duplicate inputs!"
+    fi
+}
+
+help() {
+    cat <<EOF
+This is a Nix flake update manager.
+
+USAGE:
+    $NAME [--help | --version] [flake [--no-script] | <some other command>]
+
+OPTIONS:
+    --help   | -h
+                            Display this help and exit.
+
+    --version   | -v
+                            Display version and copyright information and exit.
+
+    --no-script
+                            Avoid running the 'update.sh' script
+COMMANDS:
+    flake
+                            update the flake project
+
+    <some other command>
+                            runs a executable called "update-<some other command>", if it exists
+EOF
+}
+
+main() {
+    if ! [ "$UPDATE_SCRIPT_NOT_ALLOWED" = true ]; then
+        update_script="$(check_for_update_script)"
+        flake_base_dir="$(search_flake_base_dir)" # Assume, that the update script is in the base dir
+        dbg "update_script is: $update_script"
+        dbg "flake_base_dir is: $flake_base_dir"
+
+        if [ "$update_script" = "" ]; then
+            update "" "$flake_base_dir" "$@"
+        elif check_for_allowed_update_script "$update_script" && ! [ "$update_script" = "" ]; then
+            update "$update_script" "$flake_base_dir" "$@"
+        else
+            ask_to_allow_update_script "$update_script"
+            main "$@"
+        fi
+    fi
+}
+
+if [ "$#" -eq 0 ]; then
+    main
+    exit 0
+fi
+
+for input in "$@"; do
+    case "$input" in
+    "--help" | "-h")
+        help
+        exit 0
+        ;;
+    "--version" | "-v")
+        version
+        exit 0
+        ;;
+    "--no-script" | "-n")
+        UPDATE_SCRIPT_NOT_WANTED=true
+        ;;
+    "--")
+        end_of_cli_options=true
+
+        # Stop processing args after that marker.
+        break
+        ;;
+    esac
+    [ "$end_of_cli_options" = "true" ] && break
+done
+
+case "$1" in
+"flake")
+    shift 1
+
+    # Filter out fupdate specific flags
+    while [ "$1" != "--" ]; do
+        # FIXME: This check allows to add a flag multiple times, but this should probably
+        # not be allowed <2024-03-29>
+        case "$1" in
+        "--no-script" | "-n")
+            shift 1
+            ;;
+        *)
+            break
+            ;;
+        esac
+    done
+
+    [ "$1" = "--" ] && shift 1
+    main "$@"
+    ;;
+*)
+    command="$1"
+    shift 1
+    [ "$1" = "--" ] && shift 1
+    if which update-"$command" >/dev/null 2>&1; then
+        update-"$command" "$@"
+    else
+        die "command \"update-$command\" is not executable, or does not exist"
+    fi
+    ;;
+esac
diff --git a/pkgs/sources/scripts/source/apps/git-edit-index.sh b/pkgs/sources/scripts/source/apps/git-edit-index.sh
new file mode 100755
index 00000000..e73dc53c
--- /dev/null
+++ b/pkgs/sources/scripts/source/apps/git-edit-index.sh
@@ -0,0 +1,98 @@
+#!/usr/bin/env dash
+
+# shellcheck source=/dev/null
+SHELL_LIBRARY_VERSION="2.1.2" . %SHELL_LIBRARY_PATH
+
+# needed for help() and version
+# shellcheck disable=2034
+AUTHORS="Soispha"
+# shellcheck disable=2034
+YEARS="2024"
+# shellcheck disable=2034
+VERSION="1.0.0"
+
+# NAME is from the wrapper
+# shellcheck disable=SC2269
+NAME="$NAME"
+
+help() {
+    cat <<EOF
+Edit a file from the index. This script does not touch the unstaged variant of the file.
+
+USAGE:
+    $NAME [OPTIONS] [--] FILES..
+
+OPTIONS:
+    --
+                            Stop parsing options and interpret everything as an file.
+
+    --help | -h
+                            Display this help and exit.
+
+    --version | -v
+                            Display version and copyright information and exit.
+ARGUMENTS:
+    FILES := [[ git diff --name-only --cached --diff-filter=AM ]]
+                            The files to edit.
+
+EOF
+}
+
+GIT_DIR="$(git rev-parse --show-toplevel)"
+materialize_file() {
+    git diff --cached "$1" >"$GIT_DIR/.git/EDIT_INDEX_PATCH"
+
+    git add "$1"
+    git restore --staged "$1"
+    cat "$1" >"$GIT_DIR/.git/EDIT_INDEX_FILE"
+    git restore "$1"
+
+    git apply "$GIT_DIR/.git/EDIT_INDEX_PATCH"
+    "$EDITOR" "$1"
+
+    git add "$1"
+    mv "$GIT_DIR/.git/EDIT_INDEX_FILE" "$1"
+}
+
+edit() {
+    files_to_add="$(mktmp)"
+    realpath --relative-to=. "$@" >"$files_to_add"
+
+    index_files="$(mktmp)"
+    git diff --name-only --cached --diff-filter=AM >"$index_files"
+
+    while read -r file; do
+        if grep -q "$file" "$files_to_add"; then
+            sed -i "s|$file||" "$files_to_add"
+            materialize_file "$file"
+        fi
+    done <"$index_files"
+
+    files_to_check="$(mktmp)"
+    clean "$files_to_add" >"$files_to_check"
+    if [ "$(wc -l <"$files_to_check")" -gt 0 ]; then
+        warn "Could not edit every file:"
+        cat "$files_to_add"
+    fi
+}
+
+for arg in "$@"; do
+    case "$arg" in
+    "--help" | "-h")
+        help
+        exit 0
+        ;;
+    "--version" | "-v")
+        version
+        exit 0
+        ;;
+    "--")
+        end_of_cli_options=true
+        ;;
+    esac
+    [ "$end_of_cli_options" = "true" ] && break
+done
+
+edit "$@"
+
+# vim: ft=sh
diff --git a/pkgs/sources/scripts/source/small_functions/brightness.sh b/pkgs/sources/scripts/source/small_functions/brightness.sh
new file mode 100755
index 00000000..a7272279
--- /dev/null
+++ b/pkgs/sources/scripts/source/small_functions/brightness.sh
@@ -0,0 +1,80 @@
+#!/usr/bin/env dash
+
+# shellcheck source=/dev/null
+SHELL_LIBRARY_VERSION="2.1.2" . %SHELL_LIBRARY_PATH
+
+help() {
+    cat <<EOF
+This is a system brightness manager
+
+USAGE:
+    $NAME up [VALUE] | down [VALUE]
+
+OPTIONS:
+    --help   | -h
+                                Output this help and exit.
+
+    --version   | -v
+                                Output the version and exit.
+
+COMMANDS:
+    up [VALUE]
+                                Increase the brightness by VALUE or 5%.
+
+    down [VALUE]
+                                Decrease the brightness by VALUE or 5%.
+
+ARGUMENTS:
+    VALUE := [[seq 0 100]]
+                                The amount to increase/decrease the brightness. In percentage
+EOF
+}
+
+BACKLIGHT="/sys/class/%BACKLIGHT_NAME"
+
+brightness() {
+    offset="$1"
+
+    max="$(cat $BACKLIGHT/max_brightness)"
+    cur="$(cat $BACKLIGHT/brightness)"
+    percentage="$(echo | awk --assign=cur="$cur" --assign=max="$max" '{printf cur / max}')"
+
+    new="$(echo | awk --assign=per="$percentage" --assign=offset="$offset" '{printf per + (offset / 10)}')"
+
+    output="$(echo | awk --assign=new="$new" --assign=max="$max" '{printf max * new}')"
+
+    msg "echo \"$output\" > $BACKLIGHT/brightness"
+}
+
+for arg in "$@"; do
+    case "$arg" in
+    "--help" | "-h")
+        help
+        exit 0
+        ;;
+    "--version" | "-v")
+        version
+        exit 0
+        ;;
+    esac
+done
+
+case "$1" in
+"up")
+    shift 1
+    value="5"
+    [ -n "$1" ] && value="$1"
+    brightness "+$value"
+    ;;
+"down")
+    shift 1
+    value="-5"
+    [ -n "$1" ] && value="$1"
+    brightness "-$value"
+    ;;
+*)
+    die "The command '$1' does not exist! See '--help' for a list"
+    ;;
+esac
+
+# vim: ft=sh
diff --git a/pkgs/sources/scripts/source/small_functions/nato.py b/pkgs/sources/scripts/source/small_functions/nato.py
new file mode 100755
index 00000000..e9d15f56
--- /dev/null
+++ b/pkgs/sources/scripts/source/small_functions/nato.py
@@ -0,0 +1,106 @@
+#!/usr/bin/env python3
+# originally from here: https://cgit.pacien.net/desktop-utilities/
+
+import sys
+
+alphabet = {
+    "nato": {
+        "A": "Alfa",  # No idea why this is not just 'Alpha' ..
+        "B": "Bravo",
+        "C": "Charlie",
+        "D": "Delta",
+        "E": "Echo",
+        "F": "Foxtrot",
+        "G": "Golf",
+        "H": "Hotel",
+        "I": "India",
+        "J": "Juliett",
+        "K": "Kilo",
+        "L": "Lima",
+        "M": "Mike",
+        "N": "November",
+        "O": "Oscar",
+        "P": "Papa",
+        "Q": "Quebec",
+        "R": "Romeo",
+        "S": "Sierra",
+        "T": "Tango",
+        "U": "Uniform",
+        "V": "Victor",
+        "W": "Whiskey",
+        "X": "X-ray",
+        "Y": "Yankee",
+        "Z": "Zulu",
+        "0": "Nadazero",
+        "1": "Unaone",
+        "2": "Bissotwo",
+        "3": "Terrathree",
+        "4": "Kartefour",
+        "5": "Pantafive",
+        "6": "Soxisix",
+        "7": "Setteseven",
+        "8": "Oktoeight",
+        "9": "Novenine",
+        ",": "Comma",
+        "/": "Forward slash",
+        ".": "Stop/Decimal",
+    },
+    "german": {
+        "A": "Aachen",
+        "Ä": "Umlaut Aachen",
+        "B": "Berlin",
+        "C": "Chemnitz",
+        "D": "Düsseldorf",
+        "E": "Essen",
+        "F": "Frankfurt",
+        "G": "Goslar",
+        "H": "Hamburg",
+        "I": "Ingelheim",
+        "J": "Jena",
+        "K": "Köln",
+        "L": "Leipzig",
+        "M": "München",
+        "N": "Nürnberg",
+        "O": "Offenbach",
+        "Ö": "Umlaut Offenbach",
+        "P": "Potsdam",
+        "Q": "Quickborn",
+        "R": "Rostock",
+        "S": "Salzwedel",
+        "ẞ": "Eszett",
+        "T": "Tübingen",
+        "U": "Unna",
+        "Ü": "Umlaut Unna",
+        "V": "Völklingen",
+        "W": "Wuppertal",
+        "X": "Xanten",
+        "Y": "Ypsilon",
+        "Z": "Zwickau",
+    },
+}
+
+
+def str_to_telephony(phrase, language):
+    language_alphabet = alphabet[language]
+
+    return [
+        language_alphabet[c] if c in language_alphabet else c for c in phrase.upper()
+    ]
+
+
+language = sys.argv[1]
+if language not in ["nato", "german"]:
+    print(
+        f"Langugae '{language}' is not a valid language, only 'nato' and 'german' are!",
+        file=sys.stderr,
+    )
+    exit(1)
+
+print(
+    "\n".join(
+        str_to_telephony(
+            " ".join(sys.argv[2:]),
+            language,
+        )
+    )
+)
diff --git a/pkgs/sources/scripts/source/small_functions/screenshot_persistent.sh b/pkgs/sources/scripts/source/small_functions/screenshot_persistent.sh
new file mode 100755
index 00000000..4308b8d2
--- /dev/null
+++ b/pkgs/sources/scripts/source/small_functions/screenshot_persistent.sh
@@ -0,0 +1,22 @@
+#! /usr/bin/env dash
+
+# shellcheck source=/dev/null
+SHELL_LIBRARY_VERSION="2.1.2" . %SHELL_LIBRARY_PATH
+
+# only generate a path (this could lead to a time-of-check/time-of-use bug)
+tmp="$(mktmp --dry-run)"
+
+if grim -g "$(slurp)" "$tmp"; then
+    name="$(rofi -dmenu -p "Name of screenshot: " -l 0)"
+    screen_shot_path="$HOME/media/pictures/screenshots/$name.png"
+    while [ -f "$screen_shot_path" ]; do
+        notify-send "Warning" 'Screenshot name already in use!'
+        name="$(rofi -dmenu -p "New name of screenshot: " -l 0)"
+        screen_shot_path="$HOME/media/pictures/screenshots/$name.png"
+    done
+
+    mv "$tmp" "$screen_shot_path"
+    alacritty -e lf -command ":{{ set sortby atime; set reverse!; }}"
+fi
+
+# vim: ft=sh
diff --git a/pkgs/sources/scripts/source/small_functions/screenshot_temporary.sh b/pkgs/sources/scripts/source/small_functions/screenshot_temporary.sh
new file mode 100755
index 00000000..8968ca79
--- /dev/null
+++ b/pkgs/sources/scripts/source/small_functions/screenshot_temporary.sh
@@ -0,0 +1,8 @@
+#! /usr/bin/env dash
+
+# shellcheck source=/dev/null
+SHELL_LIBRARY_VERSION="2.1.2" . %SHELL_LIBRARY_PATH
+
+grim -g "$(slurp)" | wl-copy
+
+# vim: ft=sh
diff --git a/pkgs/sources/scripts/source/small_functions/update-sys.sh b/pkgs/sources/scripts/source/small_functions/update-sys.sh
new file mode 100755
index 00000000..d28247f6
--- /dev/null
+++ b/pkgs/sources/scripts/source/small_functions/update-sys.sh
@@ -0,0 +1,85 @@
+#!/usr/bin/env dash
+
+# shellcheck source=/dev/null
+SHELL_LIBRARY_VERSION="2.1.2" . %SHELL_LIBRARY_PATH
+
+help() {
+    cat <<EOF
+This is a NixOS System flake update manager.
+
+USAGE:
+    $NAME [--branch <branchname>] [--help]
+
+OPTIONS:
+    --branch | -b  BRANCHNAME
+                                select a branch to update from.
+
+    --mode   | -m  MODE
+                                select a mode to update with
+
+    --help   | -h
+                                output this help.
+ARGUMENTS:
+    BRANCHNAME := [[ git branch --list --format '%(refname:short)' ]]
+                                The name of the branch to deploy the config from
+
+    MODE := switch|boot|test|build|dry-build|dry-activate|edit|repl|build-vm|build-vm-with-bootloader
+                                See the 'nixos-rebuild' manpage for more information about these modes.
+EOF
+    exit "$1"
+}
+default_branch=$(mktmp)
+BRANCH=""
+
+while [ "$#" -gt 0 ]; do
+    case "$1" in
+    "--help" | "-h")
+        help 0
+        ;;
+    "--branch" | "-b")
+        if [ -n "$2" ]; then
+            BRANCH="$2"
+        else
+            error "$1 requires an argument"
+            help 1
+        fi
+        shift 2
+        ;;
+    "--mode" | "-m")
+        if [ -n "$2" ]; then
+            MODE="$2"
+        else
+            error "$1 requires an argument"
+            help 1
+        fi
+        shift 2
+        ;;
+    *)
+        error "the option $1 does not exist!"
+        help 1
+        ;;
+    esac
+done
+
+cd /etc/nixos || die "No /etc/nixos"
+msg "Starting system update..."
+git remote update origin --prune >/dev/null 2>&1
+if ! [ "$BRANCH" = "" ]; then
+    git switch "$BRANCH" >/dev/null 2>&1 && msg2 "Switched to branch '$BRANCH'"
+fi
+msg2 "Updating git repository..."
+git pull --rebase
+
+git remote show origin | grep 'HEAD' | cut -d':' -f2 | sed -e 's/^ *//g' -e 's/ *$//g' >"$default_branch" &
+
+msg2 "Updating system..."
+if [ -n "$MODE" ]; then
+    nixos-rebuild "$MODE"
+else
+    nixos-rebuild switch
+fi
+
+git switch "$(cat "$default_branch")" >/dev/null 2>&1 && msg2 "Switched to branch '$(cat "$default_branch")'"
+msg "Finished Update!"
+
+# vim: ft=sh
diff --git a/pkgs/sources/scripts/source/specific/neorg/neorg_id_function.sh b/pkgs/sources/scripts/source/specific/neorg/neorg_id_function.sh
new file mode 100755
index 00000000..865ecacf
--- /dev/null
+++ b/pkgs/sources/scripts/source/specific/neorg/neorg_id_function.sh
@@ -0,0 +1,16 @@
+#! /usr/bin/env dash
+
+# shellcheck source=/dev/null
+SHELL_LIBRARY_VERSION="2.1.2" . %SHELL_LIBRARY_PATH
+
+context="$(task _get rc.context)"
+if [ "$context" ]; then
+    filter="project:$context"
+else
+    filter="0-10000"
+fi
+tasks="$(task "$filter" _ids)"
+
+if [ "$tasks" ]; then
+    echo "$tasks" | xargs task _zshids | awk -F: -v q="'" '{gsub(/'\''/, q "\\" q q ); print $1 ":" q $2 q}'
+fi
diff --git a/pkgs/sources/scripts/source/specific/neorg/sh/functions/add.sh b/pkgs/sources/scripts/source/specific/neorg/sh/functions/add.sh
new file mode 100755
index 00000000..5a830a10
--- /dev/null
+++ b/pkgs/sources/scripts/source/specific/neorg/sh/functions/add.sh
@@ -0,0 +1,23 @@
+#!/usr/bin/env dash
+
+add0open_taskwarrior_project_file() {
+    task_project_file="%TASK_PROJECT_FILE"
+
+    cd "$(dirname $task_project_file)" || die "BUG: task_project_file ('$task_project_file') can't be accessed"
+
+    git_dir="$(search_flake_base_dir)"
+    [ "$git_dir" ] || die "(BUG): No git directory?"
+    cd "$git_dir" || die "Unreachable, this MUST exists"
+
+    nvim "$task_project_file"
+    git add "$task_project_file"
+
+    base_task_project_file_path="$(awk "{ gsub(\"$git_dir/\", \"\", \$0); print }" "$(ptmp "$task_project_file")")"
+    git add $task_project_file
+
+    # Check that only the project file has been added (and that our file is actually
+    # modified)
+    if git status --porcelain=v2 | awk -v path="$base_task_project_file_path" 'BEGIN { hit = 0 } { if ($2 ~ /A./ || $2 ~ /M./) { if ($NF ~ path) { hit = 1 } else { hit = 0; exit 1 } } } END { if (hit == 1) { exit 0 } else { exit 1 } }'; then
+        git commit --verbose --message="chore($(dirname "$base_task_project_file_path")): Update"
+    fi
+}
diff --git a/pkgs/sources/scripts/source/specific/neorg/sh/functions/context.sh b/pkgs/sources/scripts/source/specific/neorg/sh/functions/context.sh
new file mode 100755
index 00000000..7095847d
--- /dev/null
+++ b/pkgs/sources/scripts/source/specific/neorg/sh/functions/context.sh
@@ -0,0 +1,43 @@
+#!/usr/bin/env dash
+
+context0open_current_task_context() {
+    current_context="$(utils0get_current_context)"
+
+    if [ "$current_context" ]; then
+        context_path="$(utils0get_current_context_path "$current_context")"
+
+        extended_neorg_project_dir="$(utils0get_neorg_project_dir)"
+        cd "$extended_neorg_project_dir" || die "(BUG?): Can not access the project dir: $extended_neorg_project_dir"
+
+        nvim "$extended_neorg_project_dir/$context_path"
+
+        git add .
+        git commit --message="chore($(dirname "$context_path")): Update" --no-gpg-sign
+    else
+        warn "No context active"
+    fi
+}
+
+context0open_current_task_context_at_task_id() {
+    task_id="$1"
+    current_context="$(utils0get_current_context)"
+
+    if [ "$current_context" ]; then
+        context_path="$(utils0get_current_context_path "$current_context")"
+        extended_neorg_project_dir="$(utils0get_neorg_project_dir)"
+        task_uuid="$(task "$task_id" uuids)"
+
+        cd "$extended_neorg_project_dir" || die "(BUG?): Can not access the project dir: $extended_neorg_project_dir"
+
+        if ! grep -q "% $task_uuid" "$extended_neorg_project_dir/$context_path"; then
+            echo "* TITLE (% $task_uuid)" >>"$extended_neorg_project_dir/$context_path"
+        fi
+
+        nvim "$extended_neorg_project_dir/$context_path" -c "/% $task_uuid"
+
+        git add .
+        git commit --message="chore($(dirname "$context_path")): Update" --no-gpg-sign
+    else
+        warn "No context active"
+    fi
+}
diff --git a/pkgs/sources/scripts/source/specific/neorg/sh/functions/dmenu.sh b/pkgs/sources/scripts/source/specific/neorg/sh/functions/dmenu.sh
new file mode 100755
index 00000000..5a138982
--- /dev/null
+++ b/pkgs/sources/scripts/source/specific/neorg/sh/functions/dmenu.sh
@@ -0,0 +1,14 @@
+#!/usr/bin/env dash
+
+dmenu0open_context_in_browser() {
+    project="$(echo "%ALL_PROJECTS_PIPE" | rofi -sep "|" -dmenu)"
+
+    if [ "$project" ]; then
+        [ -d "%NEORG_REVIEW_PATH" ] || mkdir --parents "%NEORG_REVIEW_PATH"
+        [ -f "%NEORG_REVIEW_PATH/$project.lock" ] || touch "%NEORG_REVIEW_PATH/$project.lock"
+        project0open_project_in_browser "$project"
+    else
+        notify-send "(neorg/dmenu) No project selected"
+        exit 1
+    fi
+}
diff --git a/pkgs/sources/scripts/source/specific/neorg/sh/functions/f_start.sh b/pkgs/sources/scripts/source/specific/neorg/sh/functions/f_start.sh
new file mode 100755
index 00000000..2423dd44
--- /dev/null
+++ b/pkgs/sources/scripts/source/specific/neorg/sh/functions/f_start.sh
@@ -0,0 +1,7 @@
+#!/usr/bin/env dash
+
+fstart0start_new_task() {
+    task_id="$1"
+    fstop0stop_current_task
+    task start "$task_id"
+}
diff --git a/pkgs/sources/scripts/source/specific/neorg/sh/functions/f_stop.sh b/pkgs/sources/scripts/source/specific/neorg/sh/functions/f_stop.sh
new file mode 100755
index 00000000..e4ff0b94
--- /dev/null
+++ b/pkgs/sources/scripts/source/specific/neorg/sh/functions/f_stop.sh
@@ -0,0 +1,7 @@
+#!/usr/bin/env dash
+
+fstop0stop_current_task() {
+    # we ensured that only one task may be active
+    active="$(task +ACTIVE _ids)"
+    [ "$active" ] && task stop "$active"
+}
diff --git a/pkgs/sources/scripts/source/specific/neorg/sh/functions/list.sh b/pkgs/sources/scripts/source/specific/neorg/sh/functions/list.sh
new file mode 100755
index 00000000..10659457
--- /dev/null
+++ b/pkgs/sources/scripts/source/specific/neorg/sh/functions/list.sh
@@ -0,0 +1,8 @@
+#!/usr/bin/env dash
+
+list0list_all_contexts_newline() {
+    print "%ALL_PROJECTS_NEWLINE"
+}
+list0list_all_contexts_comma() {
+    print "%ALL_PROJECTS_COMMA"
+}
diff --git a/pkgs/sources/scripts/source/specific/neorg/sh/functions/project.sh b/pkgs/sources/scripts/source/specific/neorg/sh/functions/project.sh
new file mode 100755
index 00000000..64591850
--- /dev/null
+++ b/pkgs/sources/scripts/source/specific/neorg/sh/functions/project.sh
@@ -0,0 +1,41 @@
+#!/usr/bin/env dash
+
+project0open_current_context_in_browser() {
+    current_context="$(utils0get_current_context)"
+    [ "$current_context" ] || die "No current context to use"
+    project0open_context_in_browser "$(utils0context2project "$current_context")"
+}
+
+project0open_project_in_browser() {
+    project="$1"
+    [ "$project" ] || die "BUG: No context supplied to project0open_context_in_browser"
+
+    old_context="$(utils0get_current_context)"
+    # We have ensured that only one task may be active
+    old_started_task="$(task +ACTIVE _ids)"
+
+    tracking="$(mktmp)"
+    task "project:$project" _ids | xargs --no-run-if-empty task _zshids >"$tracking"
+    task context "$(utils0project2context "$project")"
+
+    while read -r description; do
+        desc="$(echo "$description" | awk -F: '{print $2}')"
+        if [ "$desc" = "tracking" ]; then
+            task_id="$(echo "$description" | awk -F: '{print $1}')"
+            notify-send "(Neorg)" "Starting task $project -> $desc"
+            task start "$task_id"
+            break
+        fi
+    done <"$tracking"
+
+    firefox -P "$project"
+
+    task stop "$task_id"
+    [ "$old_started_task" ] && task start "$old_started_task"
+
+    if [ "$old_context" ]; then
+        task context "$old_context"
+    else
+        task context none
+    fi
+}
diff --git a/pkgs/sources/scripts/source/specific/neorg/sh/functions/review.sh b/pkgs/sources/scripts/source/specific/neorg/sh/functions/review.sh
new file mode 100755
index 00000000..a0a9ab8d
--- /dev/null
+++ b/pkgs/sources/scripts/source/specific/neorg/sh/functions/review.sh
@@ -0,0 +1,12 @@
+#!/usr/bin/env dash
+
+review0start() {
+    for project in $(list0list_all_contexts_newline); do
+        if [ -f "%NEORG_REVIEW_PATH/$project.lock" ]; then
+            msg "Reviewing '$project'"
+            notify-send "Neorg" "Reviewing '$project'"
+            firefox -P "$project"
+            rm "%NEORG_REVIEW_PATH/$project.lock"
+        fi
+    done
+}
diff --git a/pkgs/sources/scripts/source/specific/neorg/sh/functions/utils.sh b/pkgs/sources/scripts/source/specific/neorg/sh/functions/utils.sh
new file mode 100755
index 00000000..c3843e8e
--- /dev/null
+++ b/pkgs/sources/scripts/source/specific/neorg/sh/functions/utils.sh
@@ -0,0 +1,40 @@
+#!/usr/bin/env dash
+
+# Runs it's first argument and then the second, regardless if the first failed or
+# succeeded
+utils0chain() {
+    eval "$1"
+    eval "$2"
+}
+
+utils0get_current_context() {
+    current_context="$(task _get rc.context)"
+    printf "%s\n" "$current_context"
+}
+
+utils0get_current_context_path() {
+    current_context="$1"
+    context_path="$(task _get rc.context."$current_context".rc.neorg_path 2>/dev/null)"
+    if ! [ "$context_path" ]; then
+        context_path="$(grep "context.$current_context.rc.neorg_path" "%HOME_TASKRC" | awk 'BEGIN {FS="="} {print $2}')"
+        [ "$context_path" ] || die "All contexts should have a 'neorg_path' set!"
+    fi
+    printf "%s\n" "$context_path"
+}
+
+utils0get_neorg_project_dir() {
+    # Perform shell expansion of Tilde
+    neorg_project_dir="$(sed "s|^~|$HOME|" "$(ptmp "%DEFAULT_NEORG_PROJECT_DIR")")"
+    printf "%s\n" "$neorg_project_dir"
+}
+
+utils0project2context() {
+    project="$1"
+    context="$(sed 's|\.|_|g' "$(ptmp "$project")")"
+    printf "%s\n" "$context"
+}
+utils0context2project() {
+    context="$1"
+    project="$(sed 's|_|\.|g' "$(ptmp "$context")")"
+    printf "%s\n" "$project"
+}
diff --git a/pkgs/sources/scripts/source/specific/neorg/sh/functions/workspace.sh b/pkgs/sources/scripts/source/specific/neorg/sh/functions/workspace.sh
new file mode 100755
index 00000000..d5eb2fca
--- /dev/null
+++ b/pkgs/sources/scripts/source/specific/neorg/sh/functions/workspace.sh
@@ -0,0 +1,9 @@
+#!/usr/bin/env dash
+
+workspace0open_neorg_workspace() {
+    workspace="$1"
+    nvim -c "NeorgStart" -s "$(ptmp ":Neorg workspace $workspace\n")"
+}
+workspace0open_neorg_workspace_prompt() {
+    nvim -c "NeorgStart" -s "$(ptmp ":Neorg workspace ")"
+}
diff --git a/pkgs/sources/scripts/source/specific/neorg/sh/main.sh b/pkgs/sources/scripts/source/specific/neorg/sh/main.sh
new file mode 100755
index 00000000..559351b9
--- /dev/null
+++ b/pkgs/sources/scripts/source/specific/neorg/sh/main.sh
@@ -0,0 +1,164 @@
+#!/usr/bin/env dash
+
+# shellcheck source=/dev/null
+SHELL_LIBRARY_VERSION="2.1.2" . %SHELL_LIBRARY_PATH
+
+# load dependencies
+. ./functions/add.sh
+. ./functions/context.sh
+. ./functions/dmenu.sh
+. ./functions/f_start.sh
+. ./functions/f_stop.sh
+. ./functions/list.sh
+. ./functions/project.sh
+. ./functions/utils.sh
+. ./functions/workspace.sh
+. ./functions/review.sh
+
+# these are used in version()
+# shellcheck disable=2034
+AUTHORS="Soispha"
+# shellcheck disable=2034
+YEARS="2023"
+
+NAME="neorg"
+
+help() {
+    cat <<EOF
+This is the core interface to the system-integrated task management
+
+USAGE:
+    $NAME [OPTIONS] [COMMAND]
+
+OPTIONS:
+    --help      | -h
+                            Display this help and exit.
+
+    --version   | -v
+                            Display version and copyright information and exit.
+COMMANDS:
+    task [ID]
+                            Open the neorg context associated with the current context and
+                            the uuid of the task with id ID. Without ID, it'll open the
+                            current context's norg file.
+                            If no context is set, drops you to the selection prompt
+
+    dmenu
+                            Select a project in dmenu mode. This will give you all projects
+                            and exectute the selected one as in 'neorg projects <selected>'
+
+    workspace [WS]
+                            The neorg workspace (WS) to open at startup, an empty value drops
+                            you at a prompt to enter the workspace yourself.
+
+    project [P]
+                            Opens the webbrowser with either the context (P) or
+                            the current active context as argument if no context is supplied
+
+    list
+                            Lists all available contexts
+
+    add
+                            Allows you to quickly add projects
+
+    fstart ID
+                            Starts the task (ID) but only after it stooped
+                            the previous active task, if it existed.
+
+    fstop
+                            Stops the current active task
+
+    review
+                            Review all firefox tabs
+ARGUMENTS:
+    ID | *([0-9]) := [[%ID_GENERATION_FUNCTION]]
+                            The function displays all possible IDs of the eligable tasks.
+
+    WS := %ALL_WORKSPACES
+                            All possible workspaces
+
+    P := %ALL_PROJECTS_PIPE
+                            The possible project
+
+EOF
+}
+
+for arg in "$@"; do
+    case "$arg" in
+    "--help" | "-h")
+        help
+        exit 0
+        ;;
+    "--version" | "-v")
+        version
+        exit 0
+        ;;
+    esac
+done
+
+while [ "$#" -ne 0 ]; do
+    case "$1" in
+    "t"*) # task
+        shift 1
+        task_id="$1"
+        [ "$task_id" ] || utils0chain context0open_current_task_context "exit 0"
+        context0open_current_task_context_at_task_id "$task_id"
+        exit 0
+        ;;
+    "w"*) # workspace
+        shift 1
+        workspace_to_open="$1"
+        # TODO: Exit with 1 on error, instead of the 0 <2023-10-20>
+        [ "$workspace_to_open" ] || utils0chain workspace0open_neorg_workspace_prompt "exit 0"
+        workspace0open_neorg_workspace "$workspace_to_open"
+        exit 0
+        ;;
+    "p"*) # project
+        shift 1
+        project_to_open="$1"
+        # TODO: Exit with 1 on error, instead of the 0 <2023-10-20>
+        [ "$project_to_open" ] || utils0chain project0open_current_context_in_browser "exit 0"
+        if ! grep -q "$project_to_open" "$(ptmp "%ALL_PROJECTS_NEWLINE")"; then
+            die "Your project ('$project_to_open') is not in the list of available projects:
+%ALL_PROJECTS_COMMA"
+        fi
+        project0open_project_in_browser "$project_to_open"
+        exit 0
+        ;;
+    "l"*) # list
+        list0list_all_contexts_newline
+        exit 0
+        ;;
+    "a"*) # add-project
+        add0open_taskwarrior_project_file
+        exit 0
+        ;;
+    "d"*) # dmenu
+        dmenu0open_context_in_browser
+        exit 0
+        ;;
+    "fsta"*) # fstart
+        shift 1
+        task_id="$1"
+        [ "$task_id" ] || die "No task id provided to fstart"
+        fstart0start_new_task "$task_id"
+        exit 0
+        ;;
+    "fsto"*) # fstop
+        fstop0stop_current_task
+        exit 0
+        ;;
+    "r"*) # review
+        shift 1
+        review0start
+        exit 0
+        ;;
+    *)
+        die "Command '$1' does not exist! Please look at:\n $NAME --help"
+        exit 0
+        ;;
+    esac
+done
+
+context0open_current_task_context
+# vim: ft=sh
diff --git a/pkgs/sources/scripts/source/specific/spodi/sh/download.sh b/pkgs/sources/scripts/source/specific/spodi/sh/download.sh
new file mode 100755
index 00000000..fe9746c8
--- /dev/null
+++ b/pkgs/sources/scripts/source/specific/spodi/sh/download.sh
@@ -0,0 +1,58 @@
+#!/usr/bin/env dash
+
+download_to_down() {
+    DOWNLOAD_DIRECTORY="%XDG_MUSIC_DIR/down/spotify"
+
+    already_downloaded_files="$(mktmp)"
+    fd . "$DOWNLOAD_DIRECTORY" --exclude spotdl.log --exclude spotdl-errors.log >"$already_downloaded_files"
+    if [ -z "$NO_CHECK" ] && [ "$(wc -l <"$already_downloaded_files")" -ne 0 ]; then
+        die "something is already downloaded"
+    fi
+    # [ -e "$DOWNLOAD_DIRECTORY/spotdl.log" ] && rm "$DOWNLOAD_DIRECTORY/spotdl.log"
+
+    download "$1" "$DOWNLOAD_DIRECTORY"
+}
+
+download() {
+    download_url="$1"
+    output_path="$2"
+
+    config="$(mktmp)"
+    cat <<EOF | clean >"$config"
+# Main options
+--audio slider-kz bandcamp youtube-music piped youtube soundcloud
+--lyrics genius musixmatch azlyrics synced
+
+# FFmpeg options
+--ffmpeg ffmpeg
+--threads 16
+--bitrate 256k
+
+# Spotify options
+--cache-path %XDG_CACHE_HOME/spotdl/.spotipy
+
+# Output options
+--preload
+--format opus
+--output {artists}_-_{title}
+--print-errors
+--save-errors $output_path/spotdl-errors.log
+# TODO: Reactive whence spotdl support for these has improved <2023-12-19>
+# --generate-lrc
+--overwrite skip
+
+# Misc options
+--log-level INFO
+EOF
+
+    cd "$output_path" || die "BUG: no $output_path"
+    touch "$output_path/spotdl-errors.log"
+
+    # The sub shell needs to be unquoted, as the arguments may not be treated as one.
+    # shellcheck disable=2046
+    unbuffer spotdl $(cat "$config") download "$download_url" | tee "$output_path/spotdl.log"
+
+    [ -d ~/.spotdl ] && rm -r ~/.spotdl
+}
+
+# vim: ft=sh
diff --git a/pkgs/sources/scripts/source/specific/spodi/sh/update.sh b/pkgs/sources/scripts/source/specific/spodi/sh/update.sh
new file mode 100755
index 00000000..a289cf58
--- /dev/null
+++ b/pkgs/sources/scripts/source/specific/spodi/sh/update.sh
@@ -0,0 +1,52 @@
+#!/usr/bin/env dash
+
+update() {
+    UPDATE_DIRECTORY="%XDG_MUSIC_DIR/artists"
+    UPDATE_CONFIG_FILE="%XDG_MUSIC_DIR/artists/update.conf"
+
+    if ! [ -f "$UPDATE_CONFIG_FILE" ]; then
+        error="$(
+            cat <<EOF
+Please provide an update config file at: '$UPDATE_CONFIG_FILE'.
+
+The 'update.conf' file should follow this pattern:
+<path_to_artist>/<artist_name>|<spotify_url>
+
+All comments and empty lines are ignored
+EOF
+        )"
+        die "$error"
+    fi
+
+    config_file="$(mktmp)"
+    clean "$UPDATE_CONFIG_FILE" >"$config_file"
+
+    while IFS="|" read -r artist url; do
+        full_artist="$UPDATE_DIRECTORY/$artist"
+        [ -d "$full_artist" ] || mkdir --parents "$full_artist"
+        [ -d "$full_artist/update" ] || mkdir --parents "$full_artist/update"
+        [ -d "$full_artist/all" ] || mkdir --parents "$full_artist/all"
+        [ -d "$full_artist/filtered" ] || mkdir --parents "$full_artist/filtered"
+
+        while read -r file; do
+            ln --symbolic --relative "$file" "$full_artist/update/$(basename "$file")"
+        done <"$(tmp fd --type file --extension opus . "$full_artist/all")"
+
+        msg2 "Updating $artist with url: '$url'"
+        download "$url" "$full_artist/update"
+
+        while read -r file; do
+            mv "$file" "$full_artist/all"
+            ln --symbolic --relative "$full_artist/all/$(basename "$file")" "$full_artist/filtered/$(basename "$file")"
+        done <"$(tmp fd --type file --extension opus . "$full_artist/update")"
+
+        while read -r file; do
+            rm "$file"
+        done <"$(tmp fd --type symlink --extension opus . "$full_artist/update")"
+
+        cp "$full_artist/update/spotdl.log" "$full_artist/all/spotdl.$(date +%Y_%m_%d).log"
+        cp "$full_artist/update/spotdl-errors.log" "$full_artist/all/spotdl-errors.$(date +%Y_%m_%d).log"
+    done <"$config_file"
+}
+
+# vim: ft=sh
diff --git a/pkgs/sources/scripts/source/specific/spodi/spodi.sh b/pkgs/sources/scripts/source/specific/spodi/spodi.sh
new file mode 100755
index 00000000..475fd48a
--- /dev/null
+++ b/pkgs/sources/scripts/source/specific/spodi/spodi.sh
@@ -0,0 +1,71 @@
+#!/usr/bin/env dash
+
+# shellcheck source=/dev/null
+SHELL_LIBRARY_VERSION="2.1.2" . %SHELL_LIBRARY_PATH
+
+# these are used in version()
+# shellcheck disable=2034
+AUTHORS="Soispha"
+# shellcheck disable=2034
+YEARS="2023"
+
+# load dependencies
+. ./sh/update.sh
+. ./sh/download.sh
+
+help() {
+    cat <<EOF
+This is a small wrapper around downloading things from spotify
+
+USAGE:
+    $NAME [OPTIONS] COMMAND
+
+OPTIONS:
+    --help      | -h
+                            Display this help and exit.
+
+    --version   | -v
+                            Display version and copyright information and exit.
+COMMANDS:
+    update
+                            Read the artist.conf file and download all newly released things
+
+    download URL
+                            Download a specific url to the DOWNLOAD_DIRECTORY
+EOF
+}
+
+for arg in "$@"; do
+    case "$arg" in
+    "--help" | "-h")
+        help
+        exit 0
+        ;;
+    "--version" | "-v")
+        version
+        exit 0
+        ;;
+    esac
+done
+
+case "$1" in
+"update")
+    shift 1
+    update
+    exit 0
+    ;;
+"download")
+    shift 1
+    download_url="$1"
+    [ -z "$download_url" ] && die "You need to provide a download url"
+    download_to_down "$download_url"
+    exit 0
+    ;;
+*)
+    die "Command '$1' is not know"
+    help
+    exit 1
+    ;;
+esac
+
+# vim: ft=sh
diff --git a/pkgs/sources/scripts/source/specific/ytcc/description.sh b/pkgs/sources/scripts/source/specific/ytcc/description.sh
new file mode 100755
index 00000000..ae9107b9
--- /dev/null
+++ b/pkgs/sources/scripts/source/specific/ytcc/description.sh
@@ -0,0 +1,8 @@
+#!/usr/bin/env dash
+
+# shellcheck source=/dev/null
+SHELL_LIBRARY_VERSION="2.1.2" . %SHELL_LIBRARY_PATH
+
+jq --raw-output '.description' "$XDG_RUNTIME_DIR/ytcc/running" | fmt -u -s | less
+
+# vim: ft=sh
diff --git a/pkgs/sources/scripts/source/wrappers/battery.sh b/pkgs/sources/scripts/source/wrappers/battery.sh
new file mode 100755
index 00000000..e650ba5d
--- /dev/null
+++ b/pkgs/sources/scripts/source/wrappers/battery.sh
@@ -0,0 +1,11 @@
+#!/usr/bin/env dash
+
+# shellcheck source=/dev/null
+SHELL_LIBRARY_VERSION="2.1.2" . %SHELL_LIBRARY_PATH
+
+capacity="$(cat /sys/class/power_supply/BAT0/capacity)"
+status="$(cat /sys/class/power_supply/BAT0/status)"
+
+printf "%s%% (%s)\n" "$capacity" "$status"
+
+# vim: ft=sh
diff --git a/pkgs/sources/scripts/source/wrappers/hibernate.sh b/pkgs/sources/scripts/source/wrappers/hibernate.sh
new file mode 100755
index 00000000..30868fd1
--- /dev/null
+++ b/pkgs/sources/scripts/source/wrappers/hibernate.sh
@@ -0,0 +1,15 @@
+#!/usr/bin/env dash
+
+# shellcheck source=/dev/null
+SHELL_LIBRARY_VERSION="2.1.2" . %SHELL_LIBRARY_PATH
+
+context="$(task _get rc.context)"
+[ "$context" ] && task context none
+
+# We have ensured that only one task is active
+active="$(task +ACTIVE _ids)"
+[ "$active" ] && task stop "$active"
+
+systemctl hibernate "$@"
+
+# vim: ft=sh
diff --git a/pkgs/sources/scripts/source/wrappers/ll.sh b/pkgs/sources/scripts/source/wrappers/ll.sh
new file mode 100755
index 00000000..f689ba44
--- /dev/null
+++ b/pkgs/sources/scripts/source/wrappers/ll.sh
@@ -0,0 +1,14 @@
+#!/usr/bin/env dash
+
+# shellcheck source=/dev/null
+SHELL_LIBRARY_VERSION="2.1.2" . %SHELL_LIBRARY_PATH
+
+last_directory="$(mktemp)"
+
+command lf -last-dir-path="$last_directory" "$@"
+
+dir="$(cat "$last_directory")"
+cd "$dir" || die "$dir does not exist!"
+rm "$last_directory"
+
+# vim: ft=sh
diff --git a/pkgs/sources/scripts/source/wrappers/lock.sh b/pkgs/sources/scripts/source/wrappers/lock.sh
new file mode 100755
index 00000000..3101ef9a
--- /dev/null
+++ b/pkgs/sources/scripts/source/wrappers/lock.sh
@@ -0,0 +1,18 @@
+#!/usr/bin/env dash
+
+# shellcheck source=/dev/null
+SHELL_LIBRARY_VERSION="2.1.2" . %SHELL_LIBRARY_PATH
+
+context="$(task _get rc.context)"
+[ "$context" ] && task context none
+
+# We have ensured that only one task is active
+active="$(task +ACTIVE _ids)"
+[ "$active" ] && task stop "$active"
+
+swaylock
+
+[ "$active" ] && task start "$active"
+
+[ "$context" ] && task context "$context"
+# vim: ft=sh
diff --git a/pkgs/sources/scripts/source/wrappers/lyrics.sh b/pkgs/sources/scripts/source/wrappers/lyrics.sh
new file mode 100755
index 00000000..02a147c8
--- /dev/null
+++ b/pkgs/sources/scripts/source/wrappers/lyrics.sh
@@ -0,0 +1,11 @@
+#!/usr/bin/env dash
+
+# shellcheck source=/dev/null
+SHELL_LIBRARY_VERSION="2.1.2" . %SHELL_LIBRARY_PATH
+
+(
+    cd "$XDG_MUSIC_DIR" || die "No music dir!"
+    exiftool "$(mpc --format '%file%' current)" -json | jq '.[0].Lyrics' -r | less
+)
+
+# vim: ft=sh
diff --git a/pkgs/sources/scripts/source/wrappers/mpc-fav.sh b/pkgs/sources/scripts/source/wrappers/mpc-fav.sh
new file mode 100755
index 00000000..795a4875
--- /dev/null
+++ b/pkgs/sources/scripts/source/wrappers/mpc-fav.sh
@@ -0,0 +1,16 @@
+#!/usr/bin/env dash
+
+# shellcheck source=/dev/null
+SHELL_LIBRARY_VERSION="2.1.2" . %SHELL_LIBRARY_PATH
+
+FAV_DIR="$XDG_MUSIC_DIR/playlists/favourites"
+
+cd "$XDG_MUSIC_DIR" || die "No music dir!"
+
+[ -d "$FAV_DIR" ] || mkdir --parents "$FAV_DIR"
+
+ln -sr "$(mpc --format '%file%' current)" "$FAV_DIR/" || die "Link failed!"
+
+mpc update
+
+# vim: ft=sh
diff --git a/pkgs/sources/scripts/source/wrappers/mpc-rm.sh b/pkgs/sources/scripts/source/wrappers/mpc-rm.sh
new file mode 100755
index 00000000..94e0634b
--- /dev/null
+++ b/pkgs/sources/scripts/source/wrappers/mpc-rm.sh
@@ -0,0 +1,10 @@
+#!/usr/bin/env dash
+
+# shellcheck source=/dev/null
+SHELL_LIBRARY_VERSION="2.1.2" . %SHELL_LIBRARY_PATH
+
+cd "$XDG_MUSIC_DIR" || die "No music dir!"
+trash-put "$(mpc --format '%file%' current)"
+mpc del 0
+
+# vim: ft=sh
diff --git a/pkgs/sources/scripts/source/wrappers/mpc.sh b/pkgs/sources/scripts/source/wrappers/mpc.sh
new file mode 100755
index 00000000..5aae5cdb
--- /dev/null
+++ b/pkgs/sources/scripts/source/wrappers/mpc.sh
@@ -0,0 +1,20 @@
+#!/usr/bin/env dash
+
+# shellcheck source=/dev/null
+SHELL_LIBRARY_VERSION="2.1.2" . %SHELL_LIBRARY_PATH
+
+case "$1" in
+"rm")
+    shift 1
+    mpc-rm "$@"
+    ;;
+"fav")
+    shift 1
+    mpc-fav "$@"
+    ;;
+*)
+    mpc "$@"
+    ;;
+esac
+
+# vim: ft=sh
diff --git a/pkgs/sources/scripts/source/wrappers/show.sh b/pkgs/sources/scripts/source/wrappers/show.sh
new file mode 100755
index 00000000..ae2bdb13
--- /dev/null
+++ b/pkgs/sources/scripts/source/wrappers/show.sh
@@ -0,0 +1,9 @@
+#!/usr/bin/env dash
+
+# shellcheck source=/dev/null
+SHELL_LIBRARY_VERSION="2.1.2" . %SHELL_LIBRARY_PATH
+
+# Maybe add `--quit-if-one-screen`
+less --redraw-on-quit "$@"
+
+# vim: ft=sh
diff --git a/pkgs/sources/scripts/source/wrappers/sort_song.sh b/pkgs/sources/scripts/source/wrappers/sort_song.sh
new file mode 100755
index 00000000..e2978507
--- /dev/null
+++ b/pkgs/sources/scripts/source/wrappers/sort_song.sh
@@ -0,0 +1,34 @@
+#!/usr/bin/env dash
+
+# shellcheck source=/dev/null
+SHELL_LIBRARY_VERSION="2.1.2" . %SHELL_LIBRARY_PATH
+
+case "$("$1" | tr '[:upper:]' '[:lower:]')" in
+"lyrics")
+    filter="LYRICS"
+    directory="lyrics"
+    ;;
+"instrumental")
+    filter="INSTRUMENTAL"
+    directory="instrumental"
+    ;;
+*)
+    die "Expected 'instrumental|lyrics' but got '$1'"
+    ;;
+esac
+
+process() {
+    mediainfo --Output=JSON "$1" | jq '.media.track | map(.Lyrics) | join("")'
+}
+
+mkdir "../$directory"
+
+fd . --extension=opus | while read -r file; do
+    if [ "$(process "$file")" = '""' ] || [ "$(process "$file")" = '"Instrumental"' ] || [ "$(process "$file")" = '"instrumental"' ]; then
+        echo "INSTRUMENTAL::$file"
+    else
+        echo "LYRICS::$file"
+    fi
+done | grep "$filter" | awk 'BEGIN {FS="::"}{print $2}' | while read -r file; do ln -s "../all/$file" "../$directory/$file"; done
+
+# vim: ft=sh
diff --git a/pkgs/sources/scripts/source/wrappers/virsh-del.sh b/pkgs/sources/scripts/source/wrappers/virsh-del.sh
new file mode 100755
index 00000000..c3de5484
--- /dev/null
+++ b/pkgs/sources/scripts/source/wrappers/virsh-del.sh
@@ -0,0 +1,10 @@
+#! /usr/bin/env dash
+
+# shellcheck source=/dev/null
+SHELL_LIBRARY_VERSION="2.1.2" . %SHELL_LIBRARY_PATH
+
+virsh destroy "$1"
+virsh undefine "$1" --nvram
+virsh vol-delete --pool default "$1".qcow2
+
+# vim: ft=sh
diff --git a/pkgs/sources/scripts/source/wrappers/yti.sh b/pkgs/sources/scripts/source/wrappers/yti.sh
new file mode 100755
index 00000000..a69ffa74
--- /dev/null
+++ b/pkgs/sources/scripts/source/wrappers/yti.sh
@@ -0,0 +1,33 @@
+#! /usr/bin/env dash
+
+# shellcheck source=/dev/null
+SHELL_LIBRARY_VERSION="2.1.2" . %SHELL_LIBRARY_PATH
+
+DOWN_DIR=/home/soispha/media/music/down/youtube
+
+tmp=$(mktmp)
+config=$(mktmp)
+
+for e in "$DOWN_DIR"/*.opus; do echo "$e" >>"$tmp"; done
+[ "$(wc -l "$tmp" | awk '{print $1}')" -gt 2 ] && die "something is already downloaded"
+
+cat <<EO >"$config"
+--paths home:"$DOWN_DIR"
+#--output %(fulltitle)
+--restrict-filenames
+--no-overwrites
+--no-write-info-json
+--clean-info-json
+--prefer-free-formats
+#--format mp3
+--extract-audio
+--audio-quality 0
+--audio-format best
+EO
+
+rm "$DOWN_DIR/yt-dlp.log"
+cd "$DOWN_DIR" || die "BUG: no $DOWN_DIR"
+
+unbuffer yt-dlp --config-location "$config" "$1" | tee "$DOWN_DIR/yt-dlp.log"
+
+# vim: ft=sh
diff --git a/pkgs/sources/snap-sync-forked/default.nix b/pkgs/sources/snap-sync-forked/default.nix
new file mode 100644
index 00000000..5b086a5a
--- /dev/null
+++ b/pkgs/sources/snap-sync-forked/default.nix
@@ -0,0 +1,24 @@
+{sysLib}: [
+  (final: prev: {
+    snap-sync-forked = sysLib.writeShellScript {
+      name = "snap-sync-forked";
+      src = ./snap-sync-forked.sh;
+      dependencies = with prev; [
+        bash
+        btrfs-progs
+        coreutils
+        gawk
+        gnugrep
+        snapper
+        util-linux
+
+        # optional:
+        libnotify
+        openssh
+        pv
+        rsync
+        sudo
+      ];
+    };
+  })
+]
diff --git a/pkgs/sources/snap-sync-forked/snap-sync-forked.sh b/pkgs/sources/snap-sync-forked/snap-sync-forked.sh
new file mode 100755
index 00000000..3d9c1ac9
--- /dev/null
+++ b/pkgs/sources/snap-sync-forked/snap-sync-forked.sh
@@ -0,0 +1,534 @@
+#!/usr/bin/env bash
+
+# shellcheck source=/dev/null
+SHELL_LIBRARY_VERSION="2.1.2" . %SHELL_LIBRARY_PATH
+
+#
+# snap-sync
+# https://github.com/wesbarnett/snap-sync
+# Copyright (C) 2016-2021 Wes Barnett
+
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+
+# You should have received a copy of the GNU General Public License along
+# with this program; if not, write to the Free Software Foundation, Inc.,
+
+# -------------------------------------------------------------------------
+
+# Takes snapshots of each snapper configuration. It then sends the snapshot to
+# a location on an external drive. After the initial transfer, it does
+# incremental snapshots on later calls. It's important not to delete the
+# snapshot created on your system since that will be used to determine the
+# difference for the next incremental snapshot.
+
+set -o errtrace
+
+version="0.7"
+name="snap-sync"
+
+printf "\nsnap-sync version %s, Copyright (C) 2016-2021 Wes Barnett\n" "$version"
+printf "snap-sync comes with ABSOLUTELY NO WARRANTY. This is free software, and you are welcome to redistribute it under certain conditions. See the license for more information. \n\n"
+
+# The following line is modified by the Makefile or
+# find_snapper_config script
+SNAPPER_CONFIG=/etc/sysconfig/snapper
+
+donotify=0
+if ! command -v notify-send &>/dev/null; then
+    donotify=1
+fi
+
+doprogress=0
+if ! command -v pv &>/dev/null; then
+    doprogress=1
+fi
+
+error() {
+    printf "==> ERROR: %s\n" "$@"
+    notify_error 'Error' 'Check journal for more information.'
+} >&2
+
+die() {
+    error "$@"
+    exit 1
+}
+
+traperror() {
+    printf "Exited due to error on line %s.\n" "$1"
+    printf "exit status: %s\n" "$2"
+    printf "command: %s\n" "$3"
+    printf "bash line: %s\n" "$4"
+    printf "function name: %s\n" "$5"
+    exit 1
+}
+
+trapkill() {
+    die "Exited due to user intervention."
+}
+
+trap 'traperror ${LINENO} $? "$BASH_COMMAND" $BASH_LINENO "${FUNCNAME[@]}"' ERR
+trap trapkill SIGTERM SIGINT
+
+usage() {
+    cat <<EOF
+$name $version
+Usage: $name [options]
+
+Options:
+ -c, --config <config>    snapper configuration to backup
+ -d, --description <desc> snapper description
+ -h, --help               print this message
+ -n, --noconfirm          do not ask for confirmation
+ -k, --keepold            keep old incremental snapshots instead of deleting them
+                          after backup is performed
+ -p, --port <port>        remote port; used with '--remote'.
+ -q, --quiet              do not send notifications; instead print them.
+ -r, --remote <address>   ip address of a remote machine to backup to
+ --sudo                   use sudo on the remote machine
+ -s, --subvolid <subvlid> subvolume id of the mounted BTRFS subvolume to back up to
+ -u, --UUID <UUID>        UUID of the mounted BTRFS subvolume to back up to
+
+See 'man snap-sync' for more details.
+EOF
+}
+
+ssh=""
+sudo=0
+while [[ $# -gt 0 ]]; do
+    key="$1"
+    case $key in
+    -d | --description)
+        description="$2"
+        shift 2
+        ;;
+    -c | --config)
+        selected_configs="$2"
+        shift 2
+        ;;
+    -u | --UUID)
+        uuid_cmdline="$2"
+        shift 2
+        ;;
+    -s | --subvolid)
+        subvolid_cmdline="$2"
+        shift 2
+        ;;
+    -k | --keepold)
+        keep="yes"
+        shift
+        ;;
+    -n | --noconfirm)
+        noconfirm="yes"
+        shift
+        ;;
+    -h | --help)
+        usage
+        exit 1
+        ;;
+    -q | --quiet)
+        donotify=1
+        shift
+        ;;
+    -r | --remote)
+        remote=$2
+        shift 2
+        ;;
+    -p | --port)
+        port=$2
+        shift 2
+        ;;
+    --sudo)
+        sudo=1
+        shift
+        ;;
+    *)
+        die "Unknown option: '$key'. Run '$name -h' for valid options."
+        ;;
+    esac
+done
+
+notify() {
+    for u in $(users | tr ' ' '\n' | sort -u); do
+        sudo -u "$u" DISPLAY=:0 \
+            DBUS_SESSION_BUS_ADDRESS="unix:path=/run/user/$(sudo -u "$u" id -u)/bus" \
+            notify-send -a $name "$1" "$2" --icon="dialog-$3"
+    done
+}
+
+notify_info() {
+    if [[ $donotify -eq 0 ]]; then
+        notify "$1" "$2" "information"
+    else
+        printf '%s\n' "$1: $2"
+    fi
+}
+
+notify_error() {
+    if [[ $donotify -eq 0 ]]; then
+        notify "$1" "$2" "error"
+    else
+        printf '%s\n' "$1: $2"
+    fi
+}
+
+[[ $EUID -ne 0 ]] && die "Script must be run as root. See '$name -h' for a description of options"
+! [[ -f $SNAPPER_CONFIG ]] && die "$SNAPPER_CONFIG does not exist."
+
+description=${description:-"latest incremental backup"}
+uuid_cmdline=${uuid_cmdline:-"none"}
+subvolid_cmdline=${subvolid_cmdline:-"5"}
+noconfirm=${noconfirm:-"no"}
+
+if [[ -z $remote ]]; then
+    if ! command -v rsync &>/dev/null; then
+        die "--remote specified but rsync command not found"
+    fi
+fi
+
+if [[ $uuid_cmdline != "none" ]]; then
+    if [[ -z $remote ]]; then
+        notify_info "Backup started" "Starting backups to $uuid_cmdline subvolid=$subvolid_cmdline..."
+    else
+        notify_info "Backup started" "Starting backups to $uuid_cmdline subvolid $subvolid_cmdline at $remote..."
+    fi
+else
+    if [[ -z $remote ]]; then
+        notify_info "Backup started" "Starting backups. Use command line menu to select disk."
+    else
+        notify_info "Backup started" "Starting backups. Use command line menu to select disk on $remote."
+    fi
+fi
+
+if [[ -n $remote ]]; then
+    ssh="ssh $remote"
+    if [[ -n $port ]]; then
+        ssh="$ssh -p $port"
+    fi
+    if [[ $sudo -eq 1 ]]; then
+        ssh="$ssh sudo"
+    fi
+fi
+
+if [[ "$($ssh findmnt -n -v --target / -o FSTYPE)" == "btrfs" ]]; then
+    EXCLUDE_UUID=$($ssh findmnt -n -v -t btrfs --target / -o UUID)
+    TARGETS=$($ssh findmnt -n -v -t btrfs -o UUID,TARGET --list | grep -v "$EXCLUDE_UUID" | awk '{print $2}')
+    UUIDS=$($ssh findmnt -n -v -t btrfs -o UUID,TARGET --list | grep -v "$EXCLUDE_UUID" | awk '{print $1}')
+else
+    TARGETS=$($ssh findmnt -n -v -t btrfs -o TARGET --list)
+    UUIDS=$($ssh findmnt -n -v -t btrfs -o UUID --list)
+fi
+
+declare -a TARGETS_ARRAY
+declare -a UUIDS_ARRAY
+declare -a SUBVOLIDS_ARRAY
+
+i=0
+for x in $TARGETS; do
+    SUBVOLIDS_ARRAY[i]=$($ssh btrfs subvolume show "$x" | awk '/Subvolume ID:/ { print $3 }')
+    TARGETS_ARRAY[i]=$x
+    i=$((i + 1))
+done
+
+i=0
+disk=-1
+disk_count=0
+for x in $UUIDS; do
+    UUIDS_ARRAY[i]=$x
+    if [[ $x == "$uuid_cmdline" && ${SUBVOLIDS_ARRAY[$((i))]} == "$subvolid_cmdline" ]]; then
+        disk=$i
+        disk_count=$((disk_count + 1))
+    fi
+    i=$((i + 1))
+done
+
+if [[ ${#UUIDS_ARRAY[$@]} -eq 0 ]]; then
+    die "No external btrfs subvolumes found to backup to. Run '$name -h' for more options."
+fi
+
+if [[ $disk_count -gt 1 ]]; then
+    printf "Multiple mount points were found with UUID %s and subvolid %s.\n" "$uuid_cmdline" "$subvolid_cmdline"
+    disk="-1"
+fi
+
+if [[ $disk == -1 ]]; then
+    if [[ $disk_count == 0 && $uuid_cmdline != "none" ]]; then
+        error "A device with UUID $uuid_cmdline and subvolid $subvolid_cmdline was not found to be mounted, or it is not a BTRFS device."
+    fi
+    if [[ -z $ssh ]]; then
+        printf "Select a mounted BTRFS device on your local machine to backup to.\nFor more options, exit and run '%s -h'.\n" "$name"
+    else
+        printf "Select a mounted BTRFS device on %s to backup to.\nFor more options, exit and run '%s -h'.\n" "$remote" "$name"
+    fi
+    while [[ $disk -lt 0 || $disk -gt $i ]]; do
+        for x in "${!TARGETS_ARRAY[@]}"; do
+            printf "%4s) %s (uuid=%s, subvolid=%s)\n" "$((x + 1))" "${TARGETS_ARRAY[$x]}" "${UUIDS_ARRAY[$x]}" "${SUBVOLIDS_ARRAY[$x]}"
+        done
+        printf "%4s) Exit\n" "0"
+        read -e -r -p "Enter a number: " disk
+        if ! [[ $disk == ?(-)+([0-9]) ]] || [[ $disk -lt 0 || $disk -gt $i ]]; then
+            printf "\nNo disk selected. Select a disk to continue.\n"
+            disk=-1
+        fi
+    done
+    if [[ $disk == 0 ]]; then
+        exit 0
+    fi
+    disk=$((disk - 1))
+fi
+
+selected_subvolid="${SUBVOLIDS_ARRAY[$((disk))]}"
+selected_uuid="${UUIDS_ARRAY[$((disk))]}"
+selected_mnt="${TARGETS_ARRAY[$((disk))]}"
+printf "\nYou selected the disk with uuid=%s, subvolid=%s.\n" "$selected_uuid" "$selected_subvolid"
+if [[ -z $ssh ]]; then
+    printf "The disk is mounted at '%s'.\n" "$selected_mnt"
+else
+    printf "The disk is mounted at '%s:%s'.\n" "$remote" "$selected_mnt"
+fi
+
+# shellcheck source=/dev/null
+source "$SNAPPER_CONFIG"
+
+if [[ -z $selected_configs ]]; then
+    printf "\nInteractively cycling through all snapper configurations...\n"
+fi
+selected_configs=${selected_configs:-$SNAPPER_CONFIGS}
+
+declare -a BACKUPDIRS_ARRAY
+declare -a MYBACKUPDIR_ARRAY
+declare -a OLD_NUM_ARRAY
+declare -a OLD_SNAP_ARRAY
+declare -a NEW_NUM_ARRAY
+declare -a NEW_SNAP_ARRAY
+declare -a NEW_INFO_ARRAY
+declare -a BACKUPLOC_ARRAY
+declare -a CONT_BACKUP_ARRAY
+
+# Initial configuration of where backup directories are
+i=0
+for x in $selected_configs; do
+
+    if [[ "$(snapper -c "$x" list --disable-used-space -t single | awk '/'"subvolid=$selected_subvolid, uuid=$selected_uuid"'/ {cnt++} END {print cnt}')" -gt 1 ]]; then
+        error "More than one snapper entry found with UUID $selected_uuid subvolid $selected_subvolid for configuration $x. Skipping configuration $x."
+        continue
+    fi
+
+    if [[ "$(snapper -c "$x" list --disable-used-space -t single | awk '/'$name' backup in progress/ {cnt++} END {print cnt}')" -gt 0 ]]; then
+        printf "\nNOTE: Previous failed %s backup snapshots found for '%s'.\n" "$name" "$x"
+        if [[ $noconfirm == "yes" ]]; then
+            printf "'noconfirm' option passed. Failed backups will not be deleted.\n"
+        else
+            read -e -r -p "Delete failed backup snapshot(s)? (These local snapshots from failed backups are not used.) [y/N]? " delete_failed
+            while [[ -n $delete_failed && $delete_failed != [Yy]"es" &&
+                $delete_failed != [Yy] && $delete_failed != [Nn]"o" &&
+                $delete_failed != [Nn] ]]; do
+                read -e -r -p "Delete failed backup snapshot(s)? (These local snapshots from failed backups are not used.) [y/N] " delete_failed
+                if [[ -n $delete_failed && $delete_failed != [Yy]"es" &&
+                    $delete_failed != [Yy] && $delete_failed != [Nn]"o" &&
+                    $delete_failed != [Nn] ]]; then
+                    printf "Select 'y' or 'N'.\n"
+                fi
+            done
+            if [[ $delete_failed == [Yy]"es" || $delete_failed == [Yy] ]]; then
+                # explicit split list of snapshots (on whitespace) into multiple arguments
+                # shellcheck disable=SC2046
+                snapper -c "$x" delete $(snapper -c "$x" list --disable-used-space | awk '/'$name' backup in progress/ {print $1}')
+            fi
+        fi
+    fi
+
+    SNAP_SYNC_EXCLUDE=no
+
+    if [[ -f "/etc/snapper/configs/$x" ]]; then
+        # shellcheck source=/dev/null
+        source "/etc/snapper/configs/$x"
+    else
+        die "Selected snapper configuration $x does not exist."
+    fi
+
+    if [[ $SNAP_SYNC_EXCLUDE == "yes" ]]; then
+        continue
+    fi
+
+    printf "\n"
+
+    old_num=$(snapper -c "$x" list --disable-used-space -t single | awk '/'"subvolid=$selected_subvolid, uuid=$selected_uuid"'/ {print $1}')
+    old_snap=$SUBVOLUME/.snapshots/$old_num/snapshot
+
+    OLD_NUM_ARRAY[i]=$old_num
+    OLD_SNAP_ARRAY[i]=$old_snap
+
+    if [[ -z $old_num ]]; then
+        printf "No backups have been performed for '%s' on this disk.\n" "$x"
+        read -e -r -p "Enter name of subvolume to store backups, relative to $selected_mnt (to be created if not existing): " mybackupdir
+        printf "This will be the initial backup for snapper configuration '%s' to this disk. This could take awhile.\n" "$x"
+        BACKUPDIR="$selected_mnt/$mybackupdir"
+        $ssh test -d "$BACKUPDIR" || $ssh btrfs subvolume create "$BACKUPDIR"
+    else
+        mybackupdir=$(snapper -c "$x" list --disable-used-space -t single | awk -F"|" '/'"subvolid=$selected_subvolid, uuid=$selected_uuid"'/ {print $5}' | awk -F "," '/backupdir/ {print $1}' | awk -F"=" '{print $2}')
+        BACKUPDIR="$selected_mnt/$mybackupdir"
+        $ssh test -d "$BACKUPDIR" || die "%s is not a directory on %s.\n" "$BACKUPDIR" "$selected_uuid"
+    fi
+    BACKUPDIRS_ARRAY[i]="$BACKUPDIR"
+    MYBACKUPDIR_ARRAY[i]="$mybackupdir"
+
+    printf "Creating new local snapshot for '%s' configuration...\n" "$x"
+    new_num=$(snapper -c "$x" create --print-number -d "$name backup in progress")
+    new_snap=$SUBVOLUME/.snapshots/$new_num/snapshot
+    new_info=$SUBVOLUME/.snapshots/$new_num/info.xml
+    sync
+    backup_location=$BACKUPDIR/$x/$new_num/
+    if [[ -z $ssh ]]; then
+        printf "Will backup %s to %s\n" "$new_snap" "$backup_location/snapshot"
+    else
+        printf "Will backup %s to %s\n" "$new_snap" "$remote":"$backup_location/snapshot"
+    fi
+
+    if ($ssh test -d "$backup_location/snapshot"); then
+        printf "WARNING: Backup directory '%s' already exists. This configuration will be skipped!\n" "$backup_location/snapshot"
+        printf "Move or delete destination directory and try backup again.\n"
+    fi
+
+    NEW_NUM_ARRAY[i]="$new_num"
+    NEW_SNAP_ARRAY[i]="$new_snap"
+    NEW_INFO_ARRAY[i]="$new_info"
+    BACKUPLOC_ARRAY[i]="$backup_location"
+
+    cont_backup="K"
+    CONT_BACKUP_ARRAY[i]="yes"
+    if [[ $noconfirm == "yes" ]]; then
+        cont_backup="yes"
+    else
+        while [[ -n $cont_backup && $cont_backup != [Yy]"es" &&
+            $cont_backup != [Yy] && $cont_backup != [Nn]"o" &&
+            $cont_backup != [Nn] ]]; do
+            read -e -r -p "Proceed with backup of '$x' configuration [Y/n]? " cont_backup
+            if [[ -n $cont_backup && $cont_backup != [Yy]"es" &&
+                $cont_backup != [Yy] && $cont_backup != [Nn]"o" &&
+                $cont_backup != [Nn] ]]; then
+                printf "Select 'Y' or 'n'.\n"
+            fi
+        done
+    fi
+
+    if [[ $cont_backup != [Yy]"es" && $cont_backup != [Yy] && -n $cont_backup ]]; then
+        CONT_BACKUP_ARRAY[i]="no"
+        printf "Not backing up '%s' configuration.\n" "$x"
+        snapper -c "$x" delete "$new_num"
+    fi
+
+    i=$((i + 1))
+
+done
+
+# Actual backing up
+printf "\nPerforming backups...\n"
+i=-1
+for x in $selected_configs; do
+
+    i=$((i + 1))
+
+    SNAP_SYNC_EXCLUDE=no
+
+    if [[ -f "/etc/snapper/configs/$x" ]]; then
+        # shellcheck source=/dev/null
+        source "/etc/snapper/configs/$x"
+    else
+        die "Selected snapper configuration $x does not exist."
+    fi
+
+    cont_backup=${CONT_BACKUP_ARRAY[$i]}
+    if [[ $cont_backup == "no" || $SNAP_SYNC_EXCLUDE == "yes" ]]; then
+        notify_info "Backup in progress" "NOTE: Skipping $x configuration."
+        continue
+    fi
+
+    notify_info "Backup in progress" "Backing up $x configuration."
+
+    printf "\n"
+
+    old_num="${OLD_NUM_ARRAY[$i]}"
+    old_snap="${OLD_SNAP_ARRAY[$i]}"
+    BACKUPDIR="${BACKUPDIRS_ARRAY[$i]}"
+    mybackupdir="${MYBACKUPDIR_ARRAY[$i]}"
+    new_num="${NEW_NUM_ARRAY[$i]}"
+    new_snap="${NEW_SNAP_ARRAY[$i]}"
+    new_info="${NEW_INFO_ARRAY[$i]}"
+    backup_location="${BACKUPLOC_ARRAY[$i]}"
+
+    if ($ssh test -d "$backup_location/snapshot"); then
+        printf "ERROR: Backup directory '%s' already exists. Skipping backup of this configuration!\n" "$backup_location/snapshot"
+        continue
+    fi
+
+    $ssh mkdir -p "$backup_location"
+
+    if [[ -z $old_num ]]; then
+        printf "Sending first snapshot for '%s' configuration...\n" "$x"
+        if [[ $doprogress -eq 0 ]]; then
+            btrfs send "$new_snap" | pv | $ssh btrfs receive "$backup_location" &>/dev/null
+        else
+            btrfs send "$new_snap" | $ssh btrfs receive "$backup_location" &>/dev/null
+        fi
+    else
+
+        printf "Sending incremental snapshot for '%s' configuration...\n" "$x"
+        # Sends the difference between the new snapshot and old snapshot to the
+        # backup location. Using the -c flag instead of -p tells it that there
+        # is an identical subvolume to the old snapshot at the receiving
+        # location where it can get its data. This helps speed up the transfer.
+
+        if [[ $doprogress -eq 0 ]]; then
+            btrfs send -c "$old_snap" "$new_snap" | pv | $ssh btrfs receive "$backup_location"
+        else
+            btrfs send -c "$old_snap" "$new_snap" | $ssh btrfs receive "$backup_location"
+        fi
+
+        if [[ $keep == "yes" ]]; then
+            printf "Modifying data for old local snapshot for '%s' configuration...\n" "$x"
+            snapper -v -c "$x" modify -d "old snap-sync snapshot (you may remove)" -u "backupdir=,subvolid=,uuid=" -c "number" "$old_num"
+        else
+            printf "Deleting old snapshot for %s...\n" "$x"
+            snapper -c "$x" delete "$old_num"
+        fi
+
+    fi
+
+    if [[ -z $remote ]]; then
+        cp "$new_info" "$backup_location"
+    else
+        if [[ -z $port ]]; then
+            rsync -avzq "$new_info" "$remote":"$backup_location"
+        else
+            rsync -avzqe "ssh -p $port" "$new_info" "$remote":"$backup_location"
+        fi
+    fi
+
+    # It's important not to change this userdata in the snapshots, since that's how
+    # we find the previous one.
+
+    userdata="backupdir=$mybackupdir, subvolid=$selected_subvolid, uuid=$selected_uuid"
+
+    # Tag new snapshot as the latest
+    printf "Tagging local snapshot as latest backup for '%s' configuration...\n" "$x"
+    snapper -v -c "$x" modify -d "$description" -u "$userdata" "$new_num"
+
+    printf "Backup complete for '%s' configuration.\n" "$x"
+
+done
+
+printf "\nDone!\n"
+
+if [[ $uuid_cmdline != "none" ]]; then
+    notify_info "Finished" "Backups to $uuid_cmdline complete!"
+else
+    notify_info "Finished" "Backups complete!"
+fi
diff --git a/pkgs/sources/tree-sitter-yts/.editorconfig b/pkgs/sources/tree-sitter-yts/.editorconfig
new file mode 100644
index 00000000..919c78fa
--- /dev/null
+++ b/pkgs/sources/tree-sitter-yts/.editorconfig
@@ -0,0 +1,21 @@
+root = true
+
+[*]
+end_of_line = lf
+insert_final_newline = true
+trim_trailing_whitespace = false
+
+# for testing purposes, the corpus may have trailing whitespace
+# and may have mixed EOL.
+# Still want a final newline though, as that makes no semantic difference.
+[corpus/*]
+trim_trailing_whitespace = false
+end_of_line = unset
+
+[**.{js,json,cc,css}]
+indent_style = space
+indent_size = 2
+
+# tree-sitter generate emits json with no trailing newline
+[src/node-types.json]
+insert_final_newline = false
diff --git a/pkgs/sources/tree-sitter-yts/.envrc b/pkgs/sources/tree-sitter-yts/.envrc
new file mode 100644
index 00000000..3550a30f
--- /dev/null
+++ b/pkgs/sources/tree-sitter-yts/.envrc
@@ -0,0 +1 @@
+use flake
diff --git a/pkgs/sources/tree-sitter-yts/.gitignore b/pkgs/sources/tree-sitter-yts/.gitignore
new file mode 100644
index 00000000..c4e2e389
--- /dev/null
+++ b/pkgs/sources/tree-sitter-yts/.gitignore
@@ -0,0 +1,3 @@
+/.direnv
+/result
+/node_modules
diff --git a/pkgs/sources/tree-sitter-yts/Cargo.toml b/pkgs/sources/tree-sitter-yts/Cargo.toml
new file mode 100644
index 00000000..5287c420
--- /dev/null
+++ b/pkgs/sources/tree-sitter-yts/Cargo.toml
@@ -0,0 +1,21 @@
+[package]
+name = "tree-sitter-yts"
+description = "yts grammar for the tree-sitter parsing library"
+version = "0.0.1"
+keywords = ["incremental", "parsing", "yts"]
+categories = ["parsing", "text-editors"]
+repository = "https://github.com/tree-sitter/tree-sitter-yts"
+edition = "2018"
+license = "MIT"
+
+build = "bindings/rust/build.rs"
+include = ["bindings/rust/*", "grammar.js", "queries/*", "src/*"]
+
+[lib]
+path = "bindings/rust/lib.rs"
+
+[dependencies]
+tree-sitter = "~0.20.10"
+
+[build-dependencies]
+cc = "1.0"
diff --git a/pkgs/sources/tree-sitter-yts/binding.gyp b/pkgs/sources/tree-sitter-yts/binding.gyp
new file mode 100644
index 00000000..b05038b4
--- /dev/null
+++ b/pkgs/sources/tree-sitter-yts/binding.gyp
@@ -0,0 +1,19 @@
+{
+  "targets": [
+    {
+      "target_name": "tree_sitter_yts_binding",
+      "include_dirs": [
+        "<!(node -e \"require('nan')\")",
+        "src"
+      ],
+      "sources": [
+        "bindings/node/binding.cc",
+        "src/parser.c",
+        # If your language uses an external scanner, add it here.
+      ],
+      "cflags_c": [
+        "-std=c99",
+      ]
+    }
+  ]
+}
diff --git a/pkgs/sources/tree-sitter-yts/bindings/node/binding.cc b/pkgs/sources/tree-sitter-yts/bindings/node/binding.cc
new file mode 100644
index 00000000..a042be54
--- /dev/null
+++ b/pkgs/sources/tree-sitter-yts/bindings/node/binding.cc
@@ -0,0 +1,33 @@
+#include "nan.h"
+#include "tree_sitter/parser.h"
+#include <node.h>
+
+using namespace v8;
+
+extern "C" TSLanguage *tree_sitter_yts ();
+
+namespace
+{
+
+NAN_METHOD (New) {}
+
+void
+Init (Local<Object> exports, Local<Object> module)
+{
+  Local<FunctionTemplate> tpl = Nan::New<FunctionTemplate> (New);
+  tpl->SetClassName (Nan::New ("Language").ToLocalChecked ());
+  tpl->InstanceTemplate ()->SetInternalFieldCount (1);
+
+  Local<Function> constructor = Nan::GetFunction (tpl).ToLocalChecked ();
+  Local<Object> instance
+      = constructor->NewInstance (Nan::GetCurrentContext ()).ToLocalChecked ();
+  Nan::SetInternalFieldPointer (instance, 0, tree_sitter_yts ());
+
+  Nan::Set (instance, Nan::New ("name").ToLocalChecked (),
+            Nan::New ("yts").ToLocalChecked ());
+  Nan::Set (module, Nan::New ("exports").ToLocalChecked (), instance);
+}
+
+NODE_MODULE (tree_sitter_yts_binding, Init)
+
+} // namespace
diff --git a/pkgs/sources/tree-sitter-yts/bindings/node/index.js b/pkgs/sources/tree-sitter-yts/bindings/node/index.js
new file mode 100644
index 00000000..32179742
--- /dev/null
+++ b/pkgs/sources/tree-sitter-yts/bindings/node/index.js
@@ -0,0 +1,19 @@
+try {
+  module.exports = require("../../build/Release/tree_sitter_yts_binding");
+} catch (error1) {
+  if (error1.code !== "MODULE_NOT_FOUND") {
+    throw error1;
+  }
+  try {
+    module.exports = require("../../build/Debug/tree_sitter_yts_binding");
+  } catch (error2) {
+    if (error2.code !== "MODULE_NOT_FOUND") {
+      throw error2;
+    }
+    throw error1;
+  }
+}
+
+try {
+  module.exports.nodeTypeInfo = require("../../src/node-types.json");
+} catch (_) {}
diff --git a/pkgs/sources/tree-sitter-yts/bindings/rust/build.rs b/pkgs/sources/tree-sitter-yts/bindings/rust/build.rs
new file mode 100644
index 00000000..c6061f09
--- /dev/null
+++ b/pkgs/sources/tree-sitter-yts/bindings/rust/build.rs
@@ -0,0 +1,40 @@
+fn main() {
+    let src_dir = std::path::Path::new("src");
+
+    let mut c_config = cc::Build::new();
+    c_config.include(&src_dir);
+    c_config
+        .flag_if_supported("-Wno-unused-parameter")
+        .flag_if_supported("-Wno-unused-but-set-variable")
+        .flag_if_supported("-Wno-trigraphs");
+    let parser_path = src_dir.join("parser.c");
+    c_config.file(&parser_path);
+
+    // If your language uses an external scanner written in C,
+    // then include this block of code:
+
+    /*
+    let scanner_path = src_dir.join("scanner.c");
+    c_config.file(&scanner_path);
+    println!("cargo:rerun-if-changed={}", scanner_path.to_str().unwrap());
+    */
+
+    c_config.compile("parser");
+    println!("cargo:rerun-if-changed={}", parser_path.to_str().unwrap());
+
+    // If your language uses an external scanner written in C++,
+    // then include this block of code:
+
+    /*
+    let mut cpp_config = cc::Build::new();
+    cpp_config.cpp(true);
+    cpp_config.include(&src_dir);
+    cpp_config
+        .flag_if_supported("-Wno-unused-parameter")
+        .flag_if_supported("-Wno-unused-but-set-variable");
+    let scanner_path = src_dir.join("scanner.cc");
+    cpp_config.file(&scanner_path);
+    cpp_config.compile("scanner");
+    println!("cargo:rerun-if-changed={}", scanner_path.to_str().unwrap());
+    */
+}
diff --git a/pkgs/sources/tree-sitter-yts/bindings/rust/lib.rs b/pkgs/sources/tree-sitter-yts/bindings/rust/lib.rs
new file mode 100644
index 00000000..f1868b2d
--- /dev/null
+++ b/pkgs/sources/tree-sitter-yts/bindings/rust/lib.rs
@@ -0,0 +1,52 @@
+//! This crate provides yts language support for the [tree-sitter][] parsing library.
+//!
+//! Typically, you will use the [language][language func] function to add this language to a
+//! tree-sitter [Parser][], and then use the parser to parse some code:
+//!
+//! ```
+//! let code = "";
+//! let mut parser = tree_sitter::Parser::new();
+//! parser.set_language(tree_sitter_yts::language()).expect("Error loading yts grammar");
+//! let tree = parser.parse(code, None).unwrap();
+//! ```
+//!
+//! [Language]: https://docs.rs/tree-sitter/*/tree_sitter/struct.Language.html
+//! [language func]: fn.language.html
+//! [Parser]: https://docs.rs/tree-sitter/*/tree_sitter/struct.Parser.html
+//! [tree-sitter]: https://tree-sitter.github.io/
+
+use tree_sitter::Language;
+
+extern "C" {
+    fn tree_sitter_yts() -> Language;
+}
+
+/// Get the tree-sitter [Language][] for this grammar.
+///
+/// [Language]: https://docs.rs/tree-sitter/*/tree_sitter/struct.Language.html
+pub fn language() -> Language {
+    unsafe { tree_sitter_yts() }
+}
+
+/// The content of the [`node-types.json`][] file for this grammar.
+///
+/// [`node-types.json`]: https://tree-sitter.github.io/tree-sitter/using-parsers#static-node-types
+pub const NODE_TYPES: &'static str = include_str!("../../src/node-types.json");
+
+// Uncomment these to include any queries that this grammar contains
+
+// pub const HIGHLIGHTS_QUERY: &'static str = include_str!("../../queries/highlights.scm");
+// pub const INJECTIONS_QUERY: &'static str = include_str!("../../queries/injections.scm");
+// pub const LOCALS_QUERY: &'static str = include_str!("../../queries/locals.scm");
+// pub const TAGS_QUERY: &'static str = include_str!("../../queries/tags.scm");
+
+#[cfg(test)]
+mod tests {
+    #[test]
+    fn test_can_load_grammar() {
+        let mut parser = tree_sitter::Parser::new();
+        parser
+            .set_language(super::language())
+            .expect("Error loading yts language");
+    }
+}
diff --git a/pkgs/sources/tree-sitter-yts/corpus/comments.txt b/pkgs/sources/tree-sitter-yts/corpus/comments.txt
new file mode 100644
index 00000000..0070baf8
--- /dev/null
+++ b/pkgs/sources/tree-sitter-yts/corpus/comments.txt
@@ -0,0 +1,51 @@
+================================================================================
+Parse multiple lines
+================================================================================
+
+pick 6221 "Name" "2024-01-17" "A" "[0m 0s]" "url"
+pick 6181 "Name2" "2024-01-16" "A2" "[0m 0s]" "url"
+
+# This is a comment
+# it contains information
+
+--------------------------------------------------------------------------------
+
+(source_file
+  (line
+    (command)
+    (id)
+    (title
+      (quote)
+      (quote))
+    (date
+      (quote)
+      (quote))
+    (author
+      (quote)
+      (quote))
+    (duration
+      (quote)
+      (quote))
+    (url
+      (quote)
+      (quote)))
+  (line
+    (command)
+    (id)
+    (title
+      (quote)
+      (quote))
+    (date
+      (quote)
+      (quote))
+    (author
+      (quote)
+      (quote))
+    (duration
+      (quote)
+      (quote))
+    (url
+      (quote)
+      (quote)))
+  (comment)
+  (comment))
diff --git a/pkgs/sources/tree-sitter-yts/corpus/comments_correct.txt b/pkgs/sources/tree-sitter-yts/corpus/comments_correct.txt
new file mode 100644
index 00000000..40cdab7d
--- /dev/null
+++ b/pkgs/sources/tree-sitter-yts/corpus/comments_correct.txt
@@ -0,0 +1,27 @@
+================================================================================
+Disregard comments in title
+================================================================================
+
+pick 6094 "#100 Name" "2024-01-12" "A" "[133m 29s]" "url"
+
+--------------------------------------------------------------------------------
+
+(source_file
+  (line
+    (command)
+    (id)
+    (title
+      (quote)
+      (quote))
+    (date
+      (quote)
+      (quote))
+    (author
+      (quote)
+      (quote))
+    (duration
+      (quote)
+      (quote))
+    (url
+      (quote)
+      (quote))))
diff --git a/pkgs/sources/tree-sitter-yts/corpus/duration.txt b/pkgs/sources/tree-sitter-yts/corpus/duration.txt
new file mode 100644
index 00000000..59476b98
--- /dev/null
+++ b/pkgs/sources/tree-sitter-yts/corpus/duration.txt
@@ -0,0 +1,84 @@
+================================================================================
+Parse multiple lines with different durations
+================================================================================
+
+pick 6221 "Name" "2024-01-17" "A" "[1h 0m]" "url"
+pick 6181 "Name2" "2024-01-16" "A2" "[20m 02s]" "url2"
+pick 6184 "Name3" "2024-01-16" "A3" "[20h 0m]" "url3"
+pick 6206 "Name4" "2024-01-16" "A4" "[No Duration]" "url4"
+
+--------------------------------------------------------------------------------
+
+(source_file
+  (line
+    (command)
+    (id)
+    (title
+      (quote)
+      (quote))
+    (date
+      (quote)
+      (quote))
+    (author
+      (quote)
+      (quote))
+    (duration
+      (quote)
+      (quote))
+    (url
+      (quote)
+      (quote)))
+  (line
+    (command)
+    (id)
+    (title
+      (quote)
+      (quote))
+    (date
+      (quote)
+      (quote))
+    (author
+      (quote)
+      (quote))
+    (duration
+      (quote)
+      (quote))
+    (url
+      (quote)
+      (quote)))
+  (line
+    (command)
+    (id)
+    (title
+      (quote)
+      (quote))
+    (date
+      (quote)
+      (quote))
+    (author
+      (quote)
+      (quote))
+    (duration
+      (quote)
+      (quote))
+    (url
+      (quote)
+      (quote)))
+  (line
+    (command)
+    (id)
+    (title
+      (quote)
+      (quote))
+    (date
+      (quote)
+      (quote))
+    (author
+      (quote)
+      (quote))
+    (duration
+      (quote)
+      (quote))
+    (url
+      (quote)
+      (quote))))
diff --git a/pkgs/sources/tree-sitter-yts/corpus/url.txt b/pkgs/sources/tree-sitter-yts/corpus/url.txt
new file mode 100644
index 00000000..1ae3d106
--- /dev/null
+++ b/pkgs/sources/tree-sitter-yts/corpus/url.txt
@@ -0,0 +1,84 @@
+================================================================================
+Parse multiple lines with url
+================================================================================
+
+pick 6221 "Name" "2024-01-17" "A" "[0h 0m]" "url"
+pick 6181 "Name2" "2024-01-16" "A2" "[0h 0m]" "url2"
+pick 6184 "Name3" "2024-01-16" "A3" "[0h 0m]" "url3"
+pick 6206 "Name4" "2024-01-16" "A4" "[0h 0m]" "url4"
+
+--------------------------------------------------------------------------------
+
+(source_file
+  (line
+    (command)
+    (id)
+    (title
+      (quote)
+      (quote))
+    (date
+      (quote)
+      (quote))
+    (author
+      (quote)
+      (quote))
+    (duration
+      (quote)
+      (quote))
+    (url
+      (quote)
+      (quote)))
+  (line
+    (command)
+    (id)
+    (title
+      (quote)
+      (quote))
+    (date
+      (quote)
+      (quote))
+    (author
+      (quote)
+      (quote))
+    (duration
+      (quote)
+      (quote))
+    (url
+      (quote)
+      (quote)))
+  (line
+    (command)
+    (id)
+    (title
+      (quote)
+      (quote))
+    (date
+      (quote)
+      (quote))
+    (author
+      (quote)
+      (quote))
+    (duration
+      (quote)
+      (quote))
+    (url
+      (quote)
+      (quote)))
+  (line
+    (command)
+    (id)
+    (title
+      (quote)
+      (quote))
+    (date
+      (quote)
+      (quote))
+    (author
+      (quote)
+      (quote))
+    (duration
+      (quote)
+      (quote))
+    (url
+      (quote)
+      (quote))))
diff --git a/pkgs/sources/tree-sitter-yts/default.nix b/pkgs/sources/tree-sitter-yts/default.nix
new file mode 100644
index 00000000..7e15481c
--- /dev/null
+++ b/pkgs/sources/tree-sitter-yts/default.nix
@@ -0,0 +1,11 @@
+[
+  (
+    final: prev: {
+      yts-grammar = (prev.callPackage ./package.nix {}) {
+        language = "yts";
+        version = "1.0";
+        src = ./.;
+      };
+    }
+  )
+]
diff --git a/pkgs/sources/tree-sitter-yts/flake.lock b/pkgs/sources/tree-sitter-yts/flake.lock
new file mode 100644
index 00000000..bff9f1fa
--- /dev/null
+++ b/pkgs/sources/tree-sitter-yts/flake.lock
@@ -0,0 +1,97 @@
+{
+  "nodes": {
+    "crane": {
+      "inputs": {
+        "nixpkgs": ["nixpkgs"]
+      },
+      "locked": {
+        "lastModified": 1704819371,
+        "narHash": "sha256-oFUfPWrWGQTZaCM3byxwYwrMLwshDxVGOrMH5cVP/X8=",
+        "owner": "ipetkov",
+        "repo": "crane",
+        "rev": "5c234301a1277e4cc759c23a2a7a00a06ddd7111",
+        "type": "github"
+      },
+      "original": {
+        "owner": "ipetkov",
+        "repo": "crane",
+        "type": "github"
+      }
+    },
+    "flake-utils": {
+      "inputs": {
+        "systems": "systems"
+      },
+      "locked": {
+        "lastModified": 1701680307,
+        "narHash": "sha256-kAuep2h5ajznlPMD9rnQyffWG8EM/C73lejGofXvdM8=",
+        "owner": "numtide",
+        "repo": "flake-utils",
+        "rev": "4022d587cbbfd70fe950c1e2083a02621806a725",
+        "type": "github"
+      },
+      "original": {
+        "owner": "numtide",
+        "repo": "flake-utils",
+        "type": "github"
+      }
+    },
+    "nixpkgs": {
+      "locked": {
+        "lastModified": 1705133751,
+        "narHash": "sha256-rCIsyE80jgiOU78gCWN3A0wE0tR2GI5nH6MlS+HaaSQ=",
+        "owner": "NixOS",
+        "repo": "nixpkgs",
+        "rev": "9b19f5e77dd906cb52dade0b7bd280339d2a1f3d",
+        "type": "github"
+      },
+      "original": {
+        "owner": "NixOS",
+        "ref": "nixos-unstable",
+        "repo": "nixpkgs",
+        "type": "github"
+      }
+    },
+    "npmlock2nix": {
+      "flake": false,
+      "locked": {
+        "lastModified": 1673447413,
+        "narHash": "sha256-sJM82Sj8yfQYs9axEmGZ9Evzdv/kDcI9sddqJ45frrU=",
+        "owner": "nix-community",
+        "repo": "npmlock2nix",
+        "rev": "9197bbf397d76059a76310523d45df10d2e4ca81",
+        "type": "github"
+      },
+      "original": {
+        "owner": "nix-community",
+        "repo": "npmlock2nix",
+        "type": "github"
+      }
+    },
+    "root": {
+      "inputs": {
+        "crane": "crane",
+        "flake-utils": "flake-utils",
+        "nixpkgs": "nixpkgs",
+        "npmlock2nix": "npmlock2nix"
+      }
+    },
+    "systems": {
+      "locked": {
+        "lastModified": 1681028828,
+        "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
+        "owner": "nix-systems",
+        "repo": "default",
+        "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
+        "type": "github"
+      },
+      "original": {
+        "owner": "nix-systems",
+        "repo": "default",
+        "type": "github"
+      }
+    }
+  },
+  "root": "root",
+  "version": 7
+}
diff --git a/pkgs/sources/tree-sitter-yts/flake.nix b/pkgs/sources/tree-sitter-yts/flake.nix
new file mode 100644
index 00000000..1b6f8ab0
--- /dev/null
+++ b/pkgs/sources/tree-sitter-yts/flake.nix
@@ -0,0 +1,82 @@
+{
+  description = "tree-sitter-yts";
+
+  inputs = {
+    nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
+
+    flake-utils.url = "github:numtide/flake-utils";
+
+    npmlock2nix = {
+      url = "github:nix-community/npmlock2nix";
+      flake = false;
+    };
+
+    crane = {
+      url = "github:ipetkov/crane";
+      inputs.nixpkgs.follows = "nixpkgs";
+    };
+  };
+
+  outputs = {
+    self,
+    nixpkgs,
+    flake-utils,
+    npmlock2nix,
+    crane,
+  }: (flake-utils.lib.eachDefaultSystem (system: let
+    pkgs = nixpkgs.legacyPackages.${system};
+    inherit (pkgs) lib;
+
+    npmlock2nix' = pkgs.callPackage npmlock2nix {};
+    craneLib = crane.lib.${system};
+  in {
+    build = self.packages.${system}.tree-sitter-nix;
+
+    rust-bindings = craneLib.buildPackage {
+      src = self;
+    };
+
+    # Requires xcode
+    node-bindings = npmlock2nix'.v2.build {
+      src = self;
+      inherit (self.devShells.${system}.default) nativeBuildInputs;
+      inherit (pkgs) nodejs;
+
+      buildCommands = [
+        "${pkgs.nodePackages.node-gyp}/bin/node-gyp configure"
+        "npm run build"
+      ];
+
+      installPhase = ''
+        touch $out
+      '';
+    };
+
+    packages.tree-sitter-yts = (pkgs.callPackage ./grammar.nix {}) {
+      language = "yts";
+      version = "1.0";
+      src = self;
+    };
+
+    packages.default = self.packages.${system}.tree-sitter-yts;
+    devShells.default = pkgs.mkShell {
+      packages = [
+        pkgs.nodejs
+        pkgs.python3
+
+        pkgs.tree-sitter
+        pkgs.editorconfig-checker
+
+        pkgs.rustc
+        pkgs.cargo
+
+        # Formatters
+        pkgs.treefmt
+        pkgs.nixpkgs-fmt
+        pkgs.nodePackages.prettier
+        pkgs.rustfmt
+        pkgs.clang-tools
+      ];
+    };
+  }));
+}
diff --git a/pkgs/sources/tree-sitter-yts/grammar.js b/pkgs/sources/tree-sitter-yts/grammar.js
new file mode 100644
index 00000000..655f6dea
--- /dev/null
+++ b/pkgs/sources/tree-sitter-yts/grammar.js
@@ -0,0 +1,26 @@
+module.exports = grammar({
+  name: "yts",
+
+  rules: {
+    source_file: ($) => repeat(choice($.line, $.comment)),
+    line: ($) =>
+      seq($.command, $.id, $.title, $.date, $.author, $.duration, $.url, "\n"),
+
+    command: ($) => choice("pick", "p", "watch", "w", "drop", "d", "url", "u"),
+    id: ($) => /[0-9]+/,
+    title: ($) => seq($._q, /[^"]+/, $._q),
+    date: ($) => seq($._q, /\d{4}-\d{2}-\d{2}/, $._q),
+    author: ($) => seq($._q, /[^"]+/, $._q),
+    duration: ($) =>
+      seq(
+        $._q,
+        seq("[", choice("No Duration", /\d+m \d+s/, /\d+h \d+m/), "]"),
+        $._q,
+      ),
+    url: ($) => seq($._q, /[^"]+/, $._q),
+    comment: ($) => /#.*/,
+    _q: ($) => $.quote,
+    quote: ($) => /"/,
+  },
+  extras: ($) => [/\s/, /\\\r?\n/],
+});
diff --git a/pkgs/sources/tree-sitter-yts/highlight.yts b/pkgs/sources/tree-sitter-yts/highlight.yts
new file mode 100644
index 00000000..319ee95c
--- /dev/null
+++ b/pkgs/sources/tree-sitter-yts/highlight.yts
@@ -0,0 +1,4 @@
+pick 6221 "Name" "2024-01-17" "A" "0:00" "url"
+pick 6181 "Name2" "2024-01-16" "A2" "0:00" "url2"
+pick 6184 "Name3" "2024-01-16" "A3" "0:00" "url3"
+pick 6206 "Name4" "2024-01-16" "A4" "299:36" "url4"
diff --git a/pkgs/sources/tree-sitter-yts/package.json b/pkgs/sources/tree-sitter-yts/package.json
new file mode 100644
index 00000000..2511ccb7
--- /dev/null
+++ b/pkgs/sources/tree-sitter-yts/package.json
@@ -0,0 +1,31 @@
+{
+  "name": "tree-sitter-yts",
+  "version": "0.0.1",
+  "description": "yts grammar for tree-sitter",
+  "main": "bindings/node",
+  "keywords": [
+    "parsing",
+    "incremental"
+  ],
+  "dependencies": {
+    "nan": "^2.12.1"
+  },
+  "devDependencies": {
+    "tree-sitter-cli": "^0.20.8"
+  },
+  "scripts": {
+    "test": "tree-sitter test"
+  },
+  "tree-sitter": [
+    {
+      "scope": "source.yts",
+      "file-types": [
+        "yts"
+      ],
+      "highlights": [
+        "queries/highlights.scm"
+      ],
+      "injection-regex": "^(yts)$"
+    }
+  ]
+}
diff --git a/pkgs/sources/tree-sitter-yts/package.nix b/pkgs/sources/tree-sitter-yts/package.nix
new file mode 100644
index 00000000..fe9a7326
--- /dev/null
+++ b/pkgs/sources/tree-sitter-yts/package.nix
@@ -0,0 +1,63 @@
+# taken from nixpgks: pkgs/development/tools/parsing/tree-sitter/grammar.nix
+{
+  stdenv,
+  nodejs,
+  tree-sitter,
+  lib,
+}:
+# Build a parser grammar and put the resulting shared object in `$out/parser`
+{
+  # language name
+  language,
+  version,
+  src,
+  location ? null,
+  generate ? false,
+  ...
+} @ args:
+stdenv.mkDerivation ({
+    pname = "${language}-grammar";
+
+    inherit src version;
+
+    nativeBuildInputs = lib.optionals generate [nodejs tree-sitter];
+
+    CFLAGS = ["-Isrc" "-O2"];
+    CXXFLAGS = ["-Isrc" "-O2"];
+
+    stripDebugList = ["parser"];
+
+    configurePhase =
+      lib.optionalString (location != null) ''
+        cd ${location}
+      ''
+      + lib.optionalString generate ''
+        tree-sitter generate
+      '';
+
+    # When both scanner.{c,cc} exist, we should not link both since they may be the same but in
+    # different languages. Just randomly prefer C++ if that happens.
+    buildPhase = ''
+      runHook preBuild
+      if [[ -e src/scanner.cc ]]; then
+        $CXX -fPIC -c src/scanner.cc -o scanner.o $CXXFLAGS
+      elif [[ -e src/scanner.c ]]; then
+        $CC -fPIC -c src/scanner.c -o scanner.o $CFLAGS
+      fi
+      $CC -fPIC -c src/parser.c -o parser.o $CFLAGS
+      rm -rf parser
+      $CXX -shared -o parser *.o
+      runHook postBuild
+    '';
+
+    installPhase = ''
+      runHook preInstall
+      mkdir $out
+      mv parser $out/
+      if [[ -d queries ]]; then
+        cp -r queries $out
+      fi
+      runHook postInstall
+    '';
+  }
+  // removeAttrs args ["language" "location" "generate"])
diff --git a/pkgs/sources/tree-sitter-yts/queries/highlights.scm b/pkgs/sources/tree-sitter-yts/queries/highlights.scm
new file mode 100644
index 00000000..674cbf18
--- /dev/null
+++ b/pkgs/sources/tree-sitter-yts/queries/highlights.scm
@@ -0,0 +1,11 @@
+(command) @keyword
+(id) @constant
+(title) @text.title
+(date) @number
+(author) @operator
+(duration) @property
+((url) @conceal (#set! conceal ""))
+
+
+((quote) @conceal (#set! conceal ""))
+(comment) @comment @spell
diff --git a/pkgs/sources/tree-sitter-yts/src/grammar.json b/pkgs/sources/tree-sitter-yts/src/grammar.json
new file mode 100644
index 00000000..a35a5464
--- /dev/null
+++ b/pkgs/sources/tree-sitter-yts/src/grammar.json
@@ -0,0 +1,238 @@
+{
+  "name": "yts",
+  "rules": {
+    "source_file": {
+      "type": "REPEAT",
+      "content": {
+        "type": "CHOICE",
+        "members": [
+          {
+            "type": "SYMBOL",
+            "name": "line"
+          },
+          {
+            "type": "SYMBOL",
+            "name": "comment"
+          }
+        ]
+      }
+    },
+    "line": {
+      "type": "SEQ",
+      "members": [
+        {
+          "type": "SYMBOL",
+          "name": "command"
+        },
+        {
+          "type": "SYMBOL",
+          "name": "id"
+        },
+        {
+          "type": "SYMBOL",
+          "name": "title"
+        },
+        {
+          "type": "SYMBOL",
+          "name": "date"
+        },
+        {
+          "type": "SYMBOL",
+          "name": "author"
+        },
+        {
+          "type": "SYMBOL",
+          "name": "duration"
+        },
+        {
+          "type": "SYMBOL",
+          "name": "url"
+        },
+        {
+          "type": "STRING",
+          "value": "\n"
+        }
+      ]
+    },
+    "command": {
+      "type": "CHOICE",
+      "members": [
+        {
+          "type": "STRING",
+          "value": "pick"
+        },
+        {
+          "type": "STRING",
+          "value": "p"
+        },
+        {
+          "type": "STRING",
+          "value": "watch"
+        },
+        {
+          "type": "STRING",
+          "value": "w"
+        },
+        {
+          "type": "STRING",
+          "value": "drop"
+        },
+        {
+          "type": "STRING",
+          "value": "d"
+        },
+        {
+          "type": "STRING",
+          "value": "url"
+        },
+        {
+          "type": "STRING",
+          "value": "u"
+        }
+      ]
+    },
+    "id": {
+      "type": "PATTERN",
+      "value": "[0-9]+"
+    },
+    "title": {
+      "type": "SEQ",
+      "members": [
+        {
+          "type": "SYMBOL",
+          "name": "_q"
+        },
+        {
+          "type": "PATTERN",
+          "value": "[^\"]+"
+        },
+        {
+          "type": "SYMBOL",
+          "name": "_q"
+        }
+      ]
+    },
+    "date": {
+      "type": "SEQ",
+      "members": [
+        {
+          "type": "SYMBOL",
+          "name": "_q"
+        },
+        {
+          "type": "PATTERN",
+          "value": "\\d{4}-\\d{2}-\\d{2}"
+        },
+        {
+          "type": "SYMBOL",
+          "name": "_q"
+        }
+      ]
+    },
+    "author": {
+      "type": "SEQ",
+      "members": [
+        {
+          "type": "SYMBOL",
+          "name": "_q"
+        },
+        {
+          "type": "PATTERN",
+          "value": "[^\"]+"
+        },
+        {
+          "type": "SYMBOL",
+          "name": "_q"
+        }
+      ]
+    },
+    "duration": {
+      "type": "SEQ",
+      "members": [
+        {
+          "type": "SYMBOL",
+          "name": "_q"
+        },
+        {
+          "type": "SEQ",
+          "members": [
+            {
+              "type": "STRING",
+              "value": "["
+            },
+            {
+              "type": "CHOICE",
+              "members": [
+                {
+                  "type": "STRING",
+                  "value": "No Duration"
+                },
+                {
+                  "type": "PATTERN",
+                  "value": "\\d+m \\d+s"
+                },
+                {
+                  "type": "PATTERN",
+                  "value": "\\d+h \\d+m"
+                }
+              ]
+            },
+            {
+              "type": "STRING",
+              "value": "]"
+            }
+          ]
+        },
+        {
+          "type": "SYMBOL",
+          "name": "_q"
+        }
+      ]
+    },
+    "url": {
+      "type": "SEQ",
+      "members": [
+        {
+          "type": "SYMBOL",
+          "name": "_q"
+        },
+        {
+          "type": "PATTERN",
+          "value": "[^\"]+"
+        },
+        {
+          "type": "SYMBOL",
+          "name": "_q"
+        }
+      ]
+    },
+    "comment": {
+      "type": "PATTERN",
+      "value": "#.*"
+    },
+    "_q": {
+      "type": "SYMBOL",
+      "name": "quote"
+    },
+    "quote": {
+      "type": "PATTERN",
+      "value": "\""
+    }
+  },
+  "extras": [
+    {
+      "type": "PATTERN",
+      "value": "\\s"
+    },
+    {
+      "type": "PATTERN",
+      "value": "\\\\\\r?\\n"
+    }
+  ],
+  "conflicts": [],
+  "precedences": [],
+  "externals": [],
+  "inline": [],
+  "supertypes": []
+}
+
diff --git a/pkgs/sources/tree-sitter-yts/src/node-types.json b/pkgs/sources/tree-sitter-yts/src/node-types.json
new file mode 100644
index 00000000..1a63a552
--- /dev/null
+++ b/pkgs/sources/tree-sitter-yts/src/node-types.json
@@ -0,0 +1,200 @@
+[
+  {
+    "type": "author",
+    "named": true,
+    "fields": {},
+    "children": {
+      "multiple": true,
+      "required": true,
+      "types": [
+        {
+          "type": "quote",
+          "named": true
+        }
+      ]
+    }
+  },
+  {
+    "type": "command",
+    "named": true,
+    "fields": {}
+  },
+  {
+    "type": "date",
+    "named": true,
+    "fields": {},
+    "children": {
+      "multiple": true,
+      "required": true,
+      "types": [
+        {
+          "type": "quote",
+          "named": true
+        }
+      ]
+    }
+  },
+  {
+    "type": "duration",
+    "named": true,
+    "fields": {},
+    "children": {
+      "multiple": true,
+      "required": true,
+      "types": [
+        {
+          "type": "quote",
+          "named": true
+        }
+      ]
+    }
+  },
+  {
+    "type": "line",
+    "named": true,
+    "fields": {},
+    "children": {
+      "multiple": true,
+      "required": true,
+      "types": [
+        {
+          "type": "author",
+          "named": true
+        },
+        {
+          "type": "command",
+          "named": true
+        },
+        {
+          "type": "date",
+          "named": true
+        },
+        {
+          "type": "duration",
+          "named": true
+        },
+        {
+          "type": "id",
+          "named": true
+        },
+        {
+          "type": "title",
+          "named": true
+        },
+        {
+          "type": "url",
+          "named": true
+        }
+      ]
+    }
+  },
+  {
+    "type": "source_file",
+    "named": true,
+    "fields": {},
+    "children": {
+      "multiple": true,
+      "required": false,
+      "types": [
+        {
+          "type": "comment",
+          "named": true
+        },
+        {
+          "type": "line",
+          "named": true
+        }
+      ]
+    }
+  },
+  {
+    "type": "title",
+    "named": true,
+    "fields": {},
+    "children": {
+      "multiple": true,
+      "required": true,
+      "types": [
+        {
+          "type": "quote",
+          "named": true
+        }
+      ]
+    }
+  },
+  {
+    "type": "url",
+    "named": true,
+    "fields": {},
+    "children": {
+      "multiple": true,
+      "required": true,
+      "types": [
+        {
+          "type": "quote",
+          "named": true
+        }
+      ]
+    }
+  },
+  {
+    "type": "\n",
+    "named": false
+  },
+  {
+    "type": "No Duration",
+    "named": false
+  },
+  {
+    "type": "[",
+    "named": false
+  },
+  {
+    "type": "]",
+    "named": false
+  },
+  {
+    "type": "comment",
+    "named": true
+  },
+  {
+    "type": "d",
+    "named": false
+  },
+  {
+    "type": "drop",
+    "named": false
+  },
+  {
+    "type": "id",
+    "named": true
+  },
+  {
+    "type": "p",
+    "named": false
+  },
+  {
+    "type": "pick",
+    "named": false
+  },
+  {
+    "type": "quote",
+    "named": true
+  },
+  {
+    "type": "u",
+    "named": false
+  },
+  {
+    "type": "url",
+    "named": false
+  },
+  {
+    "type": "w",
+    "named": false
+  },
+  {
+    "type": "watch",
+    "named": false
+  }
+]
\ No newline at end of file
diff --git a/pkgs/sources/tree-sitter-yts/src/parser.c b/pkgs/sources/tree-sitter-yts/src/parser.c
new file mode 100644
index 00000000..ded5c342
--- /dev/null
+++ b/pkgs/sources/tree-sitter-yts/src/parser.c
@@ -0,0 +1,1108 @@
+#include <tree_sitter/parser.h>
+
+#if defined(__GNUC__) || defined(__clang__)
+#pragma GCC diagnostic push
+#pragma GCC diagnostic ignored "-Wmissing-field-initializers"
+#endif
+
+#define LANGUAGE_VERSION 14
+#define STATE_COUNT 31
+#define LARGE_STATE_COUNT 2
+#define SYMBOL_COUNT 30
+#define ALIAS_COUNT 0
+#define TOKEN_COUNT 20
+#define EXTERNAL_TOKEN_COUNT 0
+#define FIELD_COUNT 0
+#define MAX_ALIAS_SEQUENCE_LENGTH 8
+#define PRODUCTION_ID_COUNT 1
+
+enum
+{
+  anon_sym_LF = 1,
+  anon_sym_pick = 2,
+  anon_sym_p = 3,
+  anon_sym_watch = 4,
+  anon_sym_w = 5,
+  anon_sym_drop = 6,
+  anon_sym_d = 7,
+  anon_sym_url = 8,
+  anon_sym_u = 9,
+  sym_id = 10,
+  aux_sym_title_token1 = 11,
+  aux_sym_date_token1 = 12,
+  anon_sym_LBRACK = 13,
+  anon_sym_NoDuration = 14,
+  aux_sym_duration_token1 = 15,
+  aux_sym_duration_token2 = 16,
+  anon_sym_RBRACK = 17,
+  sym_comment = 18,
+  sym_quote = 19,
+  sym_source_file = 20,
+  sym_line = 21,
+  sym_command = 22,
+  sym_title = 23,
+  sym_date = 24,
+  sym_author = 25,
+  sym_duration = 26,
+  sym_url = 27,
+  sym__q = 28,
+  aux_sym_source_file_repeat1 = 29,
+};
+
+static const char *const ts_symbol_names[] = {
+  [ts_builtin_sym_end] = "end",
+  [anon_sym_LF] = "\n",
+  [anon_sym_pick] = "pick",
+  [anon_sym_p] = "p",
+  [anon_sym_watch] = "watch",
+  [anon_sym_w] = "w",
+  [anon_sym_drop] = "drop",
+  [anon_sym_d] = "d",
+  [anon_sym_url] = "url",
+  [anon_sym_u] = "u",
+  [sym_id] = "id",
+  [aux_sym_title_token1] = "title_token1",
+  [aux_sym_date_token1] = "date_token1",
+  [anon_sym_LBRACK] = "[",
+  [anon_sym_NoDuration] = "No Duration",
+  [aux_sym_duration_token1] = "duration_token1",
+  [aux_sym_duration_token2] = "duration_token2",
+  [anon_sym_RBRACK] = "]",
+  [sym_comment] = "comment",
+  [sym_quote] = "quote",
+  [sym_source_file] = "source_file",
+  [sym_line] = "line",
+  [sym_command] = "command",
+  [sym_title] = "title",
+  [sym_date] = "date",
+  [sym_author] = "author",
+  [sym_duration] = "duration",
+  [sym_url] = "url",
+  [sym__q] = "_q",
+  [aux_sym_source_file_repeat1] = "source_file_repeat1",
+};
+
+static const TSSymbol ts_symbol_map[] = {
+  [ts_builtin_sym_end] = ts_builtin_sym_end,
+  [anon_sym_LF] = anon_sym_LF,
+  [anon_sym_pick] = anon_sym_pick,
+  [anon_sym_p] = anon_sym_p,
+  [anon_sym_watch] = anon_sym_watch,
+  [anon_sym_w] = anon_sym_w,
+  [anon_sym_drop] = anon_sym_drop,
+  [anon_sym_d] = anon_sym_d,
+  [anon_sym_url] = anon_sym_url,
+  [anon_sym_u] = anon_sym_u,
+  [sym_id] = sym_id,
+  [aux_sym_title_token1] = aux_sym_title_token1,
+  [aux_sym_date_token1] = aux_sym_date_token1,
+  [anon_sym_LBRACK] = anon_sym_LBRACK,
+  [anon_sym_NoDuration] = anon_sym_NoDuration,
+  [aux_sym_duration_token1] = aux_sym_duration_token1,
+  [aux_sym_duration_token2] = aux_sym_duration_token2,
+  [anon_sym_RBRACK] = anon_sym_RBRACK,
+  [sym_comment] = sym_comment,
+  [sym_quote] = sym_quote,
+  [sym_source_file] = sym_source_file,
+  [sym_line] = sym_line,
+  [sym_command] = sym_command,
+  [sym_title] = sym_title,
+  [sym_date] = sym_date,
+  [sym_author] = sym_author,
+  [sym_duration] = sym_duration,
+  [sym_url] = sym_url,
+  [sym__q] = sym__q,
+  [aux_sym_source_file_repeat1] = aux_sym_source_file_repeat1,
+};
+
+static const TSSymbolMetadata ts_symbol_metadata[] = {
+  [ts_builtin_sym_end] = {
+    .visible = false,
+    .named = true,
+  },
+  [anon_sym_LF] = {
+    .visible = true,
+    .named = false,
+  },
+  [anon_sym_pick] = {
+    .visible = true,
+    .named = false,
+  },
+  [anon_sym_p] = {
+    .visible = true,
+    .named = false,
+  },
+  [anon_sym_watch] = {
+    .visible = true,
+    .named = false,
+  },
+  [anon_sym_w] = {
+    .visible = true,
+    .named = false,
+  },
+  [anon_sym_drop] = {
+    .visible = true,
+    .named = false,
+  },
+  [anon_sym_d] = {
+    .visible = true,
+    .named = false,
+  },
+  [anon_sym_url] = {
+    .visible = true,
+    .named = false,
+  },
+  [anon_sym_u] = {
+    .visible = true,
+    .named = false,
+  },
+  [sym_id] = {
+    .visible = true,
+    .named = true,
+  },
+  [aux_sym_title_token1] = {
+    .visible = false,
+    .named = false,
+  },
+  [aux_sym_date_token1] = {
+    .visible = false,
+    .named = false,
+  },
+  [anon_sym_LBRACK] = {
+    .visible = true,
+    .named = false,
+  },
+  [anon_sym_NoDuration] = {
+    .visible = true,
+    .named = false,
+  },
+  [aux_sym_duration_token1] = {
+    .visible = false,
+    .named = false,
+  },
+  [aux_sym_duration_token2] = {
+    .visible = false,
+    .named = false,
+  },
+  [anon_sym_RBRACK] = {
+    .visible = true,
+    .named = false,
+  },
+  [sym_comment] = {
+    .visible = true,
+    .named = true,
+  },
+  [sym_quote] = {
+    .visible = true,
+    .named = true,
+  },
+  [sym_source_file] = {
+    .visible = true,
+    .named = true,
+  },
+  [sym_line] = {
+    .visible = true,
+    .named = true,
+  },
+  [sym_command] = {
+    .visible = true,
+    .named = true,
+  },
+  [sym_title] = {
+    .visible = true,
+    .named = true,
+  },
+  [sym_date] = {
+    .visible = true,
+    .named = true,
+  },
+  [sym_author] = {
+    .visible = true,
+    .named = true,
+  },
+  [sym_duration] = {
+    .visible = true,
+    .named = true,
+  },
+  [sym_url] = {
+    .visible = true,
+    .named = true,
+  },
+  [sym__q] = {
+    .visible = false,
+    .named = true,
+  },
+  [aux_sym_source_file_repeat1] = {
+    .visible = false,
+    .named = false,
+  },
+};
+
+static const TSSymbol ts_alias_sequences[PRODUCTION_ID_COUNT]
+                                        [MAX_ALIAS_SEQUENCE_LENGTH]
+    = {
+        [0] = { 0 },
+      };
+
+static const uint16_t ts_non_terminal_alias_map[] = {
+  0,
+};
+
+static const TSStateId ts_primary_state_ids[STATE_COUNT] = {
+  [0] = 0,   [1] = 1,   [2] = 2,   [3] = 3,   [4] = 4,   [5] = 5,   [6] = 6,
+  [7] = 7,   [8] = 8,   [9] = 9,   [10] = 10, [11] = 11, [12] = 12, [13] = 13,
+  [14] = 14, [15] = 15, [16] = 16, [17] = 17, [18] = 18, [19] = 19, [20] = 20,
+  [21] = 21, [22] = 22, [23] = 23, [24] = 24, [25] = 25, [26] = 26, [27] = 27,
+  [28] = 28, [29] = 29, [30] = 30,
+};
+
+static bool
+ts_lex (TSLexer *lexer, TSStateId state)
+{
+  START_LEXER ();
+  eof = lexer->eof (lexer);
+  switch (state)
+    {
+    case 0:
+      if (eof)
+        ADVANCE (47);
+      if (lookahead == '"')
+        ADVANCE (74);
+      if (lookahead == '#')
+        ADVANCE (73);
+      if (lookahead == 'N')
+        ADVANCE (30);
+      if (lookahead == '[')
+        ADVANCE (68);
+      if (lookahead == '\\')
+        SKIP (46)
+      if (lookahead == ']')
+        ADVANCE (72);
+      if (lookahead == 'd')
+        ADVANCE (54);
+      if (lookahead == 'p')
+        ADVANCE (50);
+      if (lookahead == 'u')
+        ADVANCE (56);
+      if (lookahead == 'w')
+        ADVANCE (52);
+      if (lookahead == '\t' || lookahead == '\n' || lookahead == '\r'
+          || lookahead == ' ')
+        SKIP (0)
+      if (('0' <= lookahead && lookahead <= '9'))
+        ADVANCE (60);
+      END_STATE ();
+    case 1:
+      if (lookahead == '\n')
+        SKIP (14)
+      END_STATE ();
+    case 2:
+      if (lookahead == '\n')
+        SKIP (14)
+      if (lookahead == '\r')
+        SKIP (1)
+      END_STATE ();
+    case 3:
+      if (lookahead == '\n')
+        SKIP (16)
+      END_STATE ();
+    case 4:
+      if (lookahead == '\n')
+        SKIP (16)
+      if (lookahead == '\r')
+        SKIP (3)
+      END_STATE ();
+    case 5:
+      if (lookahead == '\n')
+        SKIP (7)
+      END_STATE ();
+    case 6:
+      if (lookahead == '\n')
+        SKIP (7)
+      if (lookahead == '\r')
+        SKIP (5)
+      END_STATE ();
+    case 7:
+      if (lookahead == '\n')
+        ADVANCE (48);
+      if (lookahead == '\\')
+        SKIP (6)
+      if (lookahead == '\t' || lookahead == '\r' || lookahead == ' ')
+        SKIP (7)
+      END_STATE ();
+    case 8:
+      if (lookahead == ' ')
+        ADVANCE (13);
+      END_STATE ();
+    case 9:
+      if (lookahead == ' ')
+        ADVANCE (39);
+      END_STATE ();
+    case 10:
+      if (lookahead == ' ')
+        ADVANCE (40);
+      END_STATE ();
+    case 11:
+      if (lookahead == '-')
+        ADVANCE (43);
+      if (lookahead == 'h')
+        ADVANCE (9);
+      if (lookahead == 'm')
+        ADVANCE (10);
+      if (('0' <= lookahead && lookahead <= '9'))
+        ADVANCE (24);
+      END_STATE ();
+    case 12:
+      if (lookahead == '-')
+        ADVANCE (44);
+      END_STATE ();
+    case 13:
+      if (lookahead == 'D')
+        ADVANCE (38);
+      END_STATE ();
+    case 14:
+      if (lookahead == 'N')
+        ADVANCE (30);
+      if (lookahead == '\\')
+        SKIP (2)
+      if (lookahead == '\t' || lookahead == '\n' || lookahead == '\r'
+          || lookahead == ' ')
+        SKIP (14)
+      if (('0' <= lookahead && lookahead <= '9'))
+        ADVANCE (23);
+      END_STATE ();
+    case 15:
+      if (lookahead == '\\')
+        ADVANCE (63);
+      if (lookahead == '\t' || lookahead == '\n' || lookahead == '\r'
+          || lookahead == ' ')
+        ADVANCE (65);
+      if (lookahead != 0 && lookahead != '"')
+        ADVANCE (66);
+      END_STATE ();
+    case 16:
+      if (lookahead == '\\')
+        SKIP (4)
+      if (lookahead == '\t' || lookahead == '\n' || lookahead == '\r'
+          || lookahead == ' ')
+        SKIP (16)
+      if (('0' <= lookahead && lookahead <= '9'))
+        ADVANCE (62);
+      END_STATE ();
+    case 17:
+      if (lookahead == 'a')
+        ADVANCE (36);
+      END_STATE ();
+    case 18:
+      if (lookahead == 'c')
+        ADVANCE (26);
+      END_STATE ();
+    case 19:
+      if (lookahead == 'c')
+        ADVANCE (20);
+      END_STATE ();
+    case 20:
+      if (lookahead == 'h')
+        ADVANCE (51);
+      END_STATE ();
+    case 21:
+      if (lookahead == 'h')
+        ADVANCE (9);
+      if (lookahead == 'm')
+        ADVANCE (10);
+      if (('0' <= lookahead && lookahead <= '9'))
+        ADVANCE (11);
+      END_STATE ();
+    case 22:
+      if (lookahead == 'h')
+        ADVANCE (9);
+      if (lookahead == 'm')
+        ADVANCE (10);
+      if (('0' <= lookahead && lookahead <= '9'))
+        ADVANCE (21);
+      END_STATE ();
+    case 23:
+      if (lookahead == 'h')
+        ADVANCE (9);
+      if (lookahead == 'm')
+        ADVANCE (10);
+      if (('0' <= lookahead && lookahead <= '9'))
+        ADVANCE (22);
+      END_STATE ();
+    case 24:
+      if (lookahead == 'h')
+        ADVANCE (9);
+      if (lookahead == 'm')
+        ADVANCE (10);
+      if (('0' <= lookahead && lookahead <= '9'))
+        ADVANCE (24);
+      END_STATE ();
+    case 25:
+      if (lookahead == 'i')
+        ADVANCE (32);
+      END_STATE ();
+    case 26:
+      if (lookahead == 'k')
+        ADVANCE (49);
+      END_STATE ();
+    case 27:
+      if (lookahead == 'l')
+        ADVANCE (55);
+      END_STATE ();
+    case 28:
+      if (lookahead == 'm')
+        ADVANCE (71);
+      if (('0' <= lookahead && lookahead <= '9'))
+        ADVANCE (28);
+      END_STATE ();
+    case 29:
+      if (lookahead == 'n')
+        ADVANCE (69);
+      END_STATE ();
+    case 30:
+      if (lookahead == 'o')
+        ADVANCE (8);
+      END_STATE ();
+    case 31:
+      if (lookahead == 'o')
+        ADVANCE (33);
+      END_STATE ();
+    case 32:
+      if (lookahead == 'o')
+        ADVANCE (29);
+      END_STATE ();
+    case 33:
+      if (lookahead == 'p')
+        ADVANCE (53);
+      END_STATE ();
+    case 34:
+      if (lookahead == 'r')
+        ADVANCE (17);
+      END_STATE ();
+    case 35:
+      if (lookahead == 's')
+        ADVANCE (70);
+      if (('0' <= lookahead && lookahead <= '9'))
+        ADVANCE (35);
+      END_STATE ();
+    case 36:
+      if (lookahead == 't')
+        ADVANCE (25);
+      END_STATE ();
+    case 37:
+      if (lookahead == 't')
+        ADVANCE (19);
+      END_STATE ();
+    case 38:
+      if (lookahead == 'u')
+        ADVANCE (34);
+      END_STATE ();
+    case 39:
+      if (('0' <= lookahead && lookahead <= '9'))
+        ADVANCE (28);
+      END_STATE ();
+    case 40:
+      if (('0' <= lookahead && lookahead <= '9'))
+        ADVANCE (35);
+      END_STATE ();
+    case 41:
+      if (('0' <= lookahead && lookahead <= '9'))
+        ADVANCE (12);
+      END_STATE ();
+    case 42:
+      if (('0' <= lookahead && lookahead <= '9'))
+        ADVANCE (67);
+      END_STATE ();
+    case 43:
+      if (('0' <= lookahead && lookahead <= '9'))
+        ADVANCE (41);
+      END_STATE ();
+    case 44:
+      if (('0' <= lookahead && lookahead <= '9'))
+        ADVANCE (42);
+      END_STATE ();
+    case 45:
+      if (eof)
+        ADVANCE (47);
+      if (lookahead == '\n')
+        SKIP (0)
+      END_STATE ();
+    case 46:
+      if (eof)
+        ADVANCE (47);
+      if (lookahead == '\n')
+        SKIP (0)
+      if (lookahead == '\r')
+        SKIP (45)
+      END_STATE ();
+    case 47:
+      ACCEPT_TOKEN (ts_builtin_sym_end);
+      END_STATE ();
+    case 48:
+      ACCEPT_TOKEN (anon_sym_LF);
+      if (lookahead == '\n')
+        ADVANCE (48);
+      END_STATE ();
+    case 49:
+      ACCEPT_TOKEN (anon_sym_pick);
+      END_STATE ();
+    case 50:
+      ACCEPT_TOKEN (anon_sym_p);
+      if (lookahead == 'i')
+        ADVANCE (18);
+      END_STATE ();
+    case 51:
+      ACCEPT_TOKEN (anon_sym_watch);
+      END_STATE ();
+    case 52:
+      ACCEPT_TOKEN (anon_sym_w);
+      if (lookahead == 'a')
+        ADVANCE (37);
+      END_STATE ();
+    case 53:
+      ACCEPT_TOKEN (anon_sym_drop);
+      END_STATE ();
+    case 54:
+      ACCEPT_TOKEN (anon_sym_d);
+      if (lookahead == 'r')
+        ADVANCE (31);
+      END_STATE ();
+    case 55:
+      ACCEPT_TOKEN (anon_sym_url);
+      END_STATE ();
+    case 56:
+      ACCEPT_TOKEN (anon_sym_u);
+      if (lookahead == 'r')
+        ADVANCE (27);
+      END_STATE ();
+    case 57:
+      ACCEPT_TOKEN (sym_id);
+      if (lookahead == '-')
+        ADVANCE (43);
+      if (lookahead == 'h')
+        ADVANCE (9);
+      if (lookahead == 'm')
+        ADVANCE (10);
+      if (('0' <= lookahead && lookahead <= '9'))
+        ADVANCE (61);
+      END_STATE ();
+    case 58:
+      ACCEPT_TOKEN (sym_id);
+      if (lookahead == 'h')
+        ADVANCE (9);
+      if (lookahead == 'm')
+        ADVANCE (10);
+      if (('0' <= lookahead && lookahead <= '9'))
+        ADVANCE (57);
+      END_STATE ();
+    case 59:
+      ACCEPT_TOKEN (sym_id);
+      if (lookahead == 'h')
+        ADVANCE (9);
+      if (lookahead == 'm')
+        ADVANCE (10);
+      if (('0' <= lookahead && lookahead <= '9'))
+        ADVANCE (58);
+      END_STATE ();
+    case 60:
+      ACCEPT_TOKEN (sym_id);
+      if (lookahead == 'h')
+        ADVANCE (9);
+      if (lookahead == 'm')
+        ADVANCE (10);
+      if (('0' <= lookahead && lookahead <= '9'))
+        ADVANCE (59);
+      END_STATE ();
+    case 61:
+      ACCEPT_TOKEN (sym_id);
+      if (lookahead == 'h')
+        ADVANCE (9);
+      if (lookahead == 'm')
+        ADVANCE (10);
+      if (('0' <= lookahead && lookahead <= '9'))
+        ADVANCE (61);
+      END_STATE ();
+    case 62:
+      ACCEPT_TOKEN (sym_id);
+      if (('0' <= lookahead && lookahead <= '9'))
+        ADVANCE (62);
+      END_STATE ();
+    case 63:
+      ACCEPT_TOKEN (aux_sym_title_token1);
+      if (lookahead == '\n')
+        ADVANCE (65);
+      if (lookahead == '\r')
+        ADVANCE (64);
+      if (lookahead != 0 && lookahead != '"')
+        ADVANCE (66);
+      END_STATE ();
+    case 64:
+      ACCEPT_TOKEN (aux_sym_title_token1);
+      if (lookahead == '\n')
+        ADVANCE (65);
+      if (lookahead != 0 && lookahead != '"')
+        ADVANCE (66);
+      END_STATE ();
+    case 65:
+      ACCEPT_TOKEN (aux_sym_title_token1);
+      if (lookahead == '\\')
+        ADVANCE (63);
+      if (lookahead == '\t' || lookahead == '\n' || lookahead == '\r'
+          || lookahead == ' ')
+        ADVANCE (65);
+      if (lookahead != 0 && lookahead != '"')
+        ADVANCE (66);
+      END_STATE ();
+    case 66:
+      ACCEPT_TOKEN (aux_sym_title_token1);
+      if (lookahead != 0 && lookahead != '"')
+        ADVANCE (66);
+      END_STATE ();
+    case 67:
+      ACCEPT_TOKEN (aux_sym_date_token1);
+      END_STATE ();
+    case 68:
+      ACCEPT_TOKEN (anon_sym_LBRACK);
+      END_STATE ();
+    case 69:
+      ACCEPT_TOKEN (anon_sym_NoDuration);
+      END_STATE ();
+    case 70:
+      ACCEPT_TOKEN (aux_sym_duration_token1);
+      END_STATE ();
+    case 71:
+      ACCEPT_TOKEN (aux_sym_duration_token2);
+      END_STATE ();
+    case 72:
+      ACCEPT_TOKEN (anon_sym_RBRACK);
+      END_STATE ();
+    case 73:
+      ACCEPT_TOKEN (sym_comment);
+      if (lookahead != 0 && lookahead != '\n')
+        ADVANCE (73);
+      END_STATE ();
+    case 74:
+      ACCEPT_TOKEN (sym_quote);
+      END_STATE ();
+    default:
+      return false;
+    }
+}
+
+static const TSLexMode ts_lex_modes[STATE_COUNT] = {
+  [0] = { .lex_state = 0 },   [1] = { .lex_state = 0 },
+  [2] = { .lex_state = 0 },   [3] = { .lex_state = 0 },
+  [4] = { .lex_state = 0 },   [5] = { .lex_state = 0 },
+  [6] = { .lex_state = 0 },   [7] = { .lex_state = 0 },
+  [8] = { .lex_state = 14 },  [9] = { .lex_state = 0 },
+  [10] = { .lex_state = 0 },  [11] = { .lex_state = 0 },
+  [12] = { .lex_state = 0 },  [13] = { .lex_state = 0 },
+  [14] = { .lex_state = 0 },  [15] = { .lex_state = 0 },
+  [16] = { .lex_state = 15 }, [17] = { .lex_state = 0 },
+  [18] = { .lex_state = 0 },  [19] = { .lex_state = 16 },
+  [20] = { .lex_state = 0 },  [21] = { .lex_state = 7 },
+  [22] = { .lex_state = 15 }, [23] = { .lex_state = 14 },
+  [24] = { .lex_state = 0 },  [25] = { .lex_state = 15 },
+  [26] = { .lex_state = 16 }, [27] = { .lex_state = 0 },
+  [28] = { .lex_state = 7 },  [29] = { .lex_state = 0 },
+  [30] = { .lex_state = 0 },
+};
+
+static const uint16_t ts_parse_table[LARGE_STATE_COUNT][SYMBOL_COUNT] = {
+  [0] = {
+    [ts_builtin_sym_end] = ACTIONS(1),
+    [anon_sym_pick] = ACTIONS(1),
+    [anon_sym_p] = ACTIONS(1),
+    [anon_sym_watch] = ACTIONS(1),
+    [anon_sym_w] = ACTIONS(1),
+    [anon_sym_drop] = ACTIONS(1),
+    [anon_sym_d] = ACTIONS(1),
+    [anon_sym_url] = ACTIONS(1),
+    [anon_sym_u] = ACTIONS(1),
+    [sym_id] = ACTIONS(1),
+    [aux_sym_date_token1] = ACTIONS(1),
+    [anon_sym_LBRACK] = ACTIONS(1),
+    [anon_sym_NoDuration] = ACTIONS(1),
+    [aux_sym_duration_token1] = ACTIONS(1),
+    [aux_sym_duration_token2] = ACTIONS(1),
+    [anon_sym_RBRACK] = ACTIONS(1),
+    [sym_comment] = ACTIONS(1),
+    [sym_quote] = ACTIONS(1),
+  },
+  [1] = {
+    [sym_source_file] = STATE(29),
+    [sym_line] = STATE(2),
+    [sym_command] = STATE(26),
+    [aux_sym_source_file_repeat1] = STATE(2),
+    [ts_builtin_sym_end] = ACTIONS(3),
+    [anon_sym_pick] = ACTIONS(5),
+    [anon_sym_p] = ACTIONS(7),
+    [anon_sym_watch] = ACTIONS(5),
+    [anon_sym_w] = ACTIONS(7),
+    [anon_sym_drop] = ACTIONS(5),
+    [anon_sym_d] = ACTIONS(7),
+    [anon_sym_url] = ACTIONS(5),
+    [anon_sym_u] = ACTIONS(7),
+    [sym_comment] = ACTIONS(9),
+  },
+};
+
+static const uint16_t ts_small_parse_table[] = {
+  [0] = 6,
+  ACTIONS (11),
+  1,
+  ts_builtin_sym_end,
+  ACTIONS (13),
+  1,
+  sym_comment,
+  STATE (26),
+  1,
+  sym_command,
+  STATE (3),
+  2,
+  sym_line,
+  aux_sym_source_file_repeat1,
+  ACTIONS (5),
+  4,
+  anon_sym_pick,
+  anon_sym_watch,
+  anon_sym_drop,
+  anon_sym_url,
+  ACTIONS (7),
+  4,
+  anon_sym_p,
+  anon_sym_w,
+  anon_sym_d,
+  anon_sym_u,
+  [26] = 6,
+  ACTIONS (15),
+  1,
+  ts_builtin_sym_end,
+  ACTIONS (23),
+  1,
+  sym_comment,
+  STATE (26),
+  1,
+  sym_command,
+  STATE (3),
+  2,
+  sym_line,
+  aux_sym_source_file_repeat1,
+  ACTIONS (17),
+  4,
+  anon_sym_pick,
+  anon_sym_watch,
+  anon_sym_drop,
+  anon_sym_url,
+  ACTIONS (20),
+  4,
+  anon_sym_p,
+  anon_sym_w,
+  anon_sym_d,
+  anon_sym_u,
+  [52] = 2,
+  ACTIONS (28),
+  4,
+  anon_sym_p,
+  anon_sym_w,
+  anon_sym_d,
+  anon_sym_u,
+  ACTIONS (26),
+  6,
+  ts_builtin_sym_end,
+  anon_sym_pick,
+  anon_sym_watch,
+  anon_sym_drop,
+  anon_sym_url,
+  sym_comment,
+  [67] = 3,
+  ACTIONS (30),
+  1,
+  sym_quote,
+  STATE (21),
+  1,
+  sym_url,
+  STATE (22),
+  1,
+  sym__q,
+  [77] = 3,
+  ACTIONS (32),
+  1,
+  sym_quote,
+  STATE (7),
+  1,
+  sym_title,
+  STATE (25),
+  1,
+  sym__q,
+  [87] = 3,
+  ACTIONS (34),
+  1,
+  sym_quote,
+  STATE (9),
+  1,
+  sym_date,
+  STATE (23),
+  1,
+  sym__q,
+  [97] = 1,
+  ACTIONS (36),
+  3,
+  anon_sym_NoDuration,
+  aux_sym_duration_token1,
+  aux_sym_duration_token2,
+  [103] = 3,
+  ACTIONS (38),
+  1,
+  sym_quote,
+  STATE (10),
+  1,
+  sym_author,
+  STATE (16),
+  1,
+  sym__q,
+  [113] = 3,
+  ACTIONS (40),
+  1,
+  sym_quote,
+  STATE (5),
+  1,
+  sym_duration,
+  STATE (18),
+  1,
+  sym__q,
+  [123] = 2,
+  ACTIONS (42),
+  1,
+  sym_quote,
+  STATE (30),
+  1,
+  sym__q,
+  [130] = 2,
+  ACTIONS (44),
+  1,
+  sym_quote,
+  STATE (28),
+  1,
+  sym__q,
+  [137] = 2,
+  ACTIONS (46),
+  1,
+  sym_quote,
+  STATE (24),
+  1,
+  sym__q,
+  [144] = 2,
+  ACTIONS (48),
+  1,
+  sym_quote,
+  STATE (17),
+  1,
+  sym__q,
+  [151] = 2,
+  ACTIONS (50),
+  1,
+  sym_quote,
+  STATE (20),
+  1,
+  sym__q,
+  [158] = 1,
+  ACTIONS (52),
+  1,
+  aux_sym_title_token1,
+  [162] = 1,
+  ACTIONS (54),
+  1,
+  sym_quote,
+  [166] = 1,
+  ACTIONS (56),
+  1,
+  anon_sym_LBRACK,
+  [170] = 1,
+  ACTIONS (58),
+  1,
+  sym_id,
+  [174] = 1,
+  ACTIONS (60),
+  1,
+  sym_quote,
+  [178] = 1,
+  ACTIONS (62),
+  1,
+  anon_sym_LF,
+  [182] = 1,
+  ACTIONS (64),
+  1,
+  aux_sym_title_token1,
+  [186] = 1,
+  ACTIONS (66),
+  1,
+  aux_sym_date_token1,
+  [190] = 1,
+  ACTIONS (68),
+  1,
+  sym_quote,
+  [194] = 1,
+  ACTIONS (70),
+  1,
+  aux_sym_title_token1,
+  [198] = 1,
+  ACTIONS (72),
+  1,
+  sym_id,
+  [202] = 1,
+  ACTIONS (74),
+  1,
+  anon_sym_RBRACK,
+  [206] = 1,
+  ACTIONS (76),
+  1,
+  anon_sym_LF,
+  [210] = 1,
+  ACTIONS (78),
+  1,
+  ts_builtin_sym_end,
+  [214] = 1,
+  ACTIONS (80),
+  1,
+  sym_quote,
+};
+
+static const uint32_t ts_small_parse_table_map[] = {
+  [SMALL_STATE (2)] = 0,    [SMALL_STATE (3)] = 26,   [SMALL_STATE (4)] = 52,
+  [SMALL_STATE (5)] = 67,   [SMALL_STATE (6)] = 77,   [SMALL_STATE (7)] = 87,
+  [SMALL_STATE (8)] = 97,   [SMALL_STATE (9)] = 103,  [SMALL_STATE (10)] = 113,
+  [SMALL_STATE (11)] = 123, [SMALL_STATE (12)] = 130, [SMALL_STATE (13)] = 137,
+  [SMALL_STATE (14)] = 144, [SMALL_STATE (15)] = 151, [SMALL_STATE (16)] = 158,
+  [SMALL_STATE (17)] = 162, [SMALL_STATE (18)] = 166, [SMALL_STATE (19)] = 170,
+  [SMALL_STATE (20)] = 174, [SMALL_STATE (21)] = 178, [SMALL_STATE (22)] = 182,
+  [SMALL_STATE (23)] = 186, [SMALL_STATE (24)] = 190, [SMALL_STATE (25)] = 194,
+  [SMALL_STATE (26)] = 198, [SMALL_STATE (27)] = 202, [SMALL_STATE (28)] = 206,
+  [SMALL_STATE (29)] = 210, [SMALL_STATE (30)] = 214,
+};
+
+static const TSParseActionEntry ts_parse_actions[] = {
+  [0] = { .entry = { .count = 0, .reusable = false } },
+  [1] = { .entry = { .count = 1, .reusable = false } },
+  RECOVER (),
+  [3] = { .entry = { .count = 1, .reusable = true } },
+  REDUCE (sym_source_file, 0),
+  [5] = { .entry = { .count = 1, .reusable = true } },
+  SHIFT (19),
+  [7] = { .entry = { .count = 1, .reusable = false } },
+  SHIFT (19),
+  [9] = { .entry = { .count = 1, .reusable = true } },
+  SHIFT (2),
+  [11] = { .entry = { .count = 1, .reusable = true } },
+  REDUCE (sym_source_file, 1),
+  [13] = { .entry = { .count = 1, .reusable = true } },
+  SHIFT (3),
+  [15] = { .entry = { .count = 1, .reusable = true } },
+  REDUCE (aux_sym_source_file_repeat1, 2),
+  [17] = { .entry = { .count = 2, .reusable = true } },
+  REDUCE (aux_sym_source_file_repeat1, 2),
+  SHIFT_REPEAT (19),
+  [20] = { .entry = { .count = 2, .reusable = false } },
+  REDUCE (aux_sym_source_file_repeat1, 2),
+  SHIFT_REPEAT (19),
+  [23] = { .entry = { .count = 2, .reusable = true } },
+  REDUCE (aux_sym_source_file_repeat1, 2),
+  SHIFT_REPEAT (3),
+  [26] = { .entry = { .count = 1, .reusable = true } },
+  REDUCE (sym_line, 8),
+  [28] = { .entry = { .count = 1, .reusable = false } },
+  REDUCE (sym_line, 8),
+  [30] = { .entry = { .count = 1, .reusable = true } },
+  SHIFT (22),
+  [32] = { .entry = { .count = 1, .reusable = true } },
+  SHIFT (25),
+  [34] = { .entry = { .count = 1, .reusable = true } },
+  SHIFT (23),
+  [36] = { .entry = { .count = 1, .reusable = true } },
+  SHIFT (27),
+  [38] = { .entry = { .count = 1, .reusable = true } },
+  SHIFT (16),
+  [40] = { .entry = { .count = 1, .reusable = true } },
+  SHIFT (18),
+  [42] = { .entry = { .count = 1, .reusable = true } },
+  SHIFT (30),
+  [44] = { .entry = { .count = 1, .reusable = true } },
+  SHIFT (28),
+  [46] = { .entry = { .count = 1, .reusable = true } },
+  SHIFT (24),
+  [48] = { .entry = { .count = 1, .reusable = true } },
+  SHIFT (17),
+  [50] = { .entry = { .count = 1, .reusable = true } },
+  SHIFT (20),
+  [52] = { .entry = { .count = 1, .reusable = true } },
+  SHIFT (13),
+  [54] = { .entry = { .count = 1, .reusable = true } },
+  REDUCE (sym_title, 3),
+  [56] = { .entry = { .count = 1, .reusable = true } },
+  SHIFT (8),
+  [58] = { .entry = { .count = 1, .reusable = true } },
+  REDUCE (sym_command, 1),
+  [60] = { .entry = { .count = 1, .reusable = true } },
+  REDUCE (sym_date, 3),
+  [62] = { .entry = { .count = 1, .reusable = true } },
+  SHIFT (4),
+  [64] = { .entry = { .count = 1, .reusable = true } },
+  SHIFT (12),
+  [66] = { .entry = { .count = 1, .reusable = true } },
+  SHIFT (15),
+  [68] = { .entry = { .count = 1, .reusable = true } },
+  REDUCE (sym_author, 3),
+  [70] = { .entry = { .count = 1, .reusable = true } },
+  SHIFT (14),
+  [72] = { .entry = { .count = 1, .reusable = true } },
+  SHIFT (6),
+  [74] = { .entry = { .count = 1, .reusable = true } },
+  SHIFT (11),
+  [76] = { .entry = { .count = 1, .reusable = true } },
+  REDUCE (sym_url, 3),
+  [78] = { .entry = { .count = 1, .reusable = true } },
+  ACCEPT_INPUT (),
+  [80] = { .entry = { .count = 1, .reusable = true } },
+  REDUCE (sym_duration, 5),
+};
+
+#ifdef __cplusplus
+extern "C"
+{
+#endif
+#ifdef _WIN32
+#define extern __declspec (dllexport)
+#endif
+
+  extern const TSLanguage *
+  tree_sitter_yts (void)
+  {
+    static const TSLanguage language = {
+      .version = LANGUAGE_VERSION,
+      .symbol_count = SYMBOL_COUNT,
+      .alias_count = ALIAS_COUNT,
+      .token_count = TOKEN_COUNT,
+      .external_token_count = EXTERNAL_TOKEN_COUNT,
+      .state_count = STATE_COUNT,
+      .large_state_count = LARGE_STATE_COUNT,
+      .production_id_count = PRODUCTION_ID_COUNT,
+      .field_count = FIELD_COUNT,
+      .max_alias_sequence_length = MAX_ALIAS_SEQUENCE_LENGTH,
+      .parse_table = &ts_parse_table[0][0],
+      .small_parse_table = ts_small_parse_table,
+      .small_parse_table_map = ts_small_parse_table_map,
+      .parse_actions = ts_parse_actions,
+      .symbol_names = ts_symbol_names,
+      .symbol_metadata = ts_symbol_metadata,
+      .public_symbol_map = ts_symbol_map,
+      .alias_map = ts_non_terminal_alias_map,
+      .alias_sequences = &ts_alias_sequences[0][0],
+      .lex_modes = ts_lex_modes,
+      .lex_fn = ts_lex,
+      .primary_state_ids = ts_primary_state_ids,
+    };
+    return &language;
+  }
+#ifdef __cplusplus
+}
+#endif
diff --git a/pkgs/sources/tree-sitter-yts/src/tree_sitter/parser.h b/pkgs/sources/tree-sitter-yts/src/tree_sitter/parser.h
new file mode 100644
index 00000000..972913cf
--- /dev/null
+++ b/pkgs/sources/tree-sitter-yts/src/tree_sitter/parser.h
@@ -0,0 +1,241 @@
+#ifndef TREE_SITTER_PARSER_H_
+#define TREE_SITTER_PARSER_H_
+
+#ifdef __cplusplus
+extern "C"
+{
+#endif
+
+#include <stdbool.h>
+#include <stdint.h>
+#include <stdlib.h>
+
+#define ts_builtin_sym_error ((TSSymbol)-1)
+#define ts_builtin_sym_end 0
+#define TREE_SITTER_SERIALIZATION_BUFFER_SIZE 1024
+
+  typedef uint16_t TSStateId;
+
+#ifndef TREE_SITTER_API_H_
+  typedef uint16_t TSSymbol;
+  typedef uint16_t TSFieldId;
+  typedef struct TSLanguage TSLanguage;
+#endif
+
+  typedef struct
+  {
+    TSFieldId field_id;
+    uint8_t child_index;
+    bool inherited;
+  } TSFieldMapEntry;
+
+  typedef struct
+  {
+    uint16_t index;
+    uint16_t length;
+  } TSFieldMapSlice;
+
+  typedef struct
+  {
+    bool visible;
+    bool named;
+    bool supertype;
+  } TSSymbolMetadata;
+
+  typedef struct TSLexer TSLexer;
+
+  struct TSLexer
+  {
+    int32_t lookahead;
+    TSSymbol result_symbol;
+    void (*advance) (TSLexer *, bool);
+    void (*mark_end) (TSLexer *);
+    uint32_t (*get_column) (TSLexer *);
+    bool (*is_at_included_range_start) (const TSLexer *);
+    bool (*eof) (const TSLexer *);
+  };
+
+  typedef enum
+  {
+    TSParseActionTypeShift,
+    TSParseActionTypeReduce,
+    TSParseActionTypeAccept,
+    TSParseActionTypeRecover,
+  } TSParseActionType;
+
+  typedef union
+  {
+    struct
+    {
+      uint8_t type;
+      TSStateId state;
+      bool extra;
+      bool repetition;
+    } shift;
+    struct
+    {
+      uint8_t type;
+      uint8_t child_count;
+      TSSymbol symbol;
+      int16_t dynamic_precedence;
+      uint16_t production_id;
+    } reduce;
+    uint8_t type;
+  } TSParseAction;
+
+  typedef struct
+  {
+    uint16_t lex_state;
+    uint16_t external_lex_state;
+  } TSLexMode;
+
+  typedef union
+  {
+    TSParseAction action;
+    struct
+    {
+      uint8_t count;
+      bool reusable;
+    } entry;
+  } TSParseActionEntry;
+
+  struct TSLanguage
+  {
+    uint32_t version;
+    uint32_t symbol_count;
+    uint32_t alias_count;
+    uint32_t token_count;
+    uint32_t external_token_count;
+    uint32_t state_count;
+    uint32_t large_state_count;
+    uint32_t production_id_count;
+    uint32_t field_count;
+    uint16_t max_alias_sequence_length;
+    const uint16_t *parse_table;
+    const uint16_t *small_parse_table;
+    const uint32_t *small_parse_table_map;
+    const TSParseActionEntry *parse_actions;
+    const char *const *symbol_names;
+    const char *const *field_names;
+    const TSFieldMapSlice *field_map_slices;
+    const TSFieldMapEntry *field_map_entries;
+    const TSSymbolMetadata *symbol_metadata;
+    const TSSymbol *public_symbol_map;
+    const uint16_t *alias_map;
+    const TSSymbol *alias_sequences;
+    const TSLexMode *lex_modes;
+    bool (*lex_fn) (TSLexer *, TSStateId);
+    bool (*keyword_lex_fn) (TSLexer *, TSStateId);
+    TSSymbol keyword_capture_token;
+    struct
+    {
+      const bool *states;
+      const TSSymbol *symbol_map;
+      void *(*create) (void);
+      void (*destroy) (void *);
+      bool (*scan) (void *, TSLexer *, const bool *symbol_whitelist);
+      unsigned (*serialize) (void *, char *);
+      void (*deserialize) (void *, const char *, unsigned);
+    } external_scanner;
+    const TSStateId *primary_state_ids;
+  };
+
+  /*
+   *  Lexer Macros
+   */
+
+#define START_LEXER()                                                         \
+  bool result = false;                                                        \
+  bool skip = false;                                                          \
+  bool eof = false;                                                           \
+  int32_t lookahead;                                                          \
+  goto start;                                                                 \
+  next_state:                                                                 \
+  lexer->advance (lexer, skip);                                               \
+  start:                                                                      \
+  skip = false;                                                               \
+  lookahead = lexer->lookahead;
+
+#define ADVANCE(state_value)                                                  \
+  {                                                                           \
+    state = state_value;                                                      \
+    goto next_state;                                                          \
+  }
+
+#define SKIP(state_value)                                                     \
+  {                                                                           \
+    skip = true;                                                              \
+    state = state_value;                                                      \
+    goto next_state;                                                          \
+  }
+
+#define ACCEPT_TOKEN(symbol_value)                                            \
+  result = true;                                                              \
+  lexer->result_symbol = symbol_value;                                        \
+  lexer->mark_end (lexer);
+
+#define END_STATE() return result;
+
+  /*
+   *  Parse Table Macros
+   */
+
+#define SMALL_STATE(id) id - LARGE_STATE_COUNT
+
+#define STATE(id) id
+
+#define ACTIONS(id) id
+
+#define SHIFT(state_value)                                                    \
+  {                                                                           \
+    {                                                                         \
+      .shift = {.type = TSParseActionTypeShift, .state = state_value }        \
+    }                                                                         \
+  }
+
+#define SHIFT_REPEAT(state_value)                                             \
+  {                                                                           \
+    {                                                                         \
+      .shift                                                                  \
+          = {.type = TSParseActionTypeShift,                                  \
+             .state = state_value,                                            \
+             .repetition = true }                                             \
+    }                                                                         \
+  }
+
+#define SHIFT_EXTRA()                                                         \
+  {                                                                           \
+    {                                                                         \
+      .shift = {.type = TSParseActionTypeShift, .extra = true }               \
+    }                                                                         \
+  }
+
+#define REDUCE(symbol_val, child_count_val, ...)                              \
+  {                                                                           \
+    {                                                                         \
+      .reduce = { .type = TSParseActionTypeReduce,                            \
+                  .symbol = symbol_val,                                       \
+                  .child_count = child_count_val,                             \
+                  __VA_ARGS__ },                                              \
+    }                                                                         \
+  }
+
+#define RECOVER()                                                             \
+  {                                                                           \
+    {                                                                         \
+      .type = TSParseActionTypeRecover                                        \
+    }                                                                         \
+  }
+
+#define ACCEPT_INPUT()                                                        \
+  {                                                                           \
+    {                                                                         \
+      .type = TSParseActionTypeAccept                                         \
+    }                                                                         \
+  }
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif // TREE_SITTER_PARSER_H_
diff --git a/pkgs/sources/tree-sitter-yts/treefmt.toml b/pkgs/sources/tree-sitter-yts/treefmt.toml
new file mode 100644
index 00000000..3d604b40
--- /dev/null
+++ b/pkgs/sources/tree-sitter-yts/treefmt.toml
@@ -0,0 +1,35 @@
+[formatter.nix]
+command = "nixpkgs-fmt"
+includes = ["*.nix"]
+excludes = ["test/**.nix"]
+
+[formatter.prettier]
+command = "prettier"
+options = ["--write"]
+includes = [
+  "*.css",
+  "*.html",
+  "*.js",
+  "*.json",
+  "*.jsx",
+  "*.md",
+  "*.mdx",
+  "*.scss",
+  "*.ts",
+]
+excludes = ["src/**.json"]
+
+[formatter.rust]
+command = "rustfmt"
+options = ["--edition", "2018"]
+includes = ["*.rs"]
+
+[formatter.c]
+command = "clang-format"
+options = ["-i"]
+includes = ["*.c", "*.cpp", "*.cc", "*.h", "*.hpp"]
+excludes = [
+  "bindings/node/binding.cc",
+  "src/parser.c",
+  "src/tree_sitter/parser.h",
+]
diff --git a/pkgs/sources/update_pkgs.sh b/pkgs/sources/update_pkgs.sh
new file mode 100755
index 00000000..be1573c6
--- /dev/null
+++ b/pkgs/sources/update_pkgs.sh
@@ -0,0 +1,25 @@
+#!/usr/bin/env sh
+
+die() {
+    printf "\033[31;1mError: \033[0m%s" "$1"
+    exit 1
+}
+
+cd "$(dirname "$0")" || die "Bug: run with false dirname ('$0')!"
+
+fd . --type directory --max-depth 1 | while read -r dir; do
+    cd "$dir" || die "Dir '$dir' does not exist"
+
+    if [ -f update.sh ]; then
+        printf "\033[34;1m> \033[0m\033[34;1m%s\033[0m\n" "Running '${dir}update.sh'"
+
+        [ -f flake.nix ] && nix flake update
+
+        direnv allow
+        eval "$(direnv export bash 2>/dev/null)"
+        ./update.sh "$@"
+    fi
+    cd - >/dev/null || die "Bug: Last dir does not exist"
+done
+
+# vim: ft=sh
diff --git a/pkgs/sources/update_vim_plugins/.envrc b/pkgs/sources/update_vim_plugins/.envrc
new file mode 100644
index 00000000..3550a30f
--- /dev/null
+++ b/pkgs/sources/update_vim_plugins/.envrc
@@ -0,0 +1 @@
+use flake
diff --git a/pkgs/sources/update_vim_plugins/check-duplicates.sh b/pkgs/sources/update_vim_plugins/check-duplicates.sh
new file mode 100755
index 00000000..781b8aeb
--- /dev/null
+++ b/pkgs/sources/update_vim_plugins/check-duplicates.sh
@@ -0,0 +1,43 @@
+#!/usr/bin/env bash
+
+plugins="$(grep -E "^  [a-zA-Z-]+ =" ./pkgs/vim-plugins.nix | sed -E 's/^  ([a-zA-Z-]+) =.*$/\1/' | sort)"
+count=$(echo "$plugins" | uniq -d | wc -l)
+
+echo "duplicates count: $count"
+
+if [ "$count" -gt 0 ]; then
+    filtered_plugins=$(echo "$plugins" | uniq -d)
+
+    if [ "$1" == "check-only" ]; then
+        echo "$filtered_plugins"
+        exit 1
+    else
+        known_issues=$(gh issue list --state "open" --label "bot" --json "body" | jq -r ".[].body")
+
+        echo "known_issues: $known_issues"
+
+        # iterate over plugins we found missing and
+        # compare them to all open issues.
+        # We no matching issue was found, we create a new one
+        for f in $filtered_plugins; do # do not add " " here. It would break the plugin
+            found=false
+
+            for k in $known_issues; do
+                if [[ $f == "$k" ]]; then
+                    found=true
+                    break
+                fi
+            done
+
+            # test if matching issue was found
+            if ! $found; then
+                echo "Did not find an issue for $f. Creating a new one ..."
+                gh issue create --title "Detected broken plugin: $f" --label "bot" --body "$f"
+            else
+                echo "Issue for $f already exists"
+            fi
+        done
+    fi
+else
+    echo "No duplicates found"
+fi
diff --git a/pkgs/sources/update_vim_plugins/default.nix b/pkgs/sources/update_vim_plugins/default.nix
new file mode 100644
index 00000000..7f0b3f0d
--- /dev/null
+++ b/pkgs/sources/update_vim_plugins/default.nix
@@ -0,0 +1,17 @@
+[
+  (
+    final: prev: {
+      update-vim-plugins = import ./package.nix {
+        inherit
+          (prev)
+          python3
+          # dependencies
+          
+          nix
+          alejandra
+          nix-prefetch-git
+          ;
+      };
+    }
+  )
+]
diff --git a/pkgs/sources/update_vim_plugins/flake.lock b/pkgs/sources/update_vim_plugins/flake.lock
new file mode 100644
index 00000000..50494465
--- /dev/null
+++ b/pkgs/sources/update_vim_plugins/flake.lock
@@ -0,0 +1,61 @@
+{
+  "nodes": {
+    "flake-utils": {
+      "inputs": {
+        "systems": "systems"
+      },
+      "locked": {
+        "lastModified": 1710146030,
+        "narHash": "sha256-SZ5L6eA7HJ/nmkzGG7/ISclqe6oZdOZTNoesiInkXPQ=",
+        "owner": "numtide",
+        "repo": "flake-utils",
+        "rev": "b1d9ab70662946ef0850d488da1c9019f3a9752a",
+        "type": "github"
+      },
+      "original": {
+        "owner": "numtide",
+        "repo": "flake-utils",
+        "type": "github"
+      }
+    },
+    "nixpkgs": {
+      "locked": {
+        "lastModified": 1715087517,
+        "narHash": "sha256-CLU5Tsg24Ke4+7sH8azHWXKd0CFd4mhLWfhYgUiDBpQ=",
+        "owner": "NixOS",
+        "repo": "nixpkgs",
+        "rev": "b211b392b8486ee79df6cdfb1157ad2133427a29",
+        "type": "github"
+      },
+      "original": {
+        "owner": "NixOS",
+        "ref": "nixos-unstable",
+        "repo": "nixpkgs",
+        "type": "github"
+      }
+    },
+    "root": {
+      "inputs": {
+        "flake-utils": "flake-utils",
+        "nixpkgs": "nixpkgs"
+      }
+    },
+    "systems": {
+      "locked": {
+        "lastModified": 1681028828,
+        "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
+        "owner": "nix-systems",
+        "repo": "default",
+        "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
+        "type": "github"
+      },
+      "original": {
+        "owner": "nix-systems",
+        "repo": "default",
+        "type": "github"
+      }
+    }
+  },
+  "root": "root",
+  "version": 7
+}
diff --git a/pkgs/sources/update_vim_plugins/flake.nix b/pkgs/sources/update_vim_plugins/flake.nix
new file mode 100644
index 00000000..ef440af0
--- /dev/null
+++ b/pkgs/sources/update_vim_plugins/flake.nix
@@ -0,0 +1,24 @@
+{
+  description = "update_vim_plugins";
+
+  inputs = {
+    nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
+
+    flake-utils.url = "github:numtide/flake-utils";
+  };
+
+  outputs = {
+    self,
+    nixpkgs,
+    flake-utils,
+  }: (flake-utils.lib.eachDefaultSystem (system: let
+    pkgs = nixpkgs.legacyPackages.${system};
+  in {
+    devShells.default = pkgs.mkShell {
+      packages = [
+        pkgs.python3
+        pkgs.poetry
+      ];
+    };
+  }));
+}
diff --git a/pkgs/sources/update_vim_plugins/package.nix b/pkgs/sources/update_vim_plugins/package.nix
new file mode 100644
index 00000000..e74a29b1
--- /dev/null
+++ b/pkgs/sources/update_vim_plugins/package.nix
@@ -0,0 +1,47 @@
+{
+  python3,
+  # dependencies
+  nix,
+  alejandra,
+  nix-prefetch-git,
+}:
+python3.pkgs.buildPythonApplication {
+  pname = "update-vim-plugins";
+  version = "0.1.0";
+  format = "pyproject";
+
+  src = ./.;
+
+  # NOTE: The test are not really meant to work <2023-12-09>
+  doCheck = false;
+
+  nativeBuildInputs = [
+    python3.pkgs.poetry-core
+  ];
+  buildInputs = [
+    alejandra
+    nix-prefetch-git
+    nix
+  ];
+  propagatedBuildInputs = with python3.pkgs; [
+    requests
+    cleo
+    jsonpickle
+    dateparser
+  ];
+  nativeCheckInputs = with python3.pkgs; [
+    pytestCheckHook
+
+    pytest-cov
+    pytest-mock
+  ];
+  pytestFlagsArray = [
+    "--cov"
+    "update_vim_plugins"
+    "--cov-report"
+    "term-missing:skip-covered"
+    "--cov-fail-under"
+    "50"
+    "update_vim_plugins/tests"
+  ];
+}
diff --git a/pkgs/sources/update_vim_plugins/poetry.lock b/pkgs/sources/update_vim_plugins/poetry.lock
new file mode 100644
index 00000000..f4764b42
--- /dev/null
+++ b/pkgs/sources/update_vim_plugins/poetry.lock
@@ -0,0 +1,680 @@
+# This file is automatically @generated by Poetry 1.8.2 and should not be changed by hand.
+
+[[package]]
+name = "certifi"
+version = "2024.2.2"
+description = "Python package for providing Mozilla's CA Bundle."
+optional = false
+python-versions = ">=3.6"
+files = [
+    {file = "certifi-2024.2.2-py3-none-any.whl", hash = "sha256:dc383c07b76109f368f6106eee2b593b04a011ea4d55f652c6ca24a754d1cdd1"},
+    {file = "certifi-2024.2.2.tar.gz", hash = "sha256:0569859f95fc761b18b45ef421b1290a0f65f147e92a1e5eb3e635f9a5e4e66f"},
+]
+
+[[package]]
+name = "charset-normalizer"
+version = "3.3.2"
+description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet."
+optional = false
+python-versions = ">=3.7.0"
+files = [
+    {file = "charset-normalizer-3.3.2.tar.gz", hash = "sha256:f30c3cb33b24454a82faecaf01b19c18562b1e89558fb6c56de4d9118a032fd5"},
+    {file = "charset_normalizer-3.3.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:25baf083bf6f6b341f4121c2f3c548875ee6f5339300e08be3f2b2ba1721cdd3"},
+    {file = "charset_normalizer-3.3.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:06435b539f889b1f6f4ac1758871aae42dc3a8c0e24ac9e60c2384973ad73027"},
+    {file = "charset_normalizer-3.3.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9063e24fdb1e498ab71cb7419e24622516c4a04476b17a2dab57e8baa30d6e03"},
+    {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6897af51655e3691ff853668779c7bad41579facacf5fd7253b0133308cf000d"},
+    {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1d3193f4a680c64b4b6a9115943538edb896edc190f0b222e73761716519268e"},
+    {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cd70574b12bb8a4d2aaa0094515df2463cb429d8536cfb6c7ce983246983e5a6"},
+    {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8465322196c8b4d7ab6d1e049e4c5cb460d0394da4a27d23cc242fbf0034b6b5"},
+    {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a9a8e9031d613fd2009c182b69c7b2c1ef8239a0efb1df3f7c8da66d5dd3d537"},
+    {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:beb58fe5cdb101e3a055192ac291b7a21e3b7ef4f67fa1d74e331a7f2124341c"},
+    {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:e06ed3eb3218bc64786f7db41917d4e686cc4856944f53d5bdf83a6884432e12"},
+    {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:2e81c7b9c8979ce92ed306c249d46894776a909505d8f5a4ba55b14206e3222f"},
+    {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:572c3763a264ba47b3cf708a44ce965d98555f618ca42c926a9c1616d8f34269"},
+    {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:fd1abc0d89e30cc4e02e4064dc67fcc51bd941eb395c502aac3ec19fab46b519"},
+    {file = "charset_normalizer-3.3.2-cp310-cp310-win32.whl", hash = "sha256:3d47fa203a7bd9c5b6cee4736ee84ca03b8ef23193c0d1ca99b5089f72645c73"},
+    {file = "charset_normalizer-3.3.2-cp310-cp310-win_amd64.whl", hash = "sha256:10955842570876604d404661fbccbc9c7e684caf432c09c715ec38fbae45ae09"},
+    {file = "charset_normalizer-3.3.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:802fe99cca7457642125a8a88a084cef28ff0cf9407060f7b93dca5aa25480db"},
+    {file = "charset_normalizer-3.3.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:573f6eac48f4769d667c4442081b1794f52919e7edada77495aaed9236d13a96"},
+    {file = "charset_normalizer-3.3.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:549a3a73da901d5bc3ce8d24e0600d1fa85524c10287f6004fbab87672bf3e1e"},
+    {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f27273b60488abe721a075bcca6d7f3964f9f6f067c8c4c605743023d7d3944f"},
+    {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1ceae2f17a9c33cb48e3263960dc5fc8005351ee19db217e9b1bb15d28c02574"},
+    {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:65f6f63034100ead094b8744b3b97965785388f308a64cf8d7c34f2f2e5be0c4"},
+    {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:753f10e867343b4511128c6ed8c82f7bec3bd026875576dfd88483c5c73b2fd8"},
+    {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4a78b2b446bd7c934f5dcedc588903fb2f5eec172f3d29e52a9096a43722adfc"},
+    {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:e537484df0d8f426ce2afb2d0f8e1c3d0b114b83f8850e5f2fbea0e797bd82ae"},
+    {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:eb6904c354526e758fda7167b33005998fb68c46fbc10e013ca97f21ca5c8887"},
+    {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:deb6be0ac38ece9ba87dea880e438f25ca3eddfac8b002a2ec3d9183a454e8ae"},
+    {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:4ab2fe47fae9e0f9dee8c04187ce5d09f48eabe611be8259444906793ab7cbce"},
+    {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:80402cd6ee291dcb72644d6eac93785fe2c8b9cb30893c1af5b8fdd753b9d40f"},
+    {file = "charset_normalizer-3.3.2-cp311-cp311-win32.whl", hash = "sha256:7cd13a2e3ddeed6913a65e66e94b51d80a041145a026c27e6bb76c31a853c6ab"},
+    {file = "charset_normalizer-3.3.2-cp311-cp311-win_amd64.whl", hash = "sha256:663946639d296df6a2bb2aa51b60a2454ca1cb29835324c640dafb5ff2131a77"},
+    {file = "charset_normalizer-3.3.2-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:0b2b64d2bb6d3fb9112bafa732def486049e63de9618b5843bcdd081d8144cd8"},
+    {file = "charset_normalizer-3.3.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:ddbb2551d7e0102e7252db79ba445cdab71b26640817ab1e3e3648dad515003b"},
+    {file = "charset_normalizer-3.3.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:55086ee1064215781fff39a1af09518bc9255b50d6333f2e4c74ca09fac6a8f6"},
+    {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8f4a014bc36d3c57402e2977dada34f9c12300af536839dc38c0beab8878f38a"},
+    {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a10af20b82360ab00827f916a6058451b723b4e65030c5a18577c8b2de5b3389"},
+    {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8d756e44e94489e49571086ef83b2bb8ce311e730092d2c34ca8f7d925cb20aa"},
+    {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:90d558489962fd4918143277a773316e56c72da56ec7aa3dc3dbbe20fdfed15b"},
+    {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6ac7ffc7ad6d040517be39eb591cac5ff87416c2537df6ba3cba3bae290c0fed"},
+    {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:7ed9e526742851e8d5cc9e6cf41427dfc6068d4f5a3bb03659444b4cabf6bc26"},
+    {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:8bdb58ff7ba23002a4c5808d608e4e6c687175724f54a5dade5fa8c67b604e4d"},
+    {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:6b3251890fff30ee142c44144871185dbe13b11bab478a88887a639655be1068"},
+    {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:b4a23f61ce87adf89be746c8a8974fe1c823c891d8f86eb218bb957c924bb143"},
+    {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:efcb3f6676480691518c177e3b465bcddf57cea040302f9f4e6e191af91174d4"},
+    {file = "charset_normalizer-3.3.2-cp312-cp312-win32.whl", hash = "sha256:d965bba47ddeec8cd560687584e88cf699fd28f192ceb452d1d7ee807c5597b7"},
+    {file = "charset_normalizer-3.3.2-cp312-cp312-win_amd64.whl", hash = "sha256:96b02a3dc4381e5494fad39be677abcb5e6634bf7b4fa83a6dd3112607547001"},
+    {file = "charset_normalizer-3.3.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:95f2a5796329323b8f0512e09dbb7a1860c46a39da62ecb2324f116fa8fdc85c"},
+    {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c002b4ffc0be611f0d9da932eb0f704fe2602a9a949d1f738e4c34c75b0863d5"},
+    {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a981a536974bbc7a512cf44ed14938cf01030a99e9b3a06dd59578882f06f985"},
+    {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3287761bc4ee9e33561a7e058c72ac0938c4f57fe49a09eae428fd88aafe7bb6"},
+    {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:42cb296636fcc8b0644486d15c12376cb9fa75443e00fb25de0b8602e64c1714"},
+    {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0a55554a2fa0d408816b3b5cedf0045f4b8e1a6065aec45849de2d6f3f8e9786"},
+    {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:c083af607d2515612056a31f0a8d9e0fcb5876b7bfc0abad3ecd275bc4ebc2d5"},
+    {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:87d1351268731db79e0f8e745d92493ee2841c974128ef629dc518b937d9194c"},
+    {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:bd8f7df7d12c2db9fab40bdd87a7c09b1530128315d047a086fa3ae3435cb3a8"},
+    {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:c180f51afb394e165eafe4ac2936a14bee3eb10debc9d9e4db8958fe36afe711"},
+    {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:8c622a5fe39a48f78944a87d4fb8a53ee07344641b0562c540d840748571b811"},
+    {file = "charset_normalizer-3.3.2-cp37-cp37m-win32.whl", hash = "sha256:db364eca23f876da6f9e16c9da0df51aa4f104a972735574842618b8c6d999d4"},
+    {file = "charset_normalizer-3.3.2-cp37-cp37m-win_amd64.whl", hash = "sha256:86216b5cee4b06df986d214f664305142d9c76df9b6512be2738aa72a2048f99"},
+    {file = "charset_normalizer-3.3.2-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:6463effa3186ea09411d50efc7d85360b38d5f09b870c48e4600f63af490e56a"},
+    {file = "charset_normalizer-3.3.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:6c4caeef8fa63d06bd437cd4bdcf3ffefe6738fb1b25951440d80dc7df8c03ac"},
+    {file = "charset_normalizer-3.3.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:37e55c8e51c236f95b033f6fb391d7d7970ba5fe7ff453dad675e88cf303377a"},
+    {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fb69256e180cb6c8a894fee62b3afebae785babc1ee98b81cdf68bbca1987f33"},
+    {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ae5f4161f18c61806f411a13b0310bea87f987c7d2ecdbdaad0e94eb2e404238"},
+    {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b2b0a0c0517616b6869869f8c581d4eb2dd83a4d79e0ebcb7d373ef9956aeb0a"},
+    {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:45485e01ff4d3630ec0d9617310448a8702f70e9c01906b0d0118bdf9d124cf2"},
+    {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:eb00ed941194665c332bf8e078baf037d6c35d7c4f3102ea2d4f16ca94a26dc8"},
+    {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:2127566c664442652f024c837091890cb1942c30937add288223dc895793f898"},
+    {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:a50aebfa173e157099939b17f18600f72f84eed3049e743b68ad15bd69b6bf99"},
+    {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:4d0d1650369165a14e14e1e47b372cfcb31d6ab44e6e33cb2d4e57265290044d"},
+    {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:923c0c831b7cfcb071580d3f46c4baf50f174be571576556269530f4bbd79d04"},
+    {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:06a81e93cd441c56a9b65d8e1d043daeb97a3d0856d177d5c90ba85acb3db087"},
+    {file = "charset_normalizer-3.3.2-cp38-cp38-win32.whl", hash = "sha256:6ef1d82a3af9d3eecdba2321dc1b3c238245d890843e040e41e470ffa64c3e25"},
+    {file = "charset_normalizer-3.3.2-cp38-cp38-win_amd64.whl", hash = "sha256:eb8821e09e916165e160797a6c17edda0679379a4be5c716c260e836e122f54b"},
+    {file = "charset_normalizer-3.3.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:c235ebd9baae02f1b77bcea61bce332cb4331dc3617d254df3323aa01ab47bd4"},
+    {file = "charset_normalizer-3.3.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:5b4c145409bef602a690e7cfad0a15a55c13320ff7a3ad7ca59c13bb8ba4d45d"},
+    {file = "charset_normalizer-3.3.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:68d1f8a9e9e37c1223b656399be5d6b448dea850bed7d0f87a8311f1ff3dabb0"},
+    {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:22afcb9f253dac0696b5a4be4a1c0f8762f8239e21b99680099abd9b2b1b2269"},
+    {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e27ad930a842b4c5eb8ac0016b0a54f5aebbe679340c26101df33424142c143c"},
+    {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1f79682fbe303db92bc2b1136016a38a42e835d932bab5b3b1bfcfbf0640e519"},
+    {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b261ccdec7821281dade748d088bb6e9b69e6d15b30652b74cbbac25e280b796"},
+    {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:122c7fa62b130ed55f8f285bfd56d5f4b4a5b503609d181f9ad85e55c89f4185"},
+    {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:d0eccceffcb53201b5bfebb52600a5fb483a20b61da9dbc885f8b103cbe7598c"},
+    {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:9f96df6923e21816da7e0ad3fd47dd8f94b2a5ce594e00677c0013018b813458"},
+    {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:7f04c839ed0b6b98b1a7501a002144b76c18fb1c1850c8b98d458ac269e26ed2"},
+    {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:34d1c8da1e78d2e001f363791c98a272bb734000fcef47a491c1e3b0505657a8"},
+    {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:ff8fa367d09b717b2a17a052544193ad76cd49979c805768879cb63d9ca50561"},
+    {file = "charset_normalizer-3.3.2-cp39-cp39-win32.whl", hash = "sha256:aed38f6e4fb3f5d6bf81bfa990a07806be9d83cf7bacef998ab1a9bd660a581f"},
+    {file = "charset_normalizer-3.3.2-cp39-cp39-win_amd64.whl", hash = "sha256:b01b88d45a6fcb69667cd6d2f7a9aeb4bf53760d7fc536bf679ec94fe9f3ff3d"},
+    {file = "charset_normalizer-3.3.2-py3-none-any.whl", hash = "sha256:3e4d1f6587322d2788836a99c69062fbb091331ec940e02d12d179c1d53e25fc"},
+]
+
+[[package]]
+name = "cleo"
+version = "2.1.0"
+description = "Cleo allows you to create beautiful and testable command-line interfaces."
+optional = false
+python-versions = ">=3.7,<4.0"
+files = [
+    {file = "cleo-2.1.0-py3-none-any.whl", hash = "sha256:4a31bd4dd45695a64ee3c4758f583f134267c2bc518d8ae9a29cf237d009b07e"},
+    {file = "cleo-2.1.0.tar.gz", hash = "sha256:0b2c880b5d13660a7ea651001fb4acb527696c01f15c9ee650f377aa543fd523"},
+]
+
+[package.dependencies]
+crashtest = ">=0.4.1,<0.5.0"
+rapidfuzz = ">=3.0.0,<4.0.0"
+
+[[package]]
+name = "colorama"
+version = "0.4.6"
+description = "Cross-platform colored terminal text."
+optional = false
+python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7"
+files = [
+    {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"},
+    {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"},
+]
+
+[[package]]
+name = "coverage"
+version = "7.5.1"
+description = "Code coverage measurement for Python"
+optional = false
+python-versions = ">=3.8"
+files = [
+    {file = "coverage-7.5.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:c0884920835a033b78d1c73b6d3bbcda8161a900f38a488829a83982925f6c2e"},
+    {file = "coverage-7.5.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:39afcd3d4339329c5f58de48a52f6e4e50f6578dd6099961cf22228feb25f38f"},
+    {file = "coverage-7.5.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4a7b0ceee8147444347da6a66be737c9d78f3353b0681715b668b72e79203e4a"},
+    {file = "coverage-7.5.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4a9ca3f2fae0088c3c71d743d85404cec8df9be818a005ea065495bedc33da35"},
+    {file = "coverage-7.5.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5fd215c0c7d7aab005221608a3c2b46f58c0285a819565887ee0b718c052aa4e"},
+    {file = "coverage-7.5.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:4bf0655ab60d754491004a5efd7f9cccefcc1081a74c9ef2da4735d6ee4a6223"},
+    {file = "coverage-7.5.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:61c4bf1ba021817de12b813338c9be9f0ad5b1e781b9b340a6d29fc13e7c1b5e"},
+    {file = "coverage-7.5.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:db66fc317a046556a96b453a58eced5024af4582a8dbdc0c23ca4dbc0d5b3146"},
+    {file = "coverage-7.5.1-cp310-cp310-win32.whl", hash = "sha256:b016ea6b959d3b9556cb401c55a37547135a587db0115635a443b2ce8f1c7228"},
+    {file = "coverage-7.5.1-cp310-cp310-win_amd64.whl", hash = "sha256:df4e745a81c110e7446b1cc8131bf986157770fa405fe90e15e850aaf7619bc8"},
+    {file = "coverage-7.5.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:796a79f63eca8814ca3317a1ea443645c9ff0d18b188de470ed7ccd45ae79428"},
+    {file = "coverage-7.5.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4fc84a37bfd98db31beae3c2748811a3fa72bf2007ff7902f68746d9757f3746"},
+    {file = "coverage-7.5.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6175d1a0559986c6ee3f7fccfc4a90ecd12ba0a383dcc2da30c2b9918d67d8a3"},
+    {file = "coverage-7.5.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1fc81d5878cd6274ce971e0a3a18a8803c3fe25457165314271cf78e3aae3aa2"},
+    {file = "coverage-7.5.1-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:556cf1a7cbc8028cb60e1ff0be806be2eded2daf8129b8811c63e2b9a6c43bca"},
+    {file = "coverage-7.5.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:9981706d300c18d8b220995ad22627647be11a4276721c10911e0e9fa44c83e8"},
+    {file = "coverage-7.5.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:d7fed867ee50edf1a0b4a11e8e5d0895150e572af1cd6d315d557758bfa9c057"},
+    {file = "coverage-7.5.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:ef48e2707fb320c8f139424a596f5b69955a85b178f15af261bab871873bb987"},
+    {file = "coverage-7.5.1-cp311-cp311-win32.whl", hash = "sha256:9314d5678dcc665330df5b69c1e726a0e49b27df0461c08ca12674bcc19ef136"},
+    {file = "coverage-7.5.1-cp311-cp311-win_amd64.whl", hash = "sha256:5fa567e99765fe98f4e7d7394ce623e794d7cabb170f2ca2ac5a4174437e90dd"},
+    {file = "coverage-7.5.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:b6cf3764c030e5338e7f61f95bd21147963cf6aa16e09d2f74f1fa52013c1206"},
+    {file = "coverage-7.5.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2ec92012fefebee89a6b9c79bc39051a6cb3891d562b9270ab10ecfdadbc0c34"},
+    {file = "coverage-7.5.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:16db7f26000a07efcf6aea00316f6ac57e7d9a96501e990a36f40c965ec7a95d"},
+    {file = "coverage-7.5.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:beccf7b8a10b09c4ae543582c1319c6df47d78fd732f854ac68d518ee1fb97fa"},
+    {file = "coverage-7.5.1-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8748731ad392d736cc9ccac03c9845b13bb07d020a33423fa5b3a36521ac6e4e"},
+    {file = "coverage-7.5.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:7352b9161b33fd0b643ccd1f21f3a3908daaddf414f1c6cb9d3a2fd618bf2572"},
+    {file = "coverage-7.5.1-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:7a588d39e0925f6a2bff87154752481273cdb1736270642aeb3635cb9b4cad07"},
+    {file = "coverage-7.5.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:68f962d9b72ce69ea8621f57551b2fa9c70509af757ee3b8105d4f51b92b41a7"},
+    {file = "coverage-7.5.1-cp312-cp312-win32.whl", hash = "sha256:f152cbf5b88aaeb836127d920dd0f5e7edff5a66f10c079157306c4343d86c19"},
+    {file = "coverage-7.5.1-cp312-cp312-win_amd64.whl", hash = "sha256:5a5740d1fb60ddf268a3811bcd353de34eb56dc24e8f52a7f05ee513b2d4f596"},
+    {file = "coverage-7.5.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:e2213def81a50519d7cc56ed643c9e93e0247f5bbe0d1247d15fa520814a7cd7"},
+    {file = "coverage-7.5.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:5037f8fcc2a95b1f0e80585bd9d1ec31068a9bcb157d9750a172836e98bc7a90"},
+    {file = "coverage-7.5.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5c3721c2c9e4c4953a41a26c14f4cef64330392a6d2d675c8b1db3b645e31f0e"},
+    {file = "coverage-7.5.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ca498687ca46a62ae590253fba634a1fe9836bc56f626852fb2720f334c9e4e5"},
+    {file = "coverage-7.5.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0cdcbc320b14c3e5877ee79e649677cb7d89ef588852e9583e6b24c2e5072661"},
+    {file = "coverage-7.5.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:57e0204b5b745594e5bc14b9b50006da722827f0b8c776949f1135677e88d0b8"},
+    {file = "coverage-7.5.1-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:8fe7502616b67b234482c3ce276ff26f39ffe88adca2acf0261df4b8454668b4"},
+    {file = "coverage-7.5.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:9e78295f4144f9dacfed4f92935fbe1780021247c2fabf73a819b17f0ccfff8d"},
+    {file = "coverage-7.5.1-cp38-cp38-win32.whl", hash = "sha256:1434e088b41594baa71188a17533083eabf5609e8e72f16ce8c186001e6b8c41"},
+    {file = "coverage-7.5.1-cp38-cp38-win_amd64.whl", hash = "sha256:0646599e9b139988b63704d704af8e8df7fa4cbc4a1f33df69d97f36cb0a38de"},
+    {file = "coverage-7.5.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:4cc37def103a2725bc672f84bd939a6fe4522310503207aae4d56351644682f1"},
+    {file = "coverage-7.5.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:fc0b4d8bfeabd25ea75e94632f5b6e047eef8adaed0c2161ada1e922e7f7cece"},
+    {file = "coverage-7.5.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0d0a0f5e06881ecedfe6f3dd2f56dcb057b6dbeb3327fd32d4b12854df36bf26"},
+    {file = "coverage-7.5.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9735317685ba6ec7e3754798c8871c2f49aa5e687cc794a0b1d284b2389d1bd5"},
+    {file = "coverage-7.5.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d21918e9ef11edf36764b93101e2ae8cc82aa5efdc7c5a4e9c6c35a48496d601"},
+    {file = "coverage-7.5.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:c3e757949f268364b96ca894b4c342b41dc6f8f8b66c37878aacef5930db61be"},
+    {file = "coverage-7.5.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:79afb6197e2f7f60c4824dd4b2d4c2ec5801ceb6ba9ce5d2c3080e5660d51a4f"},
+    {file = "coverage-7.5.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:d1d0d98d95dd18fe29dc66808e1accf59f037d5716f86a501fc0256455219668"},
+    {file = "coverage-7.5.1-cp39-cp39-win32.whl", hash = "sha256:1cc0fe9b0b3a8364093c53b0b4c0c2dd4bb23acbec4c9240b5f284095ccf7981"},
+    {file = "coverage-7.5.1-cp39-cp39-win_amd64.whl", hash = "sha256:dde0070c40ea8bb3641e811c1cfbf18e265d024deff6de52c5950677a8fb1e0f"},
+    {file = "coverage-7.5.1-pp38.pp39.pp310-none-any.whl", hash = "sha256:6537e7c10cc47c595828b8a8be04c72144725c383c4702703ff4e42e44577312"},
+    {file = "coverage-7.5.1.tar.gz", hash = "sha256:54de9ef3a9da981f7af93eafde4ede199e0846cd819eb27c88e2b712aae9708c"},
+]
+
+[package.dependencies]
+tomli = {version = "*", optional = true, markers = "python_full_version <= \"3.11.0a6\" and extra == \"toml\""}
+
+[package.extras]
+toml = ["tomli"]
+
+[[package]]
+name = "crashtest"
+version = "0.4.1"
+description = "Manage Python errors with ease"
+optional = false
+python-versions = ">=3.7,<4.0"
+files = [
+    {file = "crashtest-0.4.1-py3-none-any.whl", hash = "sha256:8d23eac5fa660409f57472e3851dab7ac18aba459a8d19cbbba86d3d5aecd2a5"},
+    {file = "crashtest-0.4.1.tar.gz", hash = "sha256:80d7b1f316ebfbd429f648076d6275c877ba30ba48979de4191714a75266f0ce"},
+]
+
+[[package]]
+name = "dateparser"
+version = "1.2.0"
+description = "Date parsing library designed to parse dates from HTML pages"
+optional = false
+python-versions = ">=3.7"
+files = [
+    {file = "dateparser-1.2.0-py2.py3-none-any.whl", hash = "sha256:0b21ad96534e562920a0083e97fd45fa959882d4162acc358705144520a35830"},
+    {file = "dateparser-1.2.0.tar.gz", hash = "sha256:7975b43a4222283e0ae15be7b4999d08c9a70e2d378ac87385b1ccf2cffbbb30"},
+]
+
+[package.dependencies]
+python-dateutil = "*"
+pytz = "*"
+regex = "<2019.02.19 || >2019.02.19,<2021.8.27 || >2021.8.27"
+tzlocal = "*"
+
+[package.extras]
+calendars = ["convertdate", "hijri-converter"]
+fasttext = ["fasttext"]
+langdetect = ["langdetect"]
+
+[[package]]
+name = "exceptiongroup"
+version = "1.2.1"
+description = "Backport of PEP 654 (exception groups)"
+optional = false
+python-versions = ">=3.7"
+files = [
+    {file = "exceptiongroup-1.2.1-py3-none-any.whl", hash = "sha256:5258b9ed329c5bbdd31a309f53cbfb0b155341807f6ff7606a1e801a891b29ad"},
+    {file = "exceptiongroup-1.2.1.tar.gz", hash = "sha256:a4785e48b045528f5bfe627b6ad554ff32def154f42372786903b7abcfe1aa16"},
+]
+
+[package.extras]
+test = ["pytest (>=6)"]
+
+[[package]]
+name = "idna"
+version = "3.7"
+description = "Internationalized Domain Names in Applications (IDNA)"
+optional = false
+python-versions = ">=3.5"
+files = [
+    {file = "idna-3.7-py3-none-any.whl", hash = "sha256:82fee1fc78add43492d3a1898bfa6d8a904cc97d8427f683ed8e798d07761aa0"},
+    {file = "idna-3.7.tar.gz", hash = "sha256:028ff3aadf0609c1fd278d8ea3089299412a7a8b9bd005dd08b9f8285bcb5cfc"},
+]
+
+[[package]]
+name = "iniconfig"
+version = "2.0.0"
+description = "brain-dead simple config-ini parsing"
+optional = false
+python-versions = ">=3.7"
+files = [
+    {file = "iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374"},
+    {file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"},
+]
+
+[[package]]
+name = "jsonpickle"
+version = "3.0.4"
+description = "Serialize any Python object to JSON"
+optional = false
+python-versions = ">=3.7"
+files = [
+    {file = "jsonpickle-3.0.4-py3-none-any.whl", hash = "sha256:04ae7567a14269579e3af66b76bda284587458d7e8a204951ca8f71a3309952e"},
+    {file = "jsonpickle-3.0.4.tar.gz", hash = "sha256:a1b14c8d6221cd8f394f2a97e735ea1d7edc927fbd135b26f2f8700657c8c62b"},
+]
+
+[package.extras]
+docs = ["furo", "rst.linker (>=1.9)", "sphinx"]
+packaging = ["build", "twine"]
+testing = ["bson", "ecdsa", "feedparser", "gmpy2", "numpy", "pandas", "pymongo", "pytest (>=3.5,!=3.7.3)", "pytest-benchmark", "pytest-benchmark[histogram]", "pytest-checkdocs (>=1.2.3)", "pytest-cov", "pytest-enabler (>=1.0.1)", "pytest-ruff (>=0.2.1)", "scikit-learn", "scipy", "scipy (>=1.9.3)", "simplejson", "sqlalchemy", "ujson"]
+
+[[package]]
+name = "packaging"
+version = "24.0"
+description = "Core utilities for Python packages"
+optional = false
+python-versions = ">=3.7"
+files = [
+    {file = "packaging-24.0-py3-none-any.whl", hash = "sha256:2ddfb553fdf02fb784c234c7ba6ccc288296ceabec964ad2eae3777778130bc5"},
+    {file = "packaging-24.0.tar.gz", hash = "sha256:eb82c5e3e56209074766e6885bb04b8c38a0c015d0a30036ebe7ece34c9989e9"},
+]
+
+[[package]]
+name = "pluggy"
+version = "1.5.0"
+description = "plugin and hook calling mechanisms for python"
+optional = false
+python-versions = ">=3.8"
+files = [
+    {file = "pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669"},
+    {file = "pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1"},
+]
+
+[package.extras]
+dev = ["pre-commit", "tox"]
+testing = ["pytest", "pytest-benchmark"]
+
+[[package]]
+name = "pytest"
+version = "7.4.4"
+description = "pytest: simple powerful testing with Python"
+optional = false
+python-versions = ">=3.7"
+files = [
+    {file = "pytest-7.4.4-py3-none-any.whl", hash = "sha256:b090cdf5ed60bf4c45261be03239c2c1c22df034fbffe691abe93cd80cea01d8"},
+    {file = "pytest-7.4.4.tar.gz", hash = "sha256:2cf0005922c6ace4a3e2ec8b4080eb0d9753fdc93107415332f50ce9e7994280"},
+]
+
+[package.dependencies]
+colorama = {version = "*", markers = "sys_platform == \"win32\""}
+exceptiongroup = {version = ">=1.0.0rc8", markers = "python_version < \"3.11\""}
+iniconfig = "*"
+packaging = "*"
+pluggy = ">=0.12,<2.0"
+tomli = {version = ">=1.0.0", markers = "python_version < \"3.11\""}
+
+[package.extras]
+testing = ["argcomplete", "attrs (>=19.2.0)", "hypothesis (>=3.56)", "mock", "nose", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"]
+
+[[package]]
+name = "pytest-cov"
+version = "4.1.0"
+description = "Pytest plugin for measuring coverage."
+optional = false
+python-versions = ">=3.7"
+files = [
+    {file = "pytest-cov-4.1.0.tar.gz", hash = "sha256:3904b13dfbfec47f003b8e77fd5b589cd11904a21ddf1ab38a64f204d6a10ef6"},
+    {file = "pytest_cov-4.1.0-py3-none-any.whl", hash = "sha256:6ba70b9e97e69fcc3fb45bfeab2d0a138fb65c4d0d6a41ef33983ad114be8c3a"},
+]
+
+[package.dependencies]
+coverage = {version = ">=5.2.1", extras = ["toml"]}
+pytest = ">=4.6"
+
+[package.extras]
+testing = ["fields", "hunter", "process-tests", "pytest-xdist", "six", "virtualenv"]
+
+[[package]]
+name = "pytest-mock"
+version = "3.14.0"
+description = "Thin-wrapper around the mock package for easier use with pytest"
+optional = false
+python-versions = ">=3.8"
+files = [
+    {file = "pytest-mock-3.14.0.tar.gz", hash = "sha256:2719255a1efeceadbc056d6bf3df3d1c5015530fb40cf347c0f9afac88410bd0"},
+    {file = "pytest_mock-3.14.0-py3-none-any.whl", hash = "sha256:0b72c38033392a5f4621342fe11e9219ac11ec9d375f8e2a0c164539e0d70f6f"},
+]
+
+[package.dependencies]
+pytest = ">=6.2.5"
+
+[package.extras]
+dev = ["pre-commit", "pytest-asyncio", "tox"]
+
+[[package]]
+name = "python-dateutil"
+version = "2.9.0.post0"
+description = "Extensions to the standard Python datetime module"
+optional = false
+python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7"
+files = [
+    {file = "python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3"},
+    {file = "python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427"},
+]
+
+[package.dependencies]
+six = ">=1.5"
+
+[[package]]
+name = "pytz"
+version = "2024.1"
+description = "World timezone definitions, modern and historical"
+optional = false
+python-versions = "*"
+files = [
+    {file = "pytz-2024.1-py2.py3-none-any.whl", hash = "sha256:328171f4e3623139da4983451950b28e95ac706e13f3f2630a879749e7a8b319"},
+    {file = "pytz-2024.1.tar.gz", hash = "sha256:2a29735ea9c18baf14b448846bde5a48030ed267578472d8955cd0e7443a9812"},
+]
+
+[[package]]
+name = "rapidfuzz"
+version = "3.9.0"
+description = "rapid fuzzy string matching"
+optional = false
+python-versions = ">=3.8"
+files = [
+    {file = "rapidfuzz-3.9.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:bd375c4830fee11d502dd93ecadef63c137ae88e1aaa29cc15031fa66d1e0abb"},
+    {file = "rapidfuzz-3.9.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:55e2c5076f38fc1dbaacb95fa026a3e409eee6ea5ac4016d44fb30e4cad42b20"},
+    {file = "rapidfuzz-3.9.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:488f74126904db6b1bea545c2f3567ea882099f4c13f46012fe8f4b990c683df"},
+    {file = "rapidfuzz-3.9.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e3f2d1ea7cd57dfcd34821e38b4924c80a31bcf8067201b1ab07386996a9faee"},
+    {file = "rapidfuzz-3.9.0-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b11e602987bcb4ea22b44178851f27406fca59b0836298d0beb009b504dba266"},
+    {file = "rapidfuzz-3.9.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3083512e9bf6ed2bb3d25883922974f55e21ae7f8e9f4e298634691ae1aee583"},
+    {file = "rapidfuzz-3.9.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b33c6d4b3a1190bc0b6c158c3981535f9434e8ed9ffa40cf5586d66c1819fb4b"},
+    {file = "rapidfuzz-3.9.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0dcb95fde22f98e6d0480db8d6038c45fe2d18a338690e6f9bba9b82323f3469"},
+    {file = "rapidfuzz-3.9.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:08d8b49b3a4fb8572e480e73fcddc750da9cbb8696752ee12cca4bf8c8220d52"},
+    {file = "rapidfuzz-3.9.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:e721842e6b601ebbeb8cc5e12c75bbdd1d9e9561ea932f2f844c418c31256e82"},
+    {file = "rapidfuzz-3.9.0-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:7988363b3a415c5194ce1a68d380629247f8713e669ad81db7548eb156c4f365"},
+    {file = "rapidfuzz-3.9.0-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:2d267d4c982ab7d177e994ab1f31b98ff3814f6791b90d35dda38307b9e7c989"},
+    {file = "rapidfuzz-3.9.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:0bb28ab5300cf974c7eb68ea21125c493e74b35b1129e629533468b2064ae0a2"},
+    {file = "rapidfuzz-3.9.0-cp310-cp310-win32.whl", hash = "sha256:1b1f74997b6d94d66375479fa55f70b1c18e4d865d7afcd13f0785bfd40a9d3c"},
+    {file = "rapidfuzz-3.9.0-cp310-cp310-win_amd64.whl", hash = "sha256:c56d2efdfaa1c642029f3a7a5bb76085c5531f7a530777be98232d2ce142553c"},
+    {file = "rapidfuzz-3.9.0-cp310-cp310-win_arm64.whl", hash = "sha256:6a83128d505cac76ea560bb9afcb3f6986e14e50a6f467db9a31faef4bd9b347"},
+    {file = "rapidfuzz-3.9.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:e2218d62ab63f3c5ad48eced898854d0c2c327a48f0fb02e2288d7e5332a22c8"},
+    {file = "rapidfuzz-3.9.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:36bf35df2d6c7d5820da20a6720aee34f67c15cd2daf8cf92e8141995c640c25"},
+    {file = "rapidfuzz-3.9.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:905b01a9b633394ff6bb5ebb1c5fd660e0e180c03fcf9d90199cc6ed74b87cf7"},
+    {file = "rapidfuzz-3.9.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:33cfabcb7fd994938a6a08e641613ce5fe46757832edc789c6a5602e7933d6fa"},
+    {file = "rapidfuzz-3.9.0-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1179dcd3d150a67b8a678cd9c84f3baff7413ff13c9e8fe85e52a16c97e24c9b"},
+    {file = "rapidfuzz-3.9.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:47d97e28c42f1efb7781993b67c749223f198f6653ef177a0c8f2b1c516efcaf"},
+    {file = "rapidfuzz-3.9.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:28da953eb2ef9ad527e536022da7afff6ace7126cdd6f3e21ac20f8762e76d2c"},
+    {file = "rapidfuzz-3.9.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:182b4e11de928fb4834e8f8b5ecd971b5b10a86fabe8636ab65d3a9b7e0e9ca7"},
+    {file = "rapidfuzz-3.9.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:c74f2da334ce597f31670db574766ddeaee5d9430c2c00e28d0fbb7f76172036"},
+    {file = "rapidfuzz-3.9.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:014ac55b03f4074f903248ded181f3000f4cdbd134e6155cbf643f0eceb4f70f"},
+    {file = "rapidfuzz-3.9.0-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:c4ef34b2ddbf448f1d644b4ec6475df8bbe5b9d0fee173ff2e87322a151663bd"},
+    {file = "rapidfuzz-3.9.0-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:fc02157f521af15143fae88f92ef3ddcc4e0cff05c40153a9549dc0fbdb9adb3"},
+    {file = "rapidfuzz-3.9.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:ff08081c49b18ba253a99e6a47f492e6ee8019e19bbb6ddc3ed360cd3ecb2f62"},
+    {file = "rapidfuzz-3.9.0-cp311-cp311-win32.whl", hash = "sha256:b9bf90b3d96925cbf8ef44e5ee3cf39ef0c422f12d40f7a497e91febec546650"},
+    {file = "rapidfuzz-3.9.0-cp311-cp311-win_amd64.whl", hash = "sha256:d5d5684f54d82d9b0cf0b2701e55a630527a9c3dd5ddcf7a2e726a475ac238f2"},
+    {file = "rapidfuzz-3.9.0-cp311-cp311-win_arm64.whl", hash = "sha256:a2de844e0e971d7bd8aa41284627dbeacc90e750b90acfb016836553c7a63192"},
+    {file = "rapidfuzz-3.9.0-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:f81fe99a69ac8ee3fd905e70c62f3af033901aeb60b69317d1d43d547b46e510"},
+    {file = "rapidfuzz-3.9.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:633b9d03fc04abc585c197104b1d0af04b1f1db1abc99f674d871224cd15557a"},
+    {file = "rapidfuzz-3.9.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ab872cb57ae97c54ba7c71a9e3c9552beb57cb907c789b726895576d1ea9af6f"},
+    {file = "rapidfuzz-3.9.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bdd8c15c3a14e409507fdf0c0434ec481d85c6cbeec8bdcd342a8cd1eda03825"},
+    {file = "rapidfuzz-3.9.0-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2444d8155d9846f206e2079bb355b85f365d9457480b0d71677a112d0a7f7128"},
+    {file = "rapidfuzz-3.9.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f83bd3d01f04061c3660742dc85143a89d49fd23eb31eccbf60ad56c4b955617"},
+    {file = "rapidfuzz-3.9.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0ca799f882364e69d0872619afb19efa3652b7133c18352e4a3d86a324fb2bb1"},
+    {file = "rapidfuzz-3.9.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6993d361f28b9ef5f0fa4e79b8541c2f3507be7471b9f9cb403a255e123b31e1"},
+    {file = "rapidfuzz-3.9.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:170822a1b1719f02b58e3dce194c8ad7d4c5b39be38c0fdec603bd19c6f9cf81"},
+    {file = "rapidfuzz-3.9.0-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:0e86e39c1c1a0816ceda836e6f7bd3743b930cbc51a43a81bb433b552f203f25"},
+    {file = "rapidfuzz-3.9.0-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:731269812ea837e0b93d913648e404736407408e33a00b75741e8f27c590caa2"},
+    {file = "rapidfuzz-3.9.0-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:8e5ff882d3a3d081157ceba7e0ebc7fac775f95b08cbb143accd4cece6043819"},
+    {file = "rapidfuzz-3.9.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:2003071aa633477a01509890c895f9ef56cf3f2eaa72c7ec0b567f743c1abcba"},
+    {file = "rapidfuzz-3.9.0-cp312-cp312-win32.whl", hash = "sha256:13857f9070600ea1f940749f123b02d0b027afbaa45e72186df0f278915761d0"},
+    {file = "rapidfuzz-3.9.0-cp312-cp312-win_amd64.whl", hash = "sha256:134b7098ac109834eeea81424b6822f33c4c52bf80b81508295611e7a21be12a"},
+    {file = "rapidfuzz-3.9.0-cp312-cp312-win_arm64.whl", hash = "sha256:2a96209f046fe328be30fc43f06e3d4b91f0d5b74e9dcd627dbfd65890fa4a5e"},
+    {file = "rapidfuzz-3.9.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:544b0bf9d17170720809918e9ccd0d482d4a3a6eca35630d8e1459f737f71755"},
+    {file = "rapidfuzz-3.9.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:1d536f8beb8dd82d6efb20fe9f82c2cfab9ffa0384b5d184327e393a4edde91d"},
+    {file = "rapidfuzz-3.9.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:30f7609da871510583f87484a10820b26555a473a90ab356cdda2f3b4456256c"},
+    {file = "rapidfuzz-3.9.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3f4a2468432a1db491af6f547fad8f6d55fa03e57265c2f20e5eaceb68c7907e"},
+    {file = "rapidfuzz-3.9.0-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:11a7ec4676242c8a430509cff42ce98bca2fbe30188a63d0f60fdcbfd7e84970"},
+    {file = "rapidfuzz-3.9.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:dcb523243e988c849cf81220164ec3bbed378a699e595a8914fffe80596dc49f"},
+    {file = "rapidfuzz-3.9.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4eea3bf72c4fe68e957526ffd6bcbb403a21baa6b3344aaae2d3252313df6199"},
+    {file = "rapidfuzz-3.9.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4514980a5d204c076dd5b756960f6b1b7598f030009456e6109d76c4c331d03c"},
+    {file = "rapidfuzz-3.9.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:9a06a99f1335fe43464d7121bc6540de7cd9c9475ac2025babb373fe7f27846b"},
+    {file = "rapidfuzz-3.9.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:6c1ed63345d1581c39d4446b1a8c8f550709656ce2a3c88c47850b258167f3c2"},
+    {file = "rapidfuzz-3.9.0-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:cd2e6e97daf17ebb3254285cf8dd86c60d56d6cf35c67f0f9a557ef26bd66290"},
+    {file = "rapidfuzz-3.9.0-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:9bc0f7e6256a9c668482c41c8a3de5d0aa12e8ca346dcc427b97c7edb82cba48"},
+    {file = "rapidfuzz-3.9.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:7c09f4e87e82a164c9db769474bc61f8c8b677f2aeb0234b8abac73d2ecf9799"},
+    {file = "rapidfuzz-3.9.0-cp38-cp38-win32.whl", hash = "sha256:e65b8f7921bf60cbb207c132842a6b45eefef48c4c3b510eb16087d6c08c70af"},
+    {file = "rapidfuzz-3.9.0-cp38-cp38-win_amd64.whl", hash = "sha256:9d6478957fb35c7844ad08f2442b62ba76c1857a56370781a707eefa4f4981e1"},
+    {file = "rapidfuzz-3.9.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:65d9250a4b0bf86320097306084bc3ca479c8f5491927c170d018787793ebe95"},
+    {file = "rapidfuzz-3.9.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:47b7c0840afa724db3b1a070bc6ed5beab73b4e659b1d395023617fc51bf68a2"},
+    {file = "rapidfuzz-3.9.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:3a16c48c6df8fb633efbbdea744361025d01d79bca988f884a620e63e782fe5b"},
+    {file = "rapidfuzz-3.9.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:48105991ff6e4a51c7f754df500baa070270ed3d41784ee0d097549bc9fcb16d"},
+    {file = "rapidfuzz-3.9.0-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6a7f273906b3c7cc6d63a76e088200805947aa0bc1ada42c6a0e582e19c390d7"},
+    {file = "rapidfuzz-3.9.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5c396562d304e974b4b0d5cd3afc4f92c113ea46a36e6bc62e45333d6aa8837e"},
+    {file = "rapidfuzz-3.9.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:68da1b70458fea5290ec9a169fcffe0c17ff7e5bb3c3257e63d7021a50601a8e"},
+    {file = "rapidfuzz-3.9.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0c5b8f9a7b177af6ce7c6ad5b95588b4b73e37917711aafa33b2e79ee80fe709"},
+    {file = "rapidfuzz-3.9.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:3c42a238bf9dd48f4ccec4c6934ac718225b00bb3a438a008c219e7ccb3894c7"},
+    {file = "rapidfuzz-3.9.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:a365886c42177b2beab475a50ba311b59b04f233ceaebc4c341f6f91a86a78e2"},
+    {file = "rapidfuzz-3.9.0-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:ce897b5dafb7fb7587a95fe4d449c1ea0b6d9ac4462fbafefdbbeef6eee4cf6a"},
+    {file = "rapidfuzz-3.9.0-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:413ac49bae291d7e226a5c9be65c71b2630b3346bce39268d02cb3290232e4b7"},
+    {file = "rapidfuzz-3.9.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:ff8982fc3bd49d55a91569fc8a3feba0de4cef0b391ff9091be546e9df075b81"},
+    {file = "rapidfuzz-3.9.0-cp39-cp39-win32.whl", hash = "sha256:3904d0084ab51f82e9f353031554965524f535522a48ec75c30b223eb5a0a488"},
+    {file = "rapidfuzz-3.9.0-cp39-cp39-win_amd64.whl", hash = "sha256:3733aede16ea112728ffeafeb29ccc62e095ed8ec816822fa2a82e92e2c08696"},
+    {file = "rapidfuzz-3.9.0-cp39-cp39-win_arm64.whl", hash = "sha256:fc4e26f592b51f97acf0a3f8dfed95e4d830c6a8fbf359361035df836381ab81"},
+    {file = "rapidfuzz-3.9.0-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:e33362e98c7899b5f60dcb06ada00acd8673ce0d59aefe9a542701251fd00423"},
+    {file = "rapidfuzz-3.9.0-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fb67cf43ad83cb886cbbbff4df7dcaad7aedf94d64fca31aea0da7d26684283c"},
+    {file = "rapidfuzz-3.9.0-pp38-pypy38_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0e2e106cc66453bb80d2ad9c0044f8287415676df5c8036d737d05d4b9cdbf8e"},
+    {file = "rapidfuzz-3.9.0-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b1256915f7e7a5cf2c151c9ac44834b37f9bd1c97e8dec6f936884f01b9dfc7d"},
+    {file = "rapidfuzz-3.9.0-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:ae643220584518cbff8bf2974a0494d3e250763af816b73326a512c86ae782ce"},
+    {file = "rapidfuzz-3.9.0-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:491274080742110427f38a6085bb12dffcaff1eef12dccf9e8758398c7e3957e"},
+    {file = "rapidfuzz-3.9.0-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2bc5559b9b94326922c096b30ae2d8e5b40b2e9c2c100c2cc396ad91bcb84d30"},
+    {file = "rapidfuzz-3.9.0-pp39-pypy39_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:849160dc0f128acb343af514ca827278005c1d00148d025e4035e034fc2d8c7f"},
+    {file = "rapidfuzz-3.9.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:623883fb78e692d54ed7c43b09beec52c6685f10a45a7518128e25746667403b"},
+    {file = "rapidfuzz-3.9.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:d20ab9abc7e19767f1951772a6ab14cb4eddd886493c2da5ee12014596ad253f"},
+    {file = "rapidfuzz-3.9.0.tar.gz", hash = "sha256:b182f0fb61f6ac435e416eb7ab330d62efdbf9b63cf0c7fa12d1f57c2eaaf6f3"},
+]
+
+[package.extras]
+full = ["numpy"]
+
+[[package]]
+name = "regex"
+version = "2024.4.28"
+description = "Alternative regular expression module, to replace re."
+optional = false
+python-versions = ">=3.8"
+files = [
+    {file = "regex-2024.4.28-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:cd196d056b40af073d95a2879678585f0b74ad35190fac04ca67954c582c6b61"},
+    {file = "regex-2024.4.28-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:8bb381f777351bd534462f63e1c6afb10a7caa9fa2a421ae22c26e796fe31b1f"},
+    {file = "regex-2024.4.28-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:47af45b6153522733aa6e92543938e97a70ce0900649ba626cf5aad290b737b6"},
+    {file = "regex-2024.4.28-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:99d6a550425cc51c656331af0e2b1651e90eaaa23fb4acde577cf15068e2e20f"},
+    {file = "regex-2024.4.28-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bf29304a8011feb58913c382902fde3395957a47645bf848eea695839aa101b7"},
+    {file = "regex-2024.4.28-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:92da587eee39a52c91aebea8b850e4e4f095fe5928d415cb7ed656b3460ae79a"},
+    {file = "regex-2024.4.28-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6277d426e2f31bdbacb377d17a7475e32b2d7d1f02faaecc48d8e370c6a3ff31"},
+    {file = "regex-2024.4.28-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:28e1f28d07220c0f3da0e8fcd5a115bbb53f8b55cecf9bec0c946eb9a059a94c"},
+    {file = "regex-2024.4.28-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:aaa179975a64790c1f2701ac562b5eeb733946eeb036b5bcca05c8d928a62f10"},
+    {file = "regex-2024.4.28-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:6f435946b7bf7a1b438b4e6b149b947c837cb23c704e780c19ba3e6855dbbdd3"},
+    {file = "regex-2024.4.28-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:19d6c11bf35a6ad077eb23852827f91c804eeb71ecb85db4ee1386825b9dc4db"},
+    {file = "regex-2024.4.28-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:fdae0120cddc839eb8e3c15faa8ad541cc6d906d3eb24d82fb041cfe2807bc1e"},
+    {file = "regex-2024.4.28-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:e672cf9caaf669053121f1766d659a8813bd547edef6e009205378faf45c67b8"},
+    {file = "regex-2024.4.28-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:f57515750d07e14743db55d59759893fdb21d2668f39e549a7d6cad5d70f9fea"},
+    {file = "regex-2024.4.28-cp310-cp310-win32.whl", hash = "sha256:a1409c4eccb6981c7baabc8888d3550df518add6e06fe74fa1d9312c1838652d"},
+    {file = "regex-2024.4.28-cp310-cp310-win_amd64.whl", hash = "sha256:1f687a28640f763f23f8a9801fe9e1b37338bb1ca5d564ddd41619458f1f22d1"},
+    {file = "regex-2024.4.28-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:84077821c85f222362b72fdc44f7a3a13587a013a45cf14534df1cbbdc9a6796"},
+    {file = "regex-2024.4.28-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b45d4503de8f4f3dc02f1d28a9b039e5504a02cc18906cfe744c11def942e9eb"},
+    {file = "regex-2024.4.28-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:457c2cd5a646dd4ed536c92b535d73548fb8e216ebee602aa9f48e068fc393f3"},
+    {file = "regex-2024.4.28-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2b51739ddfd013c6f657b55a508de8b9ea78b56d22b236052c3a85a675102dc6"},
+    {file = "regex-2024.4.28-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:459226445c7d7454981c4c0ce0ad1a72e1e751c3e417f305722bbcee6697e06a"},
+    {file = "regex-2024.4.28-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:670fa596984b08a4a769491cbdf22350431970d0112e03d7e4eeaecaafcd0fec"},
+    {file = "regex-2024.4.28-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fe00f4fe11c8a521b173e6324d862ee7ee3412bf7107570c9b564fe1119b56fb"},
+    {file = "regex-2024.4.28-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:36f392dc7763fe7924575475736bddf9ab9f7a66b920932d0ea50c2ded2f5636"},
+    {file = "regex-2024.4.28-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:23a412b7b1a7063f81a742463f38821097b6a37ce1e5b89dd8e871d14dbfd86b"},
+    {file = "regex-2024.4.28-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:f1d6e4b7b2ae3a6a9df53efbf199e4bfcff0959dbdb5fd9ced34d4407348e39a"},
+    {file = "regex-2024.4.28-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:499334ad139557de97cbc4347ee921c0e2b5e9c0f009859e74f3f77918339257"},
+    {file = "regex-2024.4.28-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:0940038bec2fe9e26b203d636c44d31dd8766abc1fe66262da6484bd82461ccf"},
+    {file = "regex-2024.4.28-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:66372c2a01782c5fe8e04bff4a2a0121a9897e19223d9eab30c54c50b2ebeb7f"},
+    {file = "regex-2024.4.28-cp311-cp311-win32.whl", hash = "sha256:c77d10ec3c1cf328b2f501ca32583625987ea0f23a0c2a49b37a39ee5c4c4630"},
+    {file = "regex-2024.4.28-cp311-cp311-win_amd64.whl", hash = "sha256:fc0916c4295c64d6890a46e02d4482bb5ccf33bf1a824c0eaa9e83b148291f90"},
+    {file = "regex-2024.4.28-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:08a1749f04fee2811c7617fdd46d2e46d09106fa8f475c884b65c01326eb15c5"},
+    {file = "regex-2024.4.28-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:b8eb28995771c087a73338f695a08c9abfdf723d185e57b97f6175c5051ff1ae"},
+    {file = "regex-2024.4.28-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:dd7ef715ccb8040954d44cfeff17e6b8e9f79c8019daae2fd30a8806ef5435c0"},
+    {file = "regex-2024.4.28-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fb0315a2b26fde4005a7c401707c5352df274460f2f85b209cf6024271373013"},
+    {file = "regex-2024.4.28-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f2fc053228a6bd3a17a9b0a3f15c3ab3cf95727b00557e92e1cfe094b88cc662"},
+    {file = "regex-2024.4.28-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7fe9739a686dc44733d52d6e4f7b9c77b285e49edf8570754b322bca6b85b4cc"},
+    {file = "regex-2024.4.28-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a74fcf77d979364f9b69fcf8200849ca29a374973dc193a7317698aa37d8b01c"},
+    {file = "regex-2024.4.28-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:965fd0cf4694d76f6564896b422724ec7b959ef927a7cb187fc6b3f4e4f59833"},
+    {file = "regex-2024.4.28-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:2fef0b38c34ae675fcbb1b5db760d40c3fc3612cfa186e9e50df5782cac02bcd"},
+    {file = "regex-2024.4.28-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:bc365ce25f6c7c5ed70e4bc674f9137f52b7dd6a125037f9132a7be52b8a252f"},
+    {file = "regex-2024.4.28-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:ac69b394764bb857429b031d29d9604842bc4cbfd964d764b1af1868eeebc4f0"},
+    {file = "regex-2024.4.28-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:144a1fc54765f5c5c36d6d4b073299832aa1ec6a746a6452c3ee7b46b3d3b11d"},
+    {file = "regex-2024.4.28-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:2630ca4e152c221072fd4a56d4622b5ada876f668ecd24d5ab62544ae6793ed6"},
+    {file = "regex-2024.4.28-cp312-cp312-win32.whl", hash = "sha256:7f3502f03b4da52bbe8ba962621daa846f38489cae5c4a7b5d738f15f6443d17"},
+    {file = "regex-2024.4.28-cp312-cp312-win_amd64.whl", hash = "sha256:0dd3f69098511e71880fb00f5815db9ed0ef62c05775395968299cb400aeab82"},
+    {file = "regex-2024.4.28-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:374f690e1dd0dbdcddea4a5c9bdd97632cf656c69113f7cd6a361f2a67221cb6"},
+    {file = "regex-2024.4.28-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:25f87ae6b96374db20f180eab083aafe419b194e96e4f282c40191e71980c666"},
+    {file = "regex-2024.4.28-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:5dbc1bcc7413eebe5f18196e22804a3be1bfdfc7e2afd415e12c068624d48247"},
+    {file = "regex-2024.4.28-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f85151ec5a232335f1be022b09fbbe459042ea1951d8a48fef251223fc67eee1"},
+    {file = "regex-2024.4.28-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:57ba112e5530530fd175ed550373eb263db4ca98b5f00694d73b18b9a02e7185"},
+    {file = "regex-2024.4.28-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:224803b74aab56aa7be313f92a8d9911dcade37e5f167db62a738d0c85fdac4b"},
+    {file = "regex-2024.4.28-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0a54a047b607fd2d2d52a05e6ad294602f1e0dec2291152b745870afc47c1397"},
+    {file = "regex-2024.4.28-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0a2a512d623f1f2d01d881513af9fc6a7c46e5cfffb7dc50c38ce959f9246c94"},
+    {file = "regex-2024.4.28-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:c06bf3f38f0707592898428636cbb75d0a846651b053a1cf748763e3063a6925"},
+    {file = "regex-2024.4.28-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:1031a5e7b048ee371ab3653aad3030ecfad6ee9ecdc85f0242c57751a05b0ac4"},
+    {file = "regex-2024.4.28-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:d7a353ebfa7154c871a35caca7bfd8f9e18666829a1dc187115b80e35a29393e"},
+    {file = "regex-2024.4.28-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:7e76b9cfbf5ced1aca15a0e5b6f229344d9b3123439ffce552b11faab0114a02"},
+    {file = "regex-2024.4.28-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:5ce479ecc068bc2a74cb98dd8dba99e070d1b2f4a8371a7dfe631f85db70fe6e"},
+    {file = "regex-2024.4.28-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:7d77b6f63f806578c604dca209280e4c54f0fa9a8128bb8d2cc5fb6f99da4150"},
+    {file = "regex-2024.4.28-cp38-cp38-win32.whl", hash = "sha256:d84308f097d7a513359757c69707ad339da799e53b7393819ec2ea36bc4beb58"},
+    {file = "regex-2024.4.28-cp38-cp38-win_amd64.whl", hash = "sha256:2cc1b87bba1dd1a898e664a31012725e48af826bf3971e786c53e32e02adae6c"},
+    {file = "regex-2024.4.28-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:7413167c507a768eafb5424413c5b2f515c606be5bb4ef8c5dee43925aa5718b"},
+    {file = "regex-2024.4.28-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:108e2dcf0b53a7c4ab8986842a8edcb8ab2e59919a74ff51c296772e8e74d0ae"},
+    {file = "regex-2024.4.28-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:f1c5742c31ba7d72f2dedf7968998730664b45e38827637e0f04a2ac7de2f5f1"},
+    {file = "regex-2024.4.28-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ecc6148228c9ae25ce403eade13a0961de1cb016bdb35c6eafd8e7b87ad028b1"},
+    {file = "regex-2024.4.28-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b7d893c8cf0e2429b823ef1a1d360a25950ed11f0e2a9df2b5198821832e1947"},
+    {file = "regex-2024.4.28-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4290035b169578ffbbfa50d904d26bec16a94526071ebec3dadbebf67a26b25e"},
+    {file = "regex-2024.4.28-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:44a22ae1cfd82e4ffa2066eb3390777dc79468f866f0625261a93e44cdf6482b"},
+    {file = "regex-2024.4.28-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fd24fd140b69f0b0bcc9165c397e9b2e89ecbeda83303abf2a072609f60239e2"},
+    {file = "regex-2024.4.28-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:39fb166d2196413bead229cd64a2ffd6ec78ebab83fff7d2701103cf9f4dfd26"},
+    {file = "regex-2024.4.28-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:9301cc6db4d83d2c0719f7fcda37229691745168bf6ae849bea2e85fc769175d"},
+    {file = "regex-2024.4.28-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:7c3d389e8d76a49923683123730c33e9553063d9041658f23897f0b396b2386f"},
+    {file = "regex-2024.4.28-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:99ef6289b62042500d581170d06e17f5353b111a15aa6b25b05b91c6886df8fc"},
+    {file = "regex-2024.4.28-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:b91d529b47798c016d4b4c1d06cc826ac40d196da54f0de3c519f5a297c5076a"},
+    {file = "regex-2024.4.28-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:43548ad74ea50456e1c68d3c67fff3de64c6edb85bcd511d1136f9b5376fc9d1"},
+    {file = "regex-2024.4.28-cp39-cp39-win32.whl", hash = "sha256:05d9b6578a22db7dedb4df81451f360395828b04f4513980b6bd7a1412c679cc"},
+    {file = "regex-2024.4.28-cp39-cp39-win_amd64.whl", hash = "sha256:3986217ec830c2109875be740531feb8ddafe0dfa49767cdcd072ed7e8927962"},
+    {file = "regex-2024.4.28.tar.gz", hash = "sha256:83ab366777ea45d58f72593adf35d36ca911ea8bd838483c1823b883a121b0e4"},
+]
+
+[[package]]
+name = "requests"
+version = "2.31.0"
+description = "Python HTTP for Humans."
+optional = false
+python-versions = ">=3.7"
+files = [
+    {file = "requests-2.31.0-py3-none-any.whl", hash = "sha256:58cd2187c01e70e6e26505bca751777aa9f2ee0b7f4300988b709f44e013003f"},
+    {file = "requests-2.31.0.tar.gz", hash = "sha256:942c5a758f98d790eaed1a29cb6eefc7ffb0d1cf7af05c3d2791656dbd6ad1e1"},
+]
+
+[package.dependencies]
+certifi = ">=2017.4.17"
+charset-normalizer = ">=2,<4"
+idna = ">=2.5,<4"
+urllib3 = ">=1.21.1,<3"
+
+[package.extras]
+socks = ["PySocks (>=1.5.6,!=1.5.7)"]
+use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"]
+
+[[package]]
+name = "six"
+version = "1.16.0"
+description = "Python 2 and 3 compatibility utilities"
+optional = false
+python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*"
+files = [
+    {file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"},
+    {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"},
+]
+
+[[package]]
+name = "tomli"
+version = "2.0.1"
+description = "A lil' TOML parser"
+optional = false
+python-versions = ">=3.7"
+files = [
+    {file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"},
+    {file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"},
+]
+
+[[package]]
+name = "tzdata"
+version = "2024.1"
+description = "Provider of IANA time zone data"
+optional = false
+python-versions = ">=2"
+files = [
+    {file = "tzdata-2024.1-py2.py3-none-any.whl", hash = "sha256:9068bc196136463f5245e51efda838afa15aaeca9903f49050dfa2679db4d252"},
+    {file = "tzdata-2024.1.tar.gz", hash = "sha256:2674120f8d891909751c38abcdfd386ac0a5a1127954fbc332af6b5ceae07efd"},
+]
+
+[[package]]
+name = "tzlocal"
+version = "5.2"
+description = "tzinfo object for the local timezone"
+optional = false
+python-versions = ">=3.8"
+files = [
+    {file = "tzlocal-5.2-py3-none-any.whl", hash = "sha256:49816ef2fe65ea8ac19d19aa7a1ae0551c834303d5014c6d5a62e4cbda8047b8"},
+    {file = "tzlocal-5.2.tar.gz", hash = "sha256:8d399205578f1a9342816409cc1e46a93ebd5755e39ea2d85334bea911bf0e6e"},
+]
+
+[package.dependencies]
+tzdata = {version = "*", markers = "platform_system == \"Windows\""}
+
+[package.extras]
+devenv = ["check-manifest", "pytest (>=4.3)", "pytest-cov", "pytest-mock (>=3.3)", "zest.releaser"]
+
+[[package]]
+name = "urllib3"
+version = "2.2.1"
+description = "HTTP library with thread-safe connection pooling, file post, and more."
+optional = false
+python-versions = ">=3.8"
+files = [
+    {file = "urllib3-2.2.1-py3-none-any.whl", hash = "sha256:450b20ec296a467077128bff42b73080516e71b56ff59a60a02bef2232c4fa9d"},
+    {file = "urllib3-2.2.1.tar.gz", hash = "sha256:d0570876c61ab9e520d776c38acbbb5b05a776d3f9ff98a5c8fd5162a444cf19"},
+]
+
+[package.extras]
+brotli = ["brotli (>=1.0.9)", "brotlicffi (>=0.8.0)"]
+h2 = ["h2 (>=4,<5)"]
+socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"]
+zstd = ["zstandard (>=0.18.0)"]
+
+[metadata]
+lock-version = "2.0"
+python-versions = "^3.10"
+content-hash = "f65cd66387236673e2a5afb3b2a75362c97815cdde592a86712737fb9ca71695"
diff --git a/pkgs/sources/update_vim_plugins/pyproject.toml b/pkgs/sources/update_vim_plugins/pyproject.toml
new file mode 100644
index 00000000..38caf76d
--- /dev/null
+++ b/pkgs/sources/update_vim_plugins/pyproject.toml
@@ -0,0 +1,45 @@
+[tool.poetry]
+name = "update_vim_plugins"
+version = "0.1.0"
+description = ""
+authors = ["Your Name <you@example.com>"]
+packages = [{ include = "update_vim_plugins" }]
+
+[tool.poetry.scripts]
+update-vim-plugins = "update_vim_plugins.__main__:main"
+
+[tool.poetry.dependencies]
+python = "^3.10"
+requests = "^2.28.2"
+cleo = "^2.0.1"
+jsonpickle = "*"
+dateparser = "^1.1.8"
+
+[tool.poetry.group.test.dependencies]
+pytest-cov = "^4.0.0"
+pytest = "^7.3.1"
+pytest-mock = "^3.10.0"
+
+[tool.poetry.group.dev]
+optional = true
+
+[tool.poetry.group.dev.dependencies]
+# black = "^23.3.0"
+# ruff-lsp = "^0.0.24"
+# mypy = "^1.2.0"
+# types-requests = "^2.28.11.17"
+# isort = "^5.12.0"
+# ruff = "^0.0.262"
+
+[tool.isort]
+profile = "black"
+
+[tool.black]
+line-length = 120
+
+[tool.ruff]
+line-length = 120
+
+[build-system]
+requires = ["poetry-core"]
+build-backend = "poetry.core.masonry.api"
diff --git a/pkgs/sources/update_vim_plugins/update.sh b/pkgs/sources/update_vim_plugins/update.sh
new file mode 100755
index 00000000..1bad12a9
--- /dev/null
+++ b/pkgs/sources/update_vim_plugins/update.sh
@@ -0,0 +1,5 @@
+#!/usr/bin/env sh
+
+poetry update --lock
+
+# vim: ft=sh
diff --git a/pkgs/sources/update_vim_plugins/update_vim_plugins/__init__.py b/pkgs/sources/update_vim_plugins/update_vim_plugins/__init__.py
new file mode 100644
index 00000000..e69de29b
--- /dev/null
+++ b/pkgs/sources/update_vim_plugins/update_vim_plugins/__init__.py
diff --git a/pkgs/sources/update_vim_plugins/update_vim_plugins/__main__.py b/pkgs/sources/update_vim_plugins/update_vim_plugins/__main__.py
new file mode 100644
index 00000000..a8d9e06f
--- /dev/null
+++ b/pkgs/sources/update_vim_plugins/update_vim_plugins/__main__.py
@@ -0,0 +1,15 @@
+from cleo.application import Application
+
+from .update import UpdateCommand
+from .cleanup import CleanUpCommand
+
+
+def main():
+    application = Application()
+    application.add(UpdateCommand())
+    application.add(CleanUpCommand())
+    application.run()
+
+
+if __name__ == "__main__":
+    main()
diff --git a/pkgs/sources/update_vim_plugins/update_vim_plugins/cleanup.py b/pkgs/sources/update_vim_plugins/update_vim_plugins/cleanup.py
new file mode 100644
index 00000000..fd313ed0
--- /dev/null
+++ b/pkgs/sources/update_vim_plugins/update_vim_plugins/cleanup.py
@@ -0,0 +1,100 @@
+from cleo.commands.command import Command
+from cleo.helpers import argument
+
+from .helpers import read_manifest_to_spec, read_blacklist_to_spec, write_manifest_from_spec
+
+
+class CleanUpCommand(Command):
+    name = "cleanup"
+    description = "Clean up manifest"
+    arguments = [argument("plug_dir", description="Path to the plugin directory", optional=False)]
+
+    def handle(self):
+        """Main command function"""
+
+        plug_dir = self.argument("plug_dir")
+        self.line("<comment>Checking manifest file</comment>")
+        # all cleaning up will be done during reading and writing automatically
+        manifest = read_manifest_to_spec(plug_dir)
+        blacklist = read_blacklist_to_spec(plug_dir)
+
+        new_manifest = [spec for spec in manifest if spec not in blacklist]
+
+        new_manifest_filterd = self.filter_renamed(new_manifest)
+
+        write_manifest_from_spec(new_manifest_filterd, plug_dir)
+
+        self.line("<comment>Done</comment>")
+
+    def filter_renamed(self, specs):
+        """Filter specs that define the same plugin (same owner and same repo) but with different properties.
+        This could be a different name, source, or branch
+        """
+
+        error = False
+        for i, p in enumerate(specs):
+            for p2 in specs:
+                same_owner = p.owner.lower() == p2.owner.lower()
+                same_repo = p.repo.lower() == p2.repo.lower()
+                different_specs = p != p2
+                marked_duplicate = p.marked_duplicate or p2.marked_duplicate
+
+                if same_owner and same_repo and different_specs and not marked_duplicate:
+                    self.line("<info>The following lines appear to define the same plugin</info>")
+
+                    p_props_defined = p.branch is not None or p.custom_name is not None
+                    p2_props_defined = p2.branch is not None or p2.custom_name is not None
+                    p_is_lower_case = p.owner == p.owner.lower() and p.name == p.name.lower()
+                    p2_is_lower_case = p2.owner == p2.owner.lower() and p2.name == p2.name.lower()
+
+                    # list of conditions for selecting p
+                    select_p = p_props_defined and not p2_props_defined or p2_is_lower_case and not p_is_lower_case
+                    # list of conditions for selecting p2
+                    select_p2 = p2_props_defined and not p_props_defined or p_is_lower_case and not p2_is_lower_case
+
+                    # one is more defined and is all lower, but the other is not all lower
+                    # (we assume the not all lower case is the correct naming)
+                    error_props_lower = (
+                        p_props_defined and p_is_lower_case and not p2_props_defined and not p2_is_lower_case
+                    )
+                    error_props_lower2 = (
+                        p2_props_defined and p2_is_lower_case and not p_props_defined and not p_is_lower_case
+                    )
+
+                    # both props are defined
+                    error_props = p_props_defined and p2_props_defined
+
+                    # the sources are different
+                    error_source = p.repository_host != p2.repository_host
+
+                    if error_props_lower or error_props_lower2 or error_props or error_source:
+                        self.line(" • <error>Cannot determine which is the correct plugin</error>")
+                        self.line(f" - {p.line}")
+                        self.line(f" - {p2.line}")
+                        error = True
+                        # remove second spec to not encounter the error twice
+                        # this will not be written to the manifest.txt because we set
+                        # the error flag and will exit after the loop
+                        specs.remove(p2)
+                    elif select_p:
+                        self.line(f" - <comment>{p.line}</comment>")
+                        self.line(f" - {p2.line}")
+                        specs.remove(p2)
+                    elif select_p2:
+                        self.line(f" - {p.line}")
+                        self.line(f" - <comment>{p2.line}</comment>")
+                        specs.remove(p)
+                    else:
+                        self.line(" • <error>Logic error in correct spec determination</error>")
+                        self.line(f" - {p.line}")
+                        self.line(f" - {p2.line}")
+                        error = True
+                        # remove second spec to not encounter the error twice
+                        # this will not be written to the manifest.txt because we set
+                        # the error flag and will exit after the loop
+                        specs.remove(p)
+        if error:
+            # exit after all errors have been found
+            exit(1)
+
+        return specs
diff --git a/pkgs/sources/update_vim_plugins/update_vim_plugins/helpers.py b/pkgs/sources/update_vim_plugins/update_vim_plugins/helpers.py
new file mode 100644
index 00000000..8a28b0e8
--- /dev/null
+++ b/pkgs/sources/update_vim_plugins/update_vim_plugins/helpers.py
@@ -0,0 +1,61 @@
+from .spec import PluginSpec
+
+MANIFEST_FILE = "manifest.txt"
+BLACKLIST_FILE = "blacklist.txt"
+PKGS_FILE = "default.nix"
+JSON_FILE = ".plugins.json"
+PLUGINS_LIST_FILE = "plugins.md"
+
+
+def get_const(const: str, plug_dir: str) -> str:
+    out = plug_dir + "/" + const
+    return out
+
+
+def read_manifest(plug_dir: str) -> list[str]:
+    with open(get_const(MANIFEST_FILE, plug_dir), "r") as file:
+        specs = set([spec.strip() for spec in file.readlines()])
+
+    return sorted(specs)
+
+
+def read_manifest_to_spec(plug_dir: str) -> list[PluginSpec]:
+    manifest = read_manifest(plug_dir)
+    specs = [PluginSpec.from_spec(spec.strip()) for spec in manifest]
+
+    return sorted(specs)
+
+
+def read_blacklist(plug_dir: str) -> list[str]:
+    with open(get_const(BLACKLIST_FILE, plug_dir), "r") as file:
+        if len(file.readlines()) == 0:
+            return [""]
+        else:
+            blacklisted_specs = set([spec.strip() for spec in file.readlines()])
+
+    return sorted(blacklisted_specs)
+
+
+def read_blacklist_to_spec(plug_dir: str) -> list[PluginSpec]:
+    blacklist = read_blacklist(plug_dir)
+    specs = [PluginSpec.from_spec(spec.strip()) for spec in blacklist]
+
+    return sorted(specs)
+
+
+def write_manifest(specs: list[str] | set[str], plug_dir: str):
+    """write specs to manifest file. Does some cleaning up"""
+
+    with open(get_const(MANIFEST_FILE, plug_dir), "w") as file:
+        specs = sorted(set(specs), key=lambda x: x.lower())
+        specs = [p for p in specs]
+
+        for s in specs:
+            file.write(f"{s}\n")
+
+
+def write_manifest_from_spec(specs: list[PluginSpec], plug_dir: str):
+    """write specs to manifest file. Does some cleaning up"""
+
+    strings = [f"{spec}" for spec in specs]
+    write_manifest(strings, plug_dir)
diff --git a/pkgs/sources/update_vim_plugins/update_vim_plugins/nix.py b/pkgs/sources/update_vim_plugins/update_vim_plugins/nix.py
new file mode 100644
index 00000000..66a8df4c
--- /dev/null
+++ b/pkgs/sources/update_vim_plugins/update_vim_plugins/nix.py
@@ -0,0 +1,121 @@
+import abc
+import enum
+import json
+import subprocess
+
+
+def nix_prefetch_url(url):
+    """Return the sha256 hash of the given url."""
+    subprocess_output = subprocess.check_output(
+        ["nix-prefetch-url", "--type", "sha256", url],
+        stderr=subprocess.DEVNULL,
+    )
+    sha256 = subprocess_output.decode("utf-8").strip()
+    return sha256
+
+
+def nix_prefetch_git(url):
+    """Return the sha256 hash of the given git url."""
+    subprocess_output = subprocess.check_output(["nix-prefetch-git", url], stderr=subprocess.DEVNULL)
+    sha256 = json.loads(subprocess_output)["sha256"]
+    return sha256
+
+
+class Source(abc.ABC):
+    """Abstract base class for sources."""
+
+    url: str
+    sha256: str
+
+    @abc.abstractmethod
+    def __init__(self, url: str) -> None:
+        """Initialize a Source."""
+
+    @abc.abstractmethod
+    def get_nix_expression(self):
+        """Return the nix expression for this source."""
+
+    def __repr__(self):
+        """Return the representation of this source."""
+        return self.get_nix_expression()
+
+
+class UrlSource(Source):
+    """A source that is a url."""
+
+    def __init__(self, url: str) -> None:
+        """Initialize a UrlSource."""
+        self.url = url
+        self.sha256 = nix_prefetch_url(url)
+
+    def get_nix_expression(self):
+        """Return the nix expression for this source."""
+        return f'fetchurl {{ url = "{self.url}"; sha256 = "{self.sha256}"; }}'
+
+
+class GitSource(Source):
+    """A source that is a git repository."""
+
+    def __init__(self, url: str, rev: str) -> None:
+        """Initialize a GitSource."""
+        self.url = url
+        self.rev = rev
+        self.sha256 = nix_prefetch_git(url)
+
+    def get_nix_expression(self):
+        """Return the nix expression for this source."""
+        return f'fetchgit {{ url = "{self.url}"; rev = "{self.rev}"; sha256 = "{self.sha256}"; }}'
+
+
+class License(enum.Enum):
+    """An enumeration of licenses."""
+
+    AGPL_3_0 = "agpl3Only"
+    APACHE_2_0 = "asf20"
+    BSD_2_CLAUSE = "bsd2"
+    BSD_3_CLAUSE = "bsd3"
+    BSL_1_0 = "bsl1_0"
+    CC0_1_0 = "cc0"
+    EPL_2_0 = "epl20"
+    GPL_2_0 = "gpl2Only"
+    GPL_3_0 = "gpl3Only"
+    ISCLGPL_2_1 = "lgpl21Only"
+    MIT = "mit"
+    MPL_2_0 = "mpl20"
+    UNLUNLICENSE = "unlicense"
+    WTFPL = "wtfpl"
+    UNFREE = "unfree"
+    UNKNOWN = ""
+
+    @classmethod
+    def from_spdx_id(cls, spdx_id: str | None) -> "License":
+        """Return the License from the given spdx_id."""
+        mapping = {
+            "AGPL-3.0": cls.AGPL_3_0,
+            "AGPL-3.0-only": cls.AGPL_3_0,
+            "Apache-2.0": cls.APACHE_2_0,
+            "BSD-2-Clause": cls.BSD_2_CLAUSE,
+            "BSD-3-Clause": cls.BSD_3_CLAUSE,
+            "BSL-1.0": cls.BSL_1_0,
+            "CC0-1.0": cls.CC0_1_0,
+            "EPL-2.0": cls.EPL_2_0,
+            "GPL-2.0": cls.GPL_2_0,
+            "GPL-2.0-only": cls.GPL_2_0,
+            "GPL-3.0": cls.GPL_3_0,
+            "GPL-3.0-only": cls.GPL_3_0,
+            "LGPL-2.1-only": cls.ISCLGPL_2_1,
+            "MIT": cls.MIT,
+            "MPL-2.0": cls.MPL_2_0,
+            "Unlicense": cls.UNLUNLICENSE,
+            "WTFPL": cls.WTFPL,
+        }
+
+        if spdx_id is None:
+            return cls.UNKNOWN
+
+        spdx_id = spdx_id.upper()
+        return mapping.get(spdx_id, cls.UNKNOWN)
+
+    def __str__(self):
+        """Return the string representation of this license."""
+        return self.value
diff --git a/pkgs/sources/update_vim_plugins/update_vim_plugins/plugin.py b/pkgs/sources/update_vim_plugins/update_vim_plugins/plugin.py
new file mode 100644
index 00000000..8334ad53
--- /dev/null
+++ b/pkgs/sources/update_vim_plugins/update_vim_plugins/plugin.py
@@ -0,0 +1,182 @@
+import logging
+import os
+import urllib
+
+import requests
+import jsonpickle
+from datetime import datetime, date
+from dateparser import parse
+
+from .nix import GitSource, License, Source, UrlSource
+from .spec import PluginSpec, RepositoryHost
+
+
+logger = logging.getLogger(__name__)
+
+
+class VimPlugin:
+    """Abstract base class for vim plugins."""
+
+    name: str
+    owner: str
+    repo: str
+    version: date
+    source: Source
+    description: str = "No description"
+    homepage: str
+    license: License
+    source_line: str
+    checked: date = datetime.now().date()
+
+    def to_nix(self):
+        """Return the nix expression for this plugin."""
+        meta = f'with lib; {{ description = "{self.description}"; homepage = "{self.homepage}"; license = with licenses; [ {self.license.value} ]; }}'
+        return f'/* Generated from: {self.source_line} */ {self.name} = buildVimPlugin {{ pname = "{self.name}";  version = "{self.version}"; src = {self.source.get_nix_expression()}; meta = {meta}; }};'
+
+    def to_json(self):
+        """Serizalize the plugin to json"""
+        return jsonpickle.encode(self)
+
+    def to_markdown(self):
+        link = f"[{self.source_line}]({self.homepage})"
+        version = f"{self.version}"
+        package_name = f"{self.name}"
+        checked = f"{self.checked}"
+
+        return f"| {link} | {version} | `{package_name}` | {checked} |"
+
+    def __lt__(self, o: object) -> bool:
+        if not isinstance(o, VimPlugin):
+            return False
+
+        return self.name.lower() < o.name.lower()
+
+    def __repr__(self):
+        """Return the representation of this plugin."""
+        return f"VimPlugin({self.name!r}, {self.version.strftime('%Y-%m-%d')})"
+
+
+def _get_github_token():
+    token = os.environ.get("GITHUB_TOKEN")
+    if token is None:
+        # NOTE: This should never use more than the free api requests <2023-12-09>
+        pass
+        # logger.warning("GITHUB_TOKEN environment variable not set")
+    return token
+
+
+class GitHubPlugin(VimPlugin):
+    def __init__(self, plugin_spec: PluginSpec) -> None:
+        """Initialize a GitHubPlugin."""
+
+        full_name = f"{plugin_spec.owner}/{plugin_spec.repo}"
+        repo_info = self._api_call(f"repos/{full_name}")
+        default_branch = plugin_spec.branch or repo_info["default_branch"]
+        api_callback = self._api_call(f"repos/{full_name}/commits/{default_branch}")
+        latest_commit = api_callback["commit"]
+        sha = api_callback["sha"]
+
+        self.name = plugin_spec.name
+        self.owner = plugin_spec.owner
+        self.version = parse(latest_commit["committer"]["date"]).date()
+        self.source = UrlSource(f"https://github.com/{full_name}/archive/{sha}.tar.gz")
+        self.description = (repo_info.get("description") or "").replace('"', '\\"')
+        self.homepage = repo_info["html_url"]
+        self.license = plugin_spec.license or License.from_spdx_id((repo_info.get("license") or {}).get("spdx_id"))
+        self.source_line = plugin_spec.line
+
+    def _api_call(self, path: str, token: str | None = _get_github_token()):
+        """Call the GitHub API."""
+        url = f"https://api.github.com/{path}"
+        headers = {"Content-Type": "application/json"}
+        if token is not None:
+            headers["Authorization"] = f"token {token}"
+        response = requests.get(url, headers=headers)
+        if response.status_code != 200:
+            raise RuntimeError(f"GitHub API call failed: {response.text}")
+        return response.json()
+
+
+class GitlabPlugin(VimPlugin):
+    def __init__(self, plugin_spec: PluginSpec) -> None:
+        """Initialize a GitlabPlugin."""
+
+        full_name = urllib.parse.quote(f"{plugin_spec.owner}/{plugin_spec.repo}", safe="")
+        repo_info = self._api_call(f"projects/{full_name}")
+        default_branch = plugin_spec.branch or repo_info["default_branch"]
+        api_callback = self._api_call(f"projects/{full_name}/repository/branches/{default_branch}")
+        latest_commit = api_callback["commit"]
+        sha = latest_commit["id"]
+
+        self.name = plugin_spec.name
+        self.owner = plugin_spec.owner
+        self.version = parse(latest_commit["created_at"]).date()
+        self.source = UrlSource(f"https://gitlab.com/api/v4/projects/{full_name}/repository/archive.tar.gz?sha={sha}")
+        self.description = (repo_info.get("description") or "").replace('"', '\\"')
+        self.homepage = repo_info["web_url"]
+        self.license = plugin_spec.license or License.from_spdx_id(repo_info.get("license", {}).get("key"))
+        self.source_line = plugin_spec.line
+
+    def _api_call(self, path: str) -> dict:
+        """Call the Gitlab API."""
+        url = f"https://gitlab.com/api/v4/{path}"
+        response = requests.get(url)
+        if response.status_code != 200:
+            raise RuntimeError(f"Gitlab API call failed: {response.text}")
+        return response.json()
+
+
+def _get_sourcehut_token():
+    token = os.environ.get("SOURCEHUT_TOKEN")
+    if token is None:
+        # NOTE: This should never use more than the free requests <2023-12-09>
+        pass
+        # logger.warning("SOURCEHUT_TOKEN environment variable not set")
+    return token
+
+
+class SourceHutPlugin(VimPlugin):
+    def __init__(self, plugin_spec: PluginSpec) -> None:
+        """Initialize a SourceHutPlugin."""
+
+        repo_info = self._api_call(f"~{plugin_spec.owner}/repos/{plugin_spec.repo}")
+        if plugin_spec.branch is None:
+            commits = self._api_call(f"~{plugin_spec.owner}/repos/{plugin_spec.repo}/log")
+        else:
+            commits = self._api_call(f"~{plugin_spec.owner}/repos/{plugin_spec.repo}/log/{plugin_spec.branch}")
+        latest_commit = commits["results"][0]
+        sha = latest_commit["id"]
+
+        self.name = plugin_spec.name
+        self.owner = plugin_spec.owner
+        self.version = parse(latest_commit["timestamp"]).date()
+        self.description = (repo_info.get("description") or "").replace('"', '\\"')
+        self.homepage = f"https://git.sr.ht/~{plugin_spec.owner}/{plugin_spec.repo}"
+        self.source = GitSource(self.homepage, sha)
+        self.license = plugin_spec.license or License.UNKNOWN  # cannot be determined via API
+        self.source_line = plugin_spec.line
+
+    def _api_call(self, path: str, token: str | None = _get_sourcehut_token()):
+        """Call the SourceHut API."""
+
+        url = f"https://git.sr.ht/api/{path}"
+        headers = {"Content-Type": "application/json"}
+        if token is not None:
+            headers["Authorization"] = f"token {token}"
+        response = requests.get(url, headers=headers)
+        if response.status_code != 200:
+            raise RuntimeError(f"SourceHut API call failed: {response.json()}")
+        return response.json()
+
+
+def plugin_from_spec(plugin_spec: PluginSpec) -> VimPlugin:
+    """Initialize a VimPlugin."""
+
+    if plugin_spec.repository_host == RepositoryHost.GITHUB:
+        return GitHubPlugin(plugin_spec)
+    elif plugin_spec.repository_host == RepositoryHost.GITLAB:
+        return GitlabPlugin(plugin_spec)
+    elif plugin_spec.repository_host == RepositoryHost.SOURCEHUT:
+        return SourceHutPlugin(plugin_spec)
+    else:
+        raise NotImplementedError(f"Unsupported source: {plugin_spec.repository_host}")
diff --git a/pkgs/sources/update_vim_plugins/update_vim_plugins/spec.py b/pkgs/sources/update_vim_plugins/update_vim_plugins/spec.py
new file mode 100644
index 00000000..0f2fb29c
--- /dev/null
+++ b/pkgs/sources/update_vim_plugins/update_vim_plugins/spec.py
@@ -0,0 +1,143 @@
+import enum
+import re
+
+from .nix import License
+
+
+class RepositoryHost(enum.Enum):
+    """A repository host."""
+
+    GITHUB = "github"
+    GITLAB = "gitlab"
+    SOURCEHUT = "sourcehut"
+
+
+class PluginSpec:
+    """A Vim plugin Spec."""
+
+    @classmethod
+    def from_spec(cls, spec):
+        """The spec line must be in the format:
+            [<repository_host>:]<owner>/<repo>[:<branch>][:name].
+
+        repository_host is one of github (default), gitlab, or sourcehut.
+        owner is the repository owner.
+        repo is the repository name.
+        branch is the git branch.
+        name is the name to use for the plugin (default is value of repo).
+        """
+        repository_host = RepositoryHost.GITHUB
+        # gitref = "master"
+
+        repository_host_regex = r"((?P<repository_host>[^:]+):)"
+        owner_regex = r"(?P<owner>[^/:]+)"
+        repo_regex = r"(?P<repo>[^:]+)"
+        branch_regex = r"(:(?P<branch>[^:]+)?)"
+        name_regex = r"(:(?P<name>[^:]+)?)"
+        license_regex = r"(:(?P<license>[^:]+)?)"
+        marked_duplicate_regex = r"(:(?P<duplicate>duplicate))"
+
+        spec_regex = re.compile(
+            f"^{repository_host_regex}?{owner_regex}/{repo_regex}{branch_regex}?{name_regex}?{license_regex}?{marked_duplicate_regex}?$",
+        )
+
+        match = spec_regex.match(spec)
+        if match is None:
+            raise ValueError(f"Invalid spec: {spec}")
+
+        group_dict = match.groupdict()
+
+        repository_host = RepositoryHost(group_dict.get("repository_host") or "github")
+
+        owner = group_dict.get("owner")
+        if owner is None:
+            raise RuntimeError("Could not get owner")
+
+        repo = group_dict.get("repo")
+        if repo is None:
+            raise RuntimeError("Could not get repo")
+
+        branch = group_dict.get("branch")
+        name = group_dict.get("name")
+        license = group_dict.get("license")
+        marked_duplicate = bool(group_dict.get("duplicate"))  # True if 'duplicate', False if None
+
+        line = spec
+
+        return cls(repository_host, owner, repo, line, branch, name, license, marked_duplicate)
+
+    def __init__(
+        self,
+        repository_host: RepositoryHost,
+        owner: str,
+        repo: str,
+        line: str,
+        branch: str | None = None,
+        name: str | None = None,
+        license: str | None = None,
+        marked_duplicate: bool = False,
+    ) -> None:
+        """Initialize a VimPluginSpec."""
+        self.repository_host = repository_host
+        self.owner = owner
+        self.repo = repo
+        self.branch = branch
+        self.custom_name = name
+        self.name = name or repo.replace(".", "-").replace("_", "-")
+        self.license = License(license) if license else None
+        self.line = line
+        self.marked_duplicate = marked_duplicate
+
+    def __str__(self) -> str:
+        """Return a string representation of a VimPluginSpec."""
+        spec = ""
+
+        if self.repository_host != RepositoryHost.GITHUB:
+            spec += f"{self.repository_host.value}:"
+
+        spec += f"{self.owner}/{self.repo}"
+
+        spec += ":"
+        if self.branch is not None:
+            spec += self.branch
+
+        spec += ":"
+        if self.custom_name is not None:
+            spec += self.custom_name
+
+        spec += ":"
+        if self.license is not None:
+            spec += str(self.license)
+
+        spec += ":"
+        if self.marked_duplicate:
+            spec += "duplicate"
+
+        return spec.rstrip(":")
+
+    def __repr__(self):
+        """Return the representation of the specs"""
+        return f"PluginSpec({self.owner}/{self.repo}, {self.name})"
+
+    def to_spec(self):
+        """Return a spec line for a VimPluginSpec."""
+        return str(self)
+
+    def __lt__(self, o: object) -> bool:
+        if not isinstance(o, PluginSpec):
+            return False
+
+        return self.name.lower() < o.name.lower()
+
+    def __eq__(self, o: object) -> bool:
+        """Return True if the two specs are equal."""
+        if not isinstance(o, PluginSpec):
+            return False
+
+        return (
+            self.repository_host == o.repository_host
+            and self.owner == o.owner
+            and self.repo == o.repo
+            and self.branch == o.branch
+            and self.name == o.name
+        )
diff --git a/pkgs/sources/update_vim_plugins/update_vim_plugins/tests/__init__.py b/pkgs/sources/update_vim_plugins/update_vim_plugins/tests/__init__.py
new file mode 100644
index 00000000..e69de29b
--- /dev/null
+++ b/pkgs/sources/update_vim_plugins/update_vim_plugins/tests/__init__.py
diff --git a/pkgs/sources/update_vim_plugins/update_vim_plugins/tests/fixtures.py b/pkgs/sources/update_vim_plugins/update_vim_plugins/tests/fixtures.py
new file mode 100644
index 00000000..75dd251a
--- /dev/null
+++ b/pkgs/sources/update_vim_plugins/update_vim_plugins/tests/fixtures.py
@@ -0,0 +1,44 @@
+import json
+
+import pytest
+from pytest_mock import MockerFixture
+
+from update_vim_plugins.nix import GitSource, UrlSource
+
+
+@pytest.fixture()
+def url():
+    return "https://example.com"
+
+
+@pytest.fixture()
+def rev():
+    return "1234567890abcdef"
+
+
+@pytest.fixture()
+def sha256():
+    return "sha256-1234567890abcdef"
+
+
+@pytest.fixture()
+def url_source(mocker: MockerFixture, url: str, sha256: str):
+    mocker.patch("subprocess.check_output", return_value=bytes(sha256, "utf-8"))
+    return UrlSource(url)
+
+
+@pytest.fixture()
+def git_source(mocker: MockerFixture, url: str, rev: str, sha256: str):
+    return_value = {
+        "url": url,
+        "rev": rev,
+        "date": "1970-01-01T00:00:00+00:00",
+        "path": "",
+        "sha256": sha256,
+        "fetchLFS": False,
+        "fetchSubmodules": False,
+        "deepClone": False,
+        "leaveDotGit": False,
+    }
+    mocker.patch("subprocess.check_output", return_value=json.dumps(return_value).encode("utf-8"))
+    return GitSource(url, rev)
diff --git a/pkgs/sources/update_vim_plugins/update_vim_plugins/tests/test_nix.py b/pkgs/sources/update_vim_plugins/update_vim_plugins/tests/test_nix.py
new file mode 100644
index 00000000..46e59f76
--- /dev/null
+++ b/pkgs/sources/update_vim_plugins/update_vim_plugins/tests/test_nix.py
@@ -0,0 +1,32 @@
+from update_vim_plugins.nix import GitSource, License, UrlSource
+
+
+def test_url_source(url_source: UrlSource, url: str, sha256: str):
+    assert url_source.url == url
+    assert url_source.sha256 == sha256
+
+
+def test_url_source_nix_expression(url_source: UrlSource, url: str, sha256: str):
+    assert url_source.get_nix_expression() == f'fetchurl {{ url = "{url}"; sha256 = "{sha256}"; }}'
+
+
+def test_git_source(git_source: GitSource, url: str, rev: str, sha256: str):
+    assert git_source.url == url
+    assert git_source.sha256 == sha256
+    assert git_source.rev == rev
+
+
+def test_git_source_nix_expression(git_source: GitSource, url: str, rev: str, sha256: str):
+    assert git_source.get_nix_expression() == f'fetchgit {{ url = "{url}"; rev = "{rev}"; sha256 = "{sha256}"; }}'
+
+
+def test_license_github():
+    github_license = "MIT"
+    license = License.from_spdx_id(github_license)
+    assert license == License.MIT
+
+
+def test_license_gitlab():
+    gitlab_license = "mit"
+    license = License.from_spdx_id(gitlab_license)
+    assert license == License.MIT
diff --git a/pkgs/sources/update_vim_plugins/update_vim_plugins/tests/test_plugin.py b/pkgs/sources/update_vim_plugins/update_vim_plugins/tests/test_plugin.py
new file mode 100644
index 00000000..32377e24
--- /dev/null
+++ b/pkgs/sources/update_vim_plugins/update_vim_plugins/tests/test_plugin.py
@@ -0,0 +1,144 @@
+import json
+from typing import Callable
+
+import pytest
+from pytest_mock import MockFixture
+
+from update_vim_plugins.nix import License, UrlSource
+from update_vim_plugins.plugin import GitHubPlugin, VimPlugin
+from update_vim_plugins.spec import PluginSpec
+
+
+@pytest.fixture()
+def mock_source(sha256: str):
+    class MockSource:
+        def __init__(self, *args, **kwargs):
+            pass
+
+        def get_nix_expression(self):
+            return "src"
+
+    return MockSource()
+
+
+@pytest.fixture()
+def mock_plugin(mock_source):
+    class MockVimPlugin(VimPlugin):
+        def __init__(self):
+            self.name = "test"
+            self.version = "1.0.0"
+            self.source = mock_source
+            self.description = "No description"
+            self.homepage = "https://example.com"
+            self.license = License.UNKNOWN
+
+    return MockVimPlugin()
+
+
+def test_vim_plugin_nix_expression(mock_plugin):
+    assert (
+        mock_plugin.get_nix_expression()
+        == 'test = buildVimPluginFrom2Nix { pname = "test"; version = "1.0.0"; src = src; meta = with lib; { description = "No description"; homepage = "https://example.com"; license = with licenses; [  ]; }; };'
+    )
+
+
+class MockResponse:
+    def __init__(self, status_code: int, content: bytes):
+        self.status_code = status_code
+        self.content = content
+
+    def json(self):
+        return json.loads(self.content)
+
+
+def mock_request_get(repsonses: dict[str, MockResponse]):
+    respones_not_found = MockResponse(404, b'{"message": "Not Found"}')
+
+    def mock_get(url: str, *args, **kwargs):
+        return repsonses.get(url, respones_not_found)
+
+    return mock_get
+
+
+@pytest.fixture()
+def github_commits_response():
+    return MockResponse(
+        200,
+        json.dumps(
+            {
+                "sha": "6dcb09b5b57875f334f61aebed695e2e4193db5e",
+                "commit": {
+                    "committer": {
+                        "date": "2011-04-14T16:00:49Z",
+                    },
+                },
+            }
+        ),
+    )
+
+
+@pytest.fixture()
+def github_get(github_commits_response: MockResponse):
+    repos_response = MockResponse(
+        200,
+        json.dumps(
+            {
+                "html_url": "https://github.com/octocat/Hello-World",
+                "description": "This your first repo!",
+                "fork": False,
+                "default_branch": "master",
+                "license": {
+                    "spdx_id": "MIT",
+                },
+            }
+        ),
+    )
+    responses = {
+        "https://api.github.com/repos/octocat/Hello-World": repos_response,
+        "https://api.github.com/repos/octocat/Hello-World/commits/master": github_commits_response,
+    }
+    return mock_request_get(responses)
+
+
+@pytest.fixture()
+def github_get_no_license(github_commits_response: MockResponse):
+    repos_response = MockResponse(
+        200,
+        json.dumps(
+            {
+                "html_url": "https://github.com/octocat/Hello-World",
+                "description": "This your first repo!",
+                "fork": False,
+                "default_branch": "master",
+            }
+        ),
+    )
+    responses = {
+        "https://api.github.com/repos/octocat/Hello-World": repos_response,
+        "https://api.github.com/repos/octocat/Hello-World/commits/master": github_commits_response,
+    }
+    return mock_request_get(responses)
+
+
+def test_github_plugin(mocker: MockFixture, github_get: Callable, url_source: UrlSource):
+    mocker.patch("requests.get", github_get)
+    url_source = mocker.patch("update_vim_plugins.nix.UrlSource", url_source)
+
+    spec = PluginSpec.from_spec("octocat/Hello-World")
+    plugin = GitHubPlugin(spec)
+
+    assert plugin.name == "Hello-World"
+    assert plugin.version == "2011-04-14"
+    assert plugin.description == "This your first repo!"
+    assert plugin.homepage == "https://github.com/octocat/Hello-World"
+    assert plugin.license == License.MIT
+
+
+def test_github_plugin_no_license(mocker: MockFixture, github_get_no_license: Callable, url_source: UrlSource):
+    mocker.patch("requests.get", github_get_no_license)
+    url_source = mocker.patch("update_vim_plugins.nix.UrlSource", url_source)
+
+    spec = PluginSpec.from_spec("octocat/Hello-World")
+    plugin = GitHubPlugin(spec)
+
+    assert plugin.license == License.UNKNOWN
diff --git a/pkgs/sources/update_vim_plugins/update_vim_plugins/tests/test_spec.py b/pkgs/sources/update_vim_plugins/update_vim_plugins/tests/test_spec.py
new file mode 100644
index 00000000..2b9a1d24
--- /dev/null
+++ b/pkgs/sources/update_vim_plugins/update_vim_plugins/tests/test_spec.py
@@ -0,0 +1,136 @@
+import pytest
+
+from update_vim_plugins.spec import PluginSpec, RepositoryHost
+
+
+@pytest.fixture()
+def owner():
+    return "owner"
+
+
+@pytest.fixture()
+def repo():
+    return "repo.nvim"
+
+
+@pytest.fixture()
+def branch():
+    return "main"
+
+
+@pytest.fixture()
+def name():
+    return "repo-nvim"
+
+
+@pytest.fixture()
+def license():
+    return "mit"
+
+
+def test_from_spec_simple(owner: str, repo: str):
+    vim_plugin = PluginSpec.from_spec(f"{owner}/{repo}")
+
+    assert vim_plugin.owner == owner
+    assert vim_plugin.repo == repo
+
+
+def test_from_spec_with_gitref(owner: str, repo: str, branch: str):
+    vim_plugin = PluginSpec.from_spec(f"{owner}/{repo}:{branch}")
+
+    assert vim_plugin.branch == branch
+
+
+def test_from_spec_with_name(owner: str, repo: str, name: str):
+    vim_plugin = PluginSpec.from_spec(f"{owner}/{repo}::{name}")
+
+    assert vim_plugin.name == name
+
+
+@pytest.mark.parametrize("host", RepositoryHost)
+def test_from_spec_with_repository_host(owner: str, repo: str, host: RepositoryHost):
+    vim_plugin = PluginSpec.from_spec(f"{host.value}:{owner}/{repo}")
+
+    assert vim_plugin.repository_host == host
+
+
+def test_from_spec_without_repository_host(owner: str, repo: str):
+    vim_plugin = PluginSpec.from_spec(f"{owner}/{repo}")
+
+    assert vim_plugin.repository_host == RepositoryHost.GITHUB
+
+
+def test_from_spec_complex(owner: str, repo: str, branch: str, name: str):
+    vim_plugin = PluginSpec.from_spec(f"gitlab:{owner}/{repo}:{branch}:{name}")
+
+    assert vim_plugin.repository_host == RepositoryHost.GITLAB
+    assert vim_plugin.owner == owner
+    assert vim_plugin.repo == repo
+    assert vim_plugin.branch == branch
+    assert vim_plugin.name == name
+
+
+def test_from_spec_invalid_spec():
+    with pytest.raises(ValueError):
+        PluginSpec.from_spec("invalid_spec")
+
+
+def test_to_spec_simple(owner: str, repo: str):
+    vim_plugin = PluginSpec(RepositoryHost.GITHUB, owner, repo)
+
+    assert vim_plugin.to_spec() == f"{owner}/{repo}"
+
+
+def test_to_spec_with_branch(owner: str, repo: str, branch: str):
+    vim_plugin = PluginSpec(RepositoryHost.GITHUB, owner, repo, branch=branch)
+    assert vim_plugin.to_spec() == f"{owner}/{repo}:{branch}"
+
+
+def test_to_spec_with_name(owner: str, repo: str, name: str):
+    vim_plugin = PluginSpec(RepositoryHost.GITHUB, owner, repo, name=name)
+
+    assert vim_plugin.to_spec() == f"{owner}/{repo}::{name}"
+
+
+@pytest.mark.parametrize("host", [RepositoryHost.GITLAB, RepositoryHost.SOURCEHUT])
+def test_to_spec_with_repository_host(host: RepositoryHost, owner: str, repo: str):
+    vim_plugin = PluginSpec(host, owner, repo)
+
+    assert vim_plugin.to_spec() == f"{host.value}:{owner}/{repo}"
+
+
+def test_to_spec_complex(owner: str, repo: str, branch: str, name: str, license: str):
+    vim_plugin = PluginSpec(RepositoryHost.GITLAB, owner, repo, branch=branch, name=name, license=license)
+
+    assert vim_plugin.to_spec() == f"gitlab:{owner}/{repo}:{branch}:{name}:{license}"
+
+
+def test_spec_equal(owner: str, repo: str):
+    vim_plugin = PluginSpec(RepositoryHost.GITHUB, owner, repo)
+    vim_plugin2 = PluginSpec(RepositoryHost.GITHUB, owner, repo)
+
+    assert vim_plugin == vim_plugin2
+
+
+def test_spec_not_equal_different_branch(owner: str, repo: str):
+    vim_plugin = PluginSpec(RepositoryHost.GITHUB, owner, repo)
+    vim_plugin2 = PluginSpec(RepositoryHost.GITHUB, owner, repo, branch="main")
+
+    assert vim_plugin != vim_plugin2
+
+
+def test_spec_not_equal_different_name(owner: str, repo: str):
+    vim_plugin = PluginSpec(RepositoryHost.GITHUB, owner, repo)
+    vim_plugin2 = PluginSpec(RepositoryHost.GITHUB, owner, repo, name="renamed")
+
+    assert vim_plugin != vim_plugin2
+
+
+def test_spec_equal_same_normalized_name(owner: str):
+    repo = "repo.nvim"
+    name = "repo-nvim"
+
+    vim_plugin = PluginSpec(RepositoryHost.GITHUB, owner, repo)
+    vim_plugin2 = PluginSpec(RepositoryHost.GITHUB, owner, repo, name=name)
+
+    assert vim_plugin == vim_plugin2
diff --git a/pkgs/sources/update_vim_plugins/update_vim_plugins/update.py b/pkgs/sources/update_vim_plugins/update_vim_plugins/update.py
new file mode 100644
index 00000000..7eb3eeb4
--- /dev/null
+++ b/pkgs/sources/update_vim_plugins/update_vim_plugins/update.py
@@ -0,0 +1,212 @@
+import subprocess
+from random import shuffle
+from cleo.helpers import argument, option
+from cleo.commands.command import Command
+from concurrent.futures import ThreadPoolExecutor, as_completed
+
+from pprint import pprint
+
+from .plugin import plugin_from_spec
+
+from .helpers import read_manifest_to_spec, get_const
+from .helpers import JSON_FILE, PLUGINS_LIST_FILE, PKGS_FILE
+
+import json
+import jsonpickle
+
+jsonpickle.set_encoder_options("json", sort_keys=True)
+
+
+class UpdateCommand(Command):
+    name = "update"
+    description = "Generate nix module from input file"
+    arguments = [argument("plug_dir", description="Path to the plugin directory", optional=False)]
+    options = [
+        option("all", "a", description="Update all plugins. Else only update new plugins", flag=True),
+        option("dry-run", "d", description="Show which plugins would be updated", flag=True),
+    ]
+
+    def handle(self):
+        """Main command function"""
+
+        plug_dir = self.argument("plug_dir")
+        self.specs = read_manifest_to_spec(plug_dir)
+
+        if self.option("all"):
+            # update all plugins
+            spec_list = self.specs
+            known_plugins = []
+        else:
+            # filter plugins we already know
+            spec_list = self.specs
+
+            with open(get_const(JSON_FILE, plug_dir), "r") as json_file:
+                data = json.load(json_file)
+
+                known_specs = list(filter(lambda x: x.line in data, spec_list))
+                known_plugins = [jsonpickle.decode(data[x.line]) for x in known_specs]
+
+                spec_list = list(filter(lambda x: x.line not in data, spec_list))
+
+        if self.option("dry-run"):
+            self.line("<comment>These plugins would be updated</comment>")
+            pprint(spec_list)
+            self.line(f"<info>Total:</info> {len(spec_list)}")
+            exit(0)
+
+        processed_plugins, failed_plugins, failed_but_known = self.process_manifest(spec_list, plug_dir)
+
+        processed_plugins += known_plugins  # add plugins from .plugins.json
+        processed_plugins: list = sorted(set(processed_plugins))  # remove duplicates based only on source line
+
+        self.check_duplicates(processed_plugins)
+
+        if failed_plugins != []:
+            self.line("<error>Not processed:</error> The following plugins could not be updated")
+            for s, e in failed_plugins:
+                self.line(f" - {s!r} - {e}")
+
+        if failed_but_known != []:
+            self.line(
+                "<error>Not updated:</error> The following plugins could not be updated but an older version is known"
+            )
+            for s, e in failed_but_known:
+                self.line(f" - {s!r} - {e}")
+
+        # update plugin "database"
+        self.write_plugins_json(processed_plugins, plug_dir)
+
+        # generate output
+        self.write_plugins_nix(processed_plugins, plug_dir)
+
+        self.write_plugins_markdown(processed_plugins, plug_dir)
+
+        self.line("<comment>Done</comment>")
+
+    def write_plugins_markdown(self, plugins, plug_dir):
+        """Write the list of all plugins to PLUGINS_LIST_FILE in markdown"""
+
+        plugins.sort()
+
+        self.line("<info>Updating plugins.md</info>")
+
+        header = f" - Plugin count: {len(plugins)}\n\n| Repo | Last Update | Nix package name | Last checked |\n|:---|:---|:---|:---|\n"
+
+        with open(get_const(PLUGINS_LIST_FILE, plug_dir), "w") as file:
+            file.write(header)
+            for plugin in plugins:
+                file.write(f"{plugin.to_markdown()}\n")
+
+    def write_plugins_nix(self, plugins, plug_dir):
+        self.line("<info>Generating nix output</info>")
+
+        plugins.sort()
+
+        header = "{ lib, buildVimPlugin, fetchurl, fetchgit }: {"
+        footer = "}"
+
+        with open(get_const(PKGS_FILE, plug_dir), "w") as file:
+            file.write(header)
+            for plugin in plugins:
+                file.write(f"{plugin.to_nix()}\n")
+            file.write(footer)
+
+        self.line("<info>Formatting nix output</info>")
+
+        subprocess.run(
+            ["alejandra", get_const(PKGS_FILE, plug_dir)],
+            stdout=subprocess.DEVNULL,
+            stderr=subprocess.DEVNULL,
+        )
+
+    def write_plugins_json(self, plugins, plug_dir):
+        self.line("<info>Storing results in .plugins.json</info>")
+
+        plugins.sort()
+
+        with open(get_const(JSON_FILE, plug_dir), "r+") as json_file:
+            data = json.load(json_file)
+
+            for plugin in plugins:
+                data.update({f"{plugin.source_line}": plugin.to_json()})
+
+            json_file.seek(0)
+            json_file.write(json.dumps(data, indent=2, sort_keys=True))
+            json_file.truncate()
+
+    def check_duplicates(self, plugins):
+        """check for duplicates in proccesed_plugins"""
+        error = False
+        for i, plugin in enumerate(plugins):
+            for p in plugins[i + 1 :]:
+                if plugin.name == p.name:
+                    self.line(
+                        f"<error>Error:</error> The following two lines produce the same plugin name:\n - {plugin.source_line}\n - {p.source_line}\n -> {p.name}"
+                    )
+                    error = True
+
+        # We want to exit if the resulting nix file would be broken
+        # But we want to go through all plugins before we do so
+        if error:
+            exit(1)
+
+    def generate_plugin(self, spec, i, size, plug_dir):
+        debug_string = ""
+
+        processed_plugin = None
+        failed_but_known = None
+        failed_plugin = None
+        try:
+            debug_string += f" - <info>({i+1}/{size}) Processing</info> {spec!r}\n"
+            vim_plugin = plugin_from_spec(spec)
+            debug_string += f"   • <comment>Success</comment> {vim_plugin!r}\n"
+            processed_plugin = vim_plugin
+        except Exception as e:
+            debug_string += f"   • <error>Error:</error> Could not update <info>{spec.name}</info>. Keeping old values. Reason: {e}\n"
+            with open(get_const(JSON_FILE, plug_dir), "r") as json_file:
+                data = json.load(json_file)
+
+            plugin_json = data.get(spec.line)
+            if plugin_json:
+                vim_plugin = jsonpickle.decode(plugin_json)
+                processed_plugin = vim_plugin
+                failed_but_known = (vim_plugin, e)
+            else:
+                debug_string += f"   • <error>Error:</error> No entries for <info>{spec.name}</info> in '.plugins.json'. Skipping...\n"
+                failed_plugin = (spec, e)
+
+        self.line(debug_string.strip())
+
+        return processed_plugin, failed_plugin, failed_but_known
+
+    def process_manifest(self, spec_list, plug_dir):
+        """Read specs in 'spec_list' and generate plugins"""
+
+        size = len(spec_list)
+
+        # We have to assume that we will reach an api limit. Therefore
+        # we randomize the spec list to give every entry the same change to be updated and
+        # not favor those at the start of the list
+        shuffle(spec_list)
+
+        with ThreadPoolExecutor() as executor:
+            futures = [
+                executor.submit(self.generate_plugin, spec, i, size, plug_dir) for i, spec in enumerate(spec_list)
+            ]
+            results = [future.result() for future in as_completed(futures)]
+
+        processed_plugins = [r[0] for r in results]
+        failed_plugins = [r[1] for r in results]
+        failed_but_known = [r[2] for r in results]
+
+        processed_plugins = list(filter(lambda x: x is not None, processed_plugins))
+        failed_plugins = list(filter(lambda x: x is not None, failed_plugins))
+        failed_but_known = list(filter(lambda x: x is not None, failed_but_known))
+
+        processed_plugins.sort()
+        failed_plugins.sort()
+        failed_but_known.sort()
+
+        assert len(processed_plugins) == len(spec_list) - len(failed_plugins)
+
+        return processed_plugins, failed_plugins, failed_but_known
diff --git a/pkgs/sources/yt/.env b/pkgs/sources/yt/.env
new file mode 100755
index 00000000..8018a738
--- /dev/null
+++ b/pkgs/sources/yt/.env
@@ -0,0 +1,3 @@
+#!/usr/bin/env sh
+
+PATH="$(pwd)/target/release/:$(pwd)/target/debug/:$PATH"
diff --git a/pkgs/sources/yt/.envrc b/pkgs/sources/yt/.envrc
new file mode 100644
index 00000000..3550a30f
--- /dev/null
+++ b/pkgs/sources/yt/.envrc
@@ -0,0 +1 @@
+use flake
diff --git a/pkgs/sources/yt/.gitignore b/pkgs/sources/yt/.gitignore
new file mode 100644
index 00000000..c84fa049
--- /dev/null
+++ b/pkgs/sources/yt/.gitignore
@@ -0,0 +1,3 @@
+# build dirs
+/target
+/result
diff --git a/pkgs/sources/yt/Cargo.lock b/pkgs/sources/yt/Cargo.lock
new file mode 100644
index 00000000..ef2a53fd
--- /dev/null
+++ b/pkgs/sources/yt/Cargo.lock
@@ -0,0 +1,640 @@
+# This file is automatically @generated by Cargo.
+# It is not intended for manual editing.
+version = 3
+
+[[package]]
+name = "android-tzdata"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0"
+
+[[package]]
+name = "android_system_properties"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311"
+dependencies = [
+ "libc",
+]
+
+[[package]]
+name = "anstream"
+version = "0.6.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "418c75fa768af9c03be99d17643f93f79bbba589895012a80e3452a19ddda15b"
+dependencies = [
+ "anstyle",
+ "anstyle-parse",
+ "anstyle-query",
+ "anstyle-wincon",
+ "colorchoice",
+ "is_terminal_polyfill",
+ "utf8parse",
+]
+
+[[package]]
+name = "anstyle"
+version = "1.0.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "038dfcf04a5feb68e9c60b21c9625a54c2c0616e79b72b0fd87075a056ae1d1b"
+
+[[package]]
+name = "anstyle-parse"
+version = "0.2.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c03a11a9034d92058ceb6ee011ce58af4a9bf61491aa7e1e59ecd24bd40d22d4"
+dependencies = [
+ "utf8parse",
+]
+
+[[package]]
+name = "anstyle-query"
+version = "1.0.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a64c907d4e79225ac72e2a354c9ce84d50ebb4586dee56c82b3ee73004f537f5"
+dependencies = [
+ "windows-sys",
+]
+
+[[package]]
+name = "anstyle-wincon"
+version = "3.0.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "61a38449feb7068f52bb06c12759005cf459ee52bb4adc1d5a7c4322d716fb19"
+dependencies = [
+ "anstyle",
+ "windows-sys",
+]
+
+[[package]]
+name = "anyhow"
+version = "1.0.83"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "25bdb32cbbdce2b519a9cd7df3a678443100e265d5e25ca763b7572a5104f5f3"
+
+[[package]]
+name = "autocfg"
+version = "1.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0c4b4d0bd25bd0b74681c0ad21497610ce1b7c91b1022cd21c80c6fbdd9476b0"
+
+[[package]]
+name = "bitflags"
+version = "2.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cf4b9d6a944f767f8e5e0db018570623c85f3d925ac718db4e06d0187adb21c1"
+
+[[package]]
+name = "bumpalo"
+version = "3.16.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c"
+
+[[package]]
+name = "cc"
+version = "1.0.97"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "099a5357d84c4c61eb35fc8eafa9a79a902c2f76911e5747ced4e032edd8d9b4"
+
+[[package]]
+name = "cfg-if"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
+
+[[package]]
+name = "chrono"
+version = "0.4.38"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a21f936df1771bf62b77f047b726c4625ff2e8aa607c01ec06e5a05bd8463401"
+dependencies = [
+ "android-tzdata",
+ "iana-time-zone",
+ "js-sys",
+ "num-traits",
+ "wasm-bindgen",
+ "windows-targets",
+]
+
+[[package]]
+name = "clap"
+version = "4.5.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "90bc066a67923782aa8515dbaea16946c5bcc5addbd668bb80af688e53e548a0"
+dependencies = [
+ "clap_builder",
+ "clap_derive",
+]
+
+[[package]]
+name = "clap_builder"
+version = "4.5.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ae129e2e766ae0ec03484e609954119f123cc1fe650337e155d03b022f24f7b4"
+dependencies = [
+ "anstream",
+ "anstyle",
+ "clap_lex",
+ "strsim",
+]
+
+[[package]]
+name = "clap_derive"
+version = "4.5.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "528131438037fd55894f62d6e9f068b8f45ac57ffa77517819645d10aed04f64"
+dependencies = [
+ "heck",
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "clap_lex"
+version = "0.7.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "98cc8fbded0c607b7ba9dd60cd98df59af97e84d24e49c8557331cfc26d301ce"
+
+[[package]]
+name = "cli-log"
+version = "2.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3d2ab00dc4c82ec28af25ac085aecc11ffeabf353755715a3113a7aa044ca5cc"
+dependencies = [
+ "chrono",
+ "file-size",
+ "log",
+ "proc-status",
+]
+
+[[package]]
+name = "colorchoice"
+version = "1.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0b6a852b24ab71dffc585bcb46eaf7959d175cb865a7152e35b348d1b2960422"
+
+[[package]]
+name = "core-foundation-sys"
+version = "0.8.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "06ea2b9bc92be3c2baa9334a323ebca2d6f074ff852cd1d7b11064035cd3868f"
+
+[[package]]
+name = "errno"
+version = "0.3.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "534c5cf6194dfab3db3242765c03bbe257cf92f22b38f6bc0c58d59108a820ba"
+dependencies = [
+ "libc",
+ "windows-sys",
+]
+
+[[package]]
+name = "fastrand"
+version = "2.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9fc0510504f03c51ada170672ac806f1f105a88aa97a5281117e1ddc3368e51a"
+
+[[package]]
+name = "file-size"
+version = "1.0.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9544f10105d33957765016b8a9baea7e689bf1f0f2f32c2fa2f568770c38d2b3"
+
+[[package]]
+name = "form_urlencoded"
+version = "1.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456"
+dependencies = [
+ "percent-encoding",
+]
+
+[[package]]
+name = "heck"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
+
+[[package]]
+name = "iana-time-zone"
+version = "0.1.60"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e7ffbb5a1b541ea2561f8c41c087286cc091e21e556a4f09a8f6cbf17b69b141"
+dependencies = [
+ "android_system_properties",
+ "core-foundation-sys",
+ "iana-time-zone-haiku",
+ "js-sys",
+ "wasm-bindgen",
+ "windows-core",
+]
+
+[[package]]
+name = "iana-time-zone-haiku"
+version = "0.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f"
+dependencies = [
+ "cc",
+]
+
+[[package]]
+name = "idna"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "634d9b1461af396cad843f47fdba5597a4f9e6ddd4bfb6ff5d85028c25cb12f6"
+dependencies = [
+ "unicode-bidi",
+ "unicode-normalization",
+]
+
+[[package]]
+name = "is_terminal_polyfill"
+version = "1.70.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f8478577c03552c21db0e2724ffb8986a5ce7af88107e6be5d2ee6e158c12800"
+
+[[package]]
+name = "itoa"
+version = "1.0.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b"
+
+[[package]]
+name = "js-sys"
+version = "0.3.69"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "29c15563dc2726973df627357ce0c9ddddbea194836909d655df6a75d2cf296d"
+dependencies = [
+ "wasm-bindgen",
+]
+
+[[package]]
+name = "libc"
+version = "0.2.154"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ae743338b92ff9146ce83992f766a31066a91a8c84a45e0e9f21e7cf6de6d346"
+
+[[package]]
+name = "linux-raw-sys"
+version = "0.4.13"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "01cda141df6706de531b6c46c3a33ecca755538219bd484262fa09410c13539c"
+
+[[package]]
+name = "log"
+version = "0.4.21"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "90ed8c1e510134f979dbc4f070f87d4313098b704861a105fe34231c70a3901c"
+
+[[package]]
+name = "num-traits"
+version = "0.2.19"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841"
+dependencies = [
+ "autocfg",
+]
+
+[[package]]
+name = "once_cell"
+version = "1.19.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92"
+
+[[package]]
+name = "percent-encoding"
+version = "2.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e"
+
+[[package]]
+name = "proc-macro2"
+version = "1.0.82"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8ad3d49ab951a01fbaafe34f2ec74122942fe18a3f9814c3268f1bb72042131b"
+dependencies = [
+ "unicode-ident",
+]
+
+[[package]]
+name = "proc-status"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f0e0c0ac915e7b76b47850ba4ffc377abde6c6ff9eeace61d0a89623db449712"
+dependencies = [
+ "thiserror",
+]
+
+[[package]]
+name = "quote"
+version = "1.0.36"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0fa76aaf39101c457836aec0ce2316dbdc3ab723cdda1c6bd4e6ad4208acaca7"
+dependencies = [
+ "proc-macro2",
+]
+
+[[package]]
+name = "rustix"
+version = "0.38.34"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "70dc5ec042f7a43c4a73241207cecc9873a06d45debb38b329f8541d85c2730f"
+dependencies = [
+ "bitflags",
+ "errno",
+ "libc",
+ "linux-raw-sys",
+ "windows-sys",
+]
+
+[[package]]
+name = "ryu"
+version = "1.0.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f"
+
+[[package]]
+name = "serde"
+version = "1.0.201"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "780f1cebed1629e4753a1a38a3c72d30b97ec044f0aef68cb26650a3c5cf363c"
+dependencies = [
+ "serde_derive",
+]
+
+[[package]]
+name = "serde_derive"
+version = "1.0.201"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c5e405930b9796f1c00bee880d03fc7e0bb4b9a11afc776885ffe84320da2865"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "serde_json"
+version = "1.0.117"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "455182ea6142b14f93f4bc5320a2b31c1f266b66a4a5c858b013302a5d8cbfc3"
+dependencies = [
+ "itoa",
+ "ryu",
+ "serde",
+]
+
+[[package]]
+name = "strsim"
+version = "0.11.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
+
+[[package]]
+name = "syn"
+version = "2.0.61"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c993ed8ccba56ae856363b1845da7266a7cb78e1d146c8a32d54b45a8b831fc9"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "unicode-ident",
+]
+
+[[package]]
+name = "tempfile"
+version = "3.10.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "85b77fafb263dd9d05cbeac119526425676db3784113aa9295c88498cbf8bff1"
+dependencies = [
+ "cfg-if",
+ "fastrand",
+ "rustix",
+ "windows-sys",
+]
+
+[[package]]
+name = "thiserror"
+version = "1.0.60"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "579e9083ca58dd9dcf91a9923bb9054071b9ebbd800b342194c9feb0ee89fc18"
+dependencies = [
+ "thiserror-impl",
+]
+
+[[package]]
+name = "thiserror-impl"
+version = "1.0.60"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e2470041c06ec3ac1ab38d0356a6119054dedaea53e12fbefc0de730a1c08524"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "tinyvec"
+version = "1.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "87cc5ceb3875bb20c2890005a4e226a4651264a5c75edb2421b52861a0a0cb50"
+dependencies = [
+ "tinyvec_macros",
+]
+
+[[package]]
+name = "tinyvec_macros"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
+
+[[package]]
+name = "unicode-bidi"
+version = "0.3.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "08f95100a766bf4f8f28f90d77e0a5461bbdb219042e7679bebe79004fed8d75"
+
+[[package]]
+name = "unicode-ident"
+version = "1.0.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b"
+
+[[package]]
+name = "unicode-normalization"
+version = "0.1.23"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a56d1686db2308d901306f92a263857ef59ea39678a5458e7cb17f01415101f5"
+dependencies = [
+ "tinyvec",
+]
+
+[[package]]
+name = "url"
+version = "2.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "31e6302e3bb753d46e83516cae55ae196fc0c309407cf11ab35cc51a4c2a4633"
+dependencies = [
+ "form_urlencoded",
+ "idna",
+ "percent-encoding",
+]
+
+[[package]]
+name = "utf8parse"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a"
+
+[[package]]
+name = "wasm-bindgen"
+version = "0.2.92"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4be2531df63900aeb2bca0daaaddec08491ee64ceecbee5076636a3b026795a8"
+dependencies = [
+ "cfg-if",
+ "wasm-bindgen-macro",
+]
+
+[[package]]
+name = "wasm-bindgen-backend"
+version = "0.2.92"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "614d787b966d3989fa7bb98a654e369c762374fd3213d212cfc0251257e747da"
+dependencies = [
+ "bumpalo",
+ "log",
+ "once_cell",
+ "proc-macro2",
+ "quote",
+ "syn",
+ "wasm-bindgen-shared",
+]
+
+[[package]]
+name = "wasm-bindgen-macro"
+version = "0.2.92"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a1f8823de937b71b9460c0c34e25f3da88250760bec0ebac694b49997550d726"
+dependencies = [
+ "quote",
+ "wasm-bindgen-macro-support",
+]
+
+[[package]]
+name = "wasm-bindgen-macro-support"
+version = "0.2.92"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e94f17b526d0a461a191c78ea52bbce64071ed5c04c9ffe424dcb38f74171bb7"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+ "wasm-bindgen-backend",
+ "wasm-bindgen-shared",
+]
+
+[[package]]
+name = "wasm-bindgen-shared"
+version = "0.2.92"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "af190c94f2773fdb3729c55b007a722abb5384da03bc0986df4c289bf5567e96"
+
+[[package]]
+name = "windows-core"
+version = "0.52.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9"
+dependencies = [
+ "windows-targets",
+]
+
+[[package]]
+name = "windows-sys"
+version = "0.52.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d"
+dependencies = [
+ "windows-targets",
+]
+
+[[package]]
+name = "windows-targets"
+version = "0.52.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6f0713a46559409d202e70e28227288446bf7841d3211583a4b53e3f6d96e7eb"
+dependencies = [
+ "windows_aarch64_gnullvm",
+ "windows_aarch64_msvc",
+ "windows_i686_gnu",
+ "windows_i686_gnullvm",
+ "windows_i686_msvc",
+ "windows_x86_64_gnu",
+ "windows_x86_64_gnullvm",
+ "windows_x86_64_msvc",
+]
+
+[[package]]
+name = "windows_aarch64_gnullvm"
+version = "0.52.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7088eed71e8b8dda258ecc8bac5fb1153c5cffaf2578fc8ff5d61e23578d3263"
+
+[[package]]
+name = "windows_aarch64_msvc"
+version = "0.52.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9985fd1504e250c615ca5f281c3f7a6da76213ebd5ccc9561496568a2752afb6"
+
+[[package]]
+name = "windows_i686_gnu"
+version = "0.52.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "88ba073cf16d5372720ec942a8ccbf61626074c6d4dd2e745299726ce8b89670"
+
+[[package]]
+name = "windows_i686_gnullvm"
+version = "0.52.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "87f4261229030a858f36b459e748ae97545d6f1ec60e5e0d6a3d32e0dc232ee9"
+
+[[package]]
+name = "windows_i686_msvc"
+version = "0.52.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "db3c2bf3d13d5b658be73463284eaf12830ac9a26a90c717b7f771dfe97487bf"
+
+[[package]]
+name = "windows_x86_64_gnu"
+version = "0.52.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4e4246f76bdeff09eb48875a0fd3e2af6aada79d409d33011886d3e1581517d9"
+
+[[package]]
+name = "windows_x86_64_gnullvm"
+version = "0.52.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "852298e482cd67c356ddd9570386e2862b5673c85bd5f88df9ab6802b334c596"
+
+[[package]]
+name = "windows_x86_64_msvc"
+version = "0.52.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bec47e5bfd1bff0eeaf6d8b485cc1074891a197ab4225d504cb7a1ab88b02bf0"
+
+[[package]]
+name = "yt"
+version = "0.1.0"
+dependencies = [
+ "anyhow",
+ "clap",
+ "cli-log",
+ "log",
+ "serde",
+ "serde_json",
+ "tempfile",
+ "url",
+]
diff --git a/pkgs/sources/yt/Cargo.toml b/pkgs/sources/yt/Cargo.toml
new file mode 100644
index 00000000..7c17d20b
--- /dev/null
+++ b/pkgs/sources/yt/Cargo.toml
@@ -0,0 +1,35 @@
+[package]
+name = "yt"
+version = "0.1.0"
+edition = "2021"
+
+# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
+
+[dependencies]
+anyhow = "1.0.83"
+clap = { version = "4.5.4", features = ["derive"] }
+cli-log = "2.0.0"
+log = "0.4.21"
+serde = { version = "1.0.201", features = ["derive"] }
+serde_json = "1.0.117"
+tempfile = "3.10.1"
+url = "2.5.0"
+
+# This is here to be able to tell nix which binary to build
+[features]
+yts = []
+ytc = []
+yt = []
+default = ["yt", "yts", "ytc"]
+
+[[bin]]
+name = "yts"
+required-features = ["yts"]
+
+[[bin]]
+name = "ytc"
+required-features = ["ytc"]
+
+[[bin]]
+name = "yt"
+required-features = ["yt"]
diff --git a/pkgs/sources/yt/default.nix b/pkgs/sources/yt/default.nix
new file mode 100644
index 00000000..32396051
--- /dev/null
+++ b/pkgs/sources/yt/default.nix
@@ -0,0 +1,51 @@
+[
+  (
+    final: prev: {
+      yt = import ./yt.nix {
+        inherit
+          (prev)
+          lib
+          makeWrapper
+          rustPlatform
+          # dependencies
+          
+          ytcc
+          yt-dlp
+          mpv
+          ;
+      };
+    }
+  )
+  (
+    final: prev: {
+      yts = import ./yts.nix {
+        inherit
+          (prev)
+          lib
+          makeWrapper
+          rustPlatform
+          # dependencies
+          
+          ytcc
+          ;
+      };
+    }
+  )
+  (
+    final: prev: {
+      ytc = import ./ytc.nix {
+        inherit
+          (prev)
+          lib
+          makeWrapper
+          rustPlatform
+          # dependencies
+          
+          ytcc
+          yt-dlp
+          mpv
+          ;
+      };
+    }
+  )
+]
diff --git a/pkgs/sources/yt/flake.lock b/pkgs/sources/yt/flake.lock
new file mode 100644
index 00000000..50494465
--- /dev/null
+++ b/pkgs/sources/yt/flake.lock
@@ -0,0 +1,61 @@
+{
+  "nodes": {
+    "flake-utils": {
+      "inputs": {
+        "systems": "systems"
+      },
+      "locked": {
+        "lastModified": 1710146030,
+        "narHash": "sha256-SZ5L6eA7HJ/nmkzGG7/ISclqe6oZdOZTNoesiInkXPQ=",
+        "owner": "numtide",
+        "repo": "flake-utils",
+        "rev": "b1d9ab70662946ef0850d488da1c9019f3a9752a",
+        "type": "github"
+      },
+      "original": {
+        "owner": "numtide",
+        "repo": "flake-utils",
+        "type": "github"
+      }
+    },
+    "nixpkgs": {
+      "locked": {
+        "lastModified": 1715087517,
+        "narHash": "sha256-CLU5Tsg24Ke4+7sH8azHWXKd0CFd4mhLWfhYgUiDBpQ=",
+        "owner": "NixOS",
+        "repo": "nixpkgs",
+        "rev": "b211b392b8486ee79df6cdfb1157ad2133427a29",
+        "type": "github"
+      },
+      "original": {
+        "owner": "NixOS",
+        "ref": "nixos-unstable",
+        "repo": "nixpkgs",
+        "type": "github"
+      }
+    },
+    "root": {
+      "inputs": {
+        "flake-utils": "flake-utils",
+        "nixpkgs": "nixpkgs"
+      }
+    },
+    "systems": {
+      "locked": {
+        "lastModified": 1681028828,
+        "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
+        "owner": "nix-systems",
+        "repo": "default",
+        "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
+        "type": "github"
+      },
+      "original": {
+        "owner": "nix-systems",
+        "repo": "default",
+        "type": "github"
+      }
+    }
+  },
+  "root": "root",
+  "version": 7
+}
diff --git a/pkgs/sources/yt/flake.nix b/pkgs/sources/yt/flake.nix
new file mode 100644
index 00000000..561b1c0d
--- /dev/null
+++ b/pkgs/sources/yt/flake.nix
@@ -0,0 +1,30 @@
+{
+  description = "yt";
+
+  inputs = {
+    nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
+
+    flake-utils.url = "github:numtide/flake-utils";
+  };
+
+  outputs = {
+    self,
+    nixpkgs,
+    flake-utils,
+  }: (flake-utils.lib.eachDefaultSystem (system: let
+    pkgs = nixpkgs.legacyPackages."${system}";
+  in {
+    devShells.default = pkgs.mkShell {
+      packages = with pkgs; [
+        # rust stuff
+        cargo
+        clippy
+        rustc
+        rustfmt
+
+        cargo-edit
+        cargo-expand
+      ];
+    };
+  }));
+}
diff --git a/pkgs/sources/yt/src/bin/yt/main.rs b/pkgs/sources/yt/src/bin/yt/main.rs
new file mode 100644
index 00000000..37348834
--- /dev/null
+++ b/pkgs/sources/yt/src/bin/yt/main.rs
@@ -0,0 +1,91 @@
+use anyhow::{bail, Context, Result};
+use std::{
+    env, fs,
+    io::{BufRead, BufReader, BufWriter, Write},
+    process::Command as StdCmd,
+};
+use tempfile::Builder;
+use yt::{
+    constants::{last_select, HELP_STR},
+    downloader::Downloader,
+    filter_line, YtccListData,
+};
+
+fn main() -> Result<()> {
+    cli_log::init_cli_log!();
+
+    let json_map = {
+        let mut ytcc = StdCmd::new("ytcc");
+        ytcc.args([
+            "--output",
+            "json",
+            "list",
+            "--order-by",
+            "publish_date",
+            "desc",
+        ]);
+
+        serde_json::from_slice::<Vec<YtccListData>>(
+            &ytcc.output().context("Failed to json from ytcc")?.stdout,
+        )
+        .context("Failed to deserialize json output")?
+    };
+
+    let temp_file = Builder::new()
+        .prefix("yt_video_select-")
+        .suffix(".yts")
+        .rand_bytes(6)
+        .tempfile()
+        .context("Failed to get tempfile")?;
+
+    {
+        let mut edit_file = BufWriter::new(&temp_file);
+
+        json_map.iter().for_each(|line| {
+            let line = line.to_string();
+            edit_file
+                .write_all(line.as_bytes())
+                .expect("This write should not fail");
+        });
+
+        edit_file.write_all(HELP_STR.as_bytes())?;
+        edit_file.flush().context("Failed to flush edit file")?;
+
+        let mut nvim = StdCmd::new("nvim");
+        nvim.arg(temp_file.path());
+        let status = nvim.status().context("Falied to run nvim")?;
+        if !status.success() {
+            bail!("nvim exited with error status: {}", status)
+        }
+    }
+
+    let read_file = temp_file.reopen()?;
+    fs::copy(
+        temp_file.path(),
+        last_select().context("Failed to get the persistent selection file path")?,
+    )
+    .context("Failed to persist selection file")?;
+
+    let mut watching = Vec::new();
+    let reader = BufReader::new(&read_file);
+    for line in reader.lines() {
+        let line = line.context("Failed to read line")?;
+
+        if let Some(downloadable) =
+            filter_line(&line).with_context(|| format!("Failed to process line: '{}'", line))?
+        {
+            watching.push(downloadable);
+        }
+    }
+
+    if watching.is_empty() {
+        return Ok(());
+    }
+
+    let downloader = Downloader::new(watching).context("Failed to construct downloader")?;
+    downloader
+        .consume()
+        .context("Failed to consume downloader")?;
+
+    Ok(())
+}
diff --git a/pkgs/sources/yt/src/bin/ytc/args.rs b/pkgs/sources/yt/src/bin/ytc/args.rs
new file mode 100644
index 00000000..8b2d6a61
--- /dev/null
+++ b/pkgs/sources/yt/src/bin/ytc/args.rs
@@ -0,0 +1,26 @@
+use clap::{Parser, Subcommand};
+/// A helper for downloading and playing youtube videos
+#[derive(Parser, Debug)]
+#[clap(author, version, about, long_about = None)]
+pub struct Args {
+    #[command(subcommand)]
+    /// The subcommand to execute
+    pub subcommand: Command,
+}
+#[derive(Subcommand, Debug)]
+pub enum Command {
+    #[clap(value_parser)]
+    /// Work based of ytcc ids
+    Id {
+        #[clap(value_parser)]
+        /// A list of ids to play
+        ids: Vec<u32>,
+    },
+    #[clap(value_parser)]
+    /// Work based of raw youtube urls
+    Url {
+        #[clap(value_parser)]
+        /// A list of urls to play
+        urls: Vec<String>,
+    },
+}
diff --git a/pkgs/sources/yt/src/bin/ytc/main.rs b/pkgs/sources/yt/src/bin/ytc/main.rs
new file mode 100644
index 00000000..b38157df
--- /dev/null
+++ b/pkgs/sources/yt/src/bin/ytc/main.rs
@@ -0,0 +1,77 @@
+use std::{env, process::Command as StdCmd};
+
+use anyhow::{bail, Context, Result};
+use clap::Parser;
+use log::debug;
+use url::Url;
+use yt::{
+    downloader::{Downloadable, Downloader},
+    YtccListData,
+};
+
+use crate::args::{Args, Command};
+
+mod args;
+
+fn main() -> Result<()> {
+    let args = Args::parse();
+    cli_log::init_cli_log!();
+
+    let playspec: Vec<Downloadable> = match args.subcommand {
+        Command::Id { ids } => {
+            let mut output = Vec::with_capacity(ids.len());
+            for id in ids {
+                debug!("Adding {}", id);
+                let mut ytcc = StdCmd::new("ytcc");
+                ytcc.args([
+                    "--output",
+                    "json",
+                    "list",
+                    "--watched",
+                    "--unwatched",
+                    "--attributes",
+                    "url",
+                    "--ids",
+                    id.to_string().as_str(),
+                ]);
+                let json = serde_json::from_slice::<Vec<YtccListData>>(
+                    &ytcc.output().context("Failed to get url from id")?.stdout,
+                )
+                .context("Failed to deserialize json output")?;
+
+                if json.is_empty() {
+                    bail!("Could not find a video with id: {}", id);
+                }
+                assert_eq!(json.len(), 1);
+                let json = json.first().expect("Has only one element");
+
+                debug!("Id resolved to: '{}'", &json.url);
+
+                output.push(Downloadable {
+                    url: Url::parse(&json.url)?,
+                    id: Some(json.id),
+                })
+            }
+            output
+        }
+        Command::Url { urls } => {
+            let mut output = Vec::with_capacity(urls.len());
+            for url in urls {
+                output.push(Downloadable {
+                    url: Url::parse(&url).context("Failed to parse url")?,
+                    id: None,
+                })
+            }
+            output
+        }
+    };
+
+    debug!("Initializing downloader");
+    let downloader = Downloader::new(playspec)?;
+
+    downloader
+        .consume()
+        .context("Failed to consume downloader")?;
+
+    Ok(())
+}
diff --git a/pkgs/sources/yt/src/bin/yts/args.rs b/pkgs/sources/yt/src/bin/yts/args.rs
new file mode 100644
index 00000000..56989421
--- /dev/null
+++ b/pkgs/sources/yt/src/bin/yts/args.rs
@@ -0,0 +1,41 @@
+use clap::{Parser, Subcommand};
+/// A helper for selecting which videos to download from ytcc to ytc
+#[derive(Parser, Debug)]
+#[clap(author, version, about, long_about = None)]
+pub struct Args {
+    #[command(subcommand)]
+    /// subcommand to execute
+    pub subcommand: Option<Command>,
+}
+
+#[derive(Subcommand, Debug)]
+pub enum Command {
+    #[clap(value_parser)]
+    /// Which ordering to use
+    Order {
+        #[command(subcommand)]
+        command: OrderCommand,
+    },
+}
+
+#[derive(Subcommand, Debug)]
+pub enum OrderCommand {
+    #[clap(value_parser)]
+    /// Order by date
+    #[group(required = true)]
+    Date {
+        #[arg(value_parser)]
+        /// Order descending
+        desc: bool,
+        #[clap(value_parser)]
+        /// Order ascending
+        asc: bool,
+    },
+    #[clap(value_parser)]
+    /// Pass a raw SQL 'ORDER BY' value
+    Raw {
+        #[clap(value_parser)]
+        /// The raw value(s) to pass
+        value: Vec<String>,
+    },
+}
diff --git a/pkgs/sources/yt/src/bin/yts/main.rs b/pkgs/sources/yt/src/bin/yts/main.rs
new file mode 100644
index 00000000..7398db61
--- /dev/null
+++ b/pkgs/sources/yt/src/bin/yts/main.rs
@@ -0,0 +1,91 @@
+use anyhow::{bail, Context, Result};
+use clap::Parser;
+use std::{
+    env,
+    io::{BufRead, BufReader, Write},
+    process::Command as StdCmd,
+};
+use tempfile::NamedTempFile;
+use yt::{constants::HELP_STR, filter_line, YtccListData};
+
+use crate::args::{Args, Command, OrderCommand};
+
+mod args;
+
+fn main() -> Result<()> {
+    let args = Args::parse();
+    cli_log::init_cli_log!();
+
+    let ordering = match args.subcommand.unwrap_or(Command::Order {
+        command: OrderCommand::Date {
+            desc: true,
+            asc: false,
+        },
+    }) {
+        Command::Order { command } => match command {
+            OrderCommand::Date { desc, asc } => {
+                if desc {
+                    vec!["--order-by".into(), "publish_date".into(), "desc".into()]
+                } else if asc {
+                    vec!["--order-by".into(), "publish_date".into(), "asc".into()]
+                } else {
+                    vec!["--order-by".into(), "publish_date".into(), "desc".into()]
+                }
+            }
+            OrderCommand::Raw { value } => [vec!["--order-by".into()], value].concat(),
+        },
+    };
+
+    let json_map = {
+        let mut ytcc = StdCmd::new("ytcc");
+        ytcc.args(["--output", "json", "list"]);
+        ytcc.args(ordering);
+
+        serde_json::from_slice::<Vec<YtccListData>>(
+            &ytcc.output().context("Failed to json from ytcc")?.stdout,
+        )
+        .context("Failed to deserialize json output")?
+    };
+
+    let mut edit_file = NamedTempFile::new().context("Failed to get tempfile")?;
+
+    json_map.iter().for_each(|line| {
+        let line = line.to_string();
+        edit_file
+            .write_all(line.as_bytes())
+            .expect("This write should not fail");
+    });
+
+    write!(&edit_file, "{}", HELP_STR)?;
+    edit_file.flush().context("Failed to flush edit file")?;
+
+    let read_file = edit_file.reopen()?;
+
+    let mut nvim = StdCmd::new("nvim");
+    nvim.arg(edit_file.path());
+
+    let status = nvim.status().context("Falied to run nvim")?;
+    if !status.success() {
+        bail!("Nvim exited with error status: {}", status)
+    }
+
+    let mut watching = Vec::new();
+    let reader = BufReader::new(&read_file);
+    for line in reader.lines() {
+        let line = line.context("Failed to read line")?;
+
+        if let Some(downloadable) =
+            filter_line(&line).with_context(|| format!("Failed to process line: '{}'", line))?
+        {
+            watching.push(downloadable);
+        }
+    }
+
+    let watching: String = watching
+        .iter()
+        .map(|d| d.to_string())
+        .collect::<Vec<String>>()
+        .join("\n");
+    println!("{}", &watching);
+    Ok(())
+}
diff --git a/pkgs/sources/yt/src/constants.rs b/pkgs/sources/yt/src/constants.rs
new file mode 100644
index 00000000..5e233656
--- /dev/null
+++ b/pkgs/sources/yt/src/constants.rs
@@ -0,0 +1,51 @@
+use std::{env, fs, path::PathBuf};
+
+pub const HELP_STR: &str = include_str!("./help.str");
+
+pub const YT_DLP_FLAGS: [&str; 13] = [
+    // Ignore errors arising of unavailable sponsor block API
+    "--ignore-errors",
+    "--format",
+    "bestvideo[height<=?1080]+bestaudio/best",
+    "--embed-chapters",
+    "--progress",
+    "--write-comments",
+    "--extractor-args",
+    "youtube:max_comments=150,all,100;comment_sort=top",
+    "--write-info-json",
+    "--sponsorblock-mark",
+    "default",
+    "--sponsorblock-remove",
+    "sponsor",
+];
+pub const MPV_FLAGS: [&str; 4] = [
+    "--speed=2.7",
+    "--volume=75",
+    "--keep-open=yes",
+    "--msg-level=osd/libass=fatal",
+];
+
+pub const CONCURRENT: u32 = 5;
+
+pub const DOWNLOAD_DIR: &str = "/tmp/ytcc";
+
+fn get_runtime_path(component: &'static str) -> anyhow::Result<PathBuf> {
+    let out: PathBuf = format!(
+        "{}/{}",
+        env::var("XDG_RUNTIME_DIR").expect("This should always exist"),
+        component
+    )
+    .into();
+    fs::create_dir_all(out.parent().expect("Parent should exist"))?;
+    Ok(out)
+}
+
+const STATUS_PATH: &str = "ytcc/running";
+pub fn status_path() -> anyhow::Result<PathBuf> {
+    get_runtime_path(STATUS_PATH)
+}
+
+const LAST_SELECT: &str = "ytcc/selected.yts";
+pub fn last_select() -> anyhow::Result<PathBuf> {
+    get_runtime_path(LAST_SELECT)
+}
diff --git a/pkgs/sources/yt/src/downloader.rs b/pkgs/sources/yt/src/downloader.rs
new file mode 100644
index 00000000..e915700d
--- /dev/null
+++ b/pkgs/sources/yt/src/downloader.rs
@@ -0,0 +1,212 @@
+use std::{
+    fs::{self, canonicalize},
+    io::{stderr, stdout, Read},
+    mem,
+    os::unix::fs::symlink,
+    path::PathBuf,
+    process::Command,
+    sync::mpsc::{self, Receiver, Sender},
+    thread::{self, JoinHandle},
+};
+
+use anyhow::{bail, Context, Result};
+use log::{debug, error, warn};
+use url::Url;
+
+use crate::constants::{status_path, CONCURRENT, DOWNLOAD_DIR, MPV_FLAGS, YT_DLP_FLAGS};
+
+#[derive(Debug)]
+pub struct Downloadable {
+    pub url: Url,
+    pub id: Option<u32>,
+}
+
+impl std::fmt::Display for Downloadable {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> {
+        write!(
+            f,
+            "{}|{}",
+            self.url.as_str().replace('|', ";"),
+            self.id.unwrap_or(0),
+        )
+    }
+}
+
+pub struct Downloader {
+    sent: usize,
+    download_thread: JoinHandle<Result<()>>,
+    orx: Receiver<(PathBuf, Option<u32>)>,
+    itx: Option<Sender<Downloadable>>,
+    playspec: Vec<Downloadable>,
+}
+
+impl Downloader {
+    pub fn new(mut playspec: Vec<Downloadable>) -> anyhow::Result<Downloader> {
+        let (itx, irx): (Sender<Downloadable>, Receiver<Downloadable>) = mpsc::channel();
+        let (otx, orx) = mpsc::channel();
+        let jh = thread::spawn(move || -> Result<()> {
+            while let Ok(pt) = irx.recv() {
+                debug!("Got '{}' to be downloaded", pt);
+                let path = download_url(&pt.url)
+                    .with_context(|| format!("Failed to download url: '{}'", &pt.url))?;
+                otx.send((path, pt.id)).expect("Should not be dropped");
+            }
+            debug!("Finished Downloading everything");
+            Ok(())
+        });
+
+        playspec.reverse();
+        let mut output = Downloader {
+            sent: 0,
+            download_thread: jh,
+            orx,
+            itx: Some(itx),
+            playspec,
+        };
+        if output.playspec.len() <= CONCURRENT as usize {
+            output.add(output.playspec.len() as u32)?;
+        } else {
+            output.add(CONCURRENT)?;
+        }
+        Ok(output)
+    }
+
+    pub fn add(&mut self, number_to_add: u32) -> Result<()> {
+        debug!("Adding {} to be downloaded concurrently", number_to_add);
+        for _ in 0..number_to_add {
+            let pt = self.playspec.pop().expect("This call should be guarded");
+            self.itx.as_ref().expect("Should still be valid").send(pt)?;
+            self.sent += 1;
+        }
+        Ok(())
+    }
+
+    /// Return the next video already downloaded, will block until the download is complete
+    pub fn next(&mut self) -> Option<(PathBuf, Option<u32>)> {
+        debug!("Requesting next output");
+        match self.orx.recv() {
+            Ok(ok) => {
+                debug!("Output downloaded to: {}", ok.0.display());
+                if !self.playspec.is_empty() {
+                    self.add(1).ok()?;
+                } else {
+                    debug!(
+                        "Done sending videos to be downloaded, downoladed: {} videos",
+                        self.sent
+                    );
+                    let itx = mem::take(&mut self.itx);
+                    drop(itx)
+                }
+                debug!("Returning: {}|{}", ok.0.display(), ok.1.unwrap_or(0));
+                Some(ok)
+            }
+            Err(err) => {
+                debug!("Received error while listening: {}", err);
+                None
+            }
+        }
+    }
+
+    pub fn drop(self) -> anyhow::Result<()> {
+        // Check that we really downloaded everything
+        assert_eq!(self.playspec.len(), 0);
+        match self.download_thread.join() {
+            Ok(ok) => ok,
+            Err(err) => panic!("Failed to join downloader thread: '{:#?}'", err),
+        }
+    }
+
+    pub fn consume(mut self) -> anyhow::Result<()> {
+        while let Some((path, id)) = self.next() {
+            debug!("Next path to play is: '{}'", path.display());
+            let mut info_json = canonicalize(&path).context("Failed to canoncialize path")?;
+            info_json.set_extension("info.json");
+
+            if status_path()?.is_symlink() {
+                fs::remove_file(status_path()?).context("Failed to delete old status file")?;
+            } else if !status_path()?.exists() {
+                debug!(
+                    "The status path at '{}' does not exists",
+                    status_path()?.display()
+                );
+            } else {
+                bail!(
+                    "The status path ('{}') is not a symlink but exists!",
+                    status_path()?.display()
+                );
+            }
+
+            symlink(info_json, status_path()?).context("Failed to symlink")?;
+
+            let mut mpv = Command::new("mpv");
+            mpv.stdout(stdout());
+            mpv.stderr(stderr());
+            mpv.args(MPV_FLAGS);
+            // TODO: Set the title to the name of the video, not the path <2024-02-09>
+            // mpv.arg(format!("--title="))
+            mpv.arg(&path);
+
+            let status = mpv.status().context("Failed to run mpv")?;
+            if status.success() {
+                fs::remove_file(&path)?;
+                if let Some(id) = id {
+                    println!("\x1b[32;1mMarking {} as watched!\x1b[0m", id);
+                    let mut ytcc = std::process::Command::new("ytcc");
+                    ytcc.stdout(stdout());
+                    ytcc.stderr(stderr());
+                    ytcc.args(["mark"]);
+                    ytcc.arg(id.to_string());
+                    let status = ytcc.status().context("Failed to run ytcc")?;
+                    if let Some(code) = status.code() {
+                        if code != 0 {
+                            bail!("Ytcc failed with status: {}", code);
+                        }
+                    }
+                }
+                debug!("mpv exited with: '{}'", status);
+            } else {
+                warn!("mpv exited with: '{}'", status);
+            }
+        }
+        self.drop()?;
+        Ok(())
+    }
+}
+
+fn download_url(url: &Url) -> Result<PathBuf> {
+    let output_file = tempfile::NamedTempFile::new().context("Failed to create tempfile")?;
+    output_file
+        .as_file()
+        .set_len(0)
+        .context("Failed to truncate temp-file")?;
+    if !Into::<PathBuf>::into(DOWNLOAD_DIR).exists() {
+        fs::create_dir_all(DOWNLOAD_DIR)
+            .with_context(|| format!("Failed to create download dir at: {}", DOWNLOAD_DIR))?
+    }
+    let mut yt_dlp = Command::new("yt-dlp");
+    yt_dlp.current_dir(DOWNLOAD_DIR);
+    yt_dlp.stdout(stdout());
+    yt_dlp.stderr(stderr());
+    yt_dlp.args(YT_DLP_FLAGS);
+    yt_dlp.args([
+        "--output",
+        "%(channel)s/%(title)s.%(ext)s",
+        url.as_str(),
+        "--print-to-file",
+        "after_move:filepath",
+    ]);
+    yt_dlp.arg(output_file.path().as_os_str());
+
+    let status = yt_dlp.status().context("Failed to run yt-dlp")?;
+    if !status.success() {
+        error!("yt-dlp execution failed with error: '{}'", status);
+    }
+
+    let mut path = String::new();
+    output_file
+        .as_file()
+        .read_to_string(&mut path)
+        .context("Failed to read output file temp file")?;
+    let path = path.trim();
+    Ok(path.into())
+}
diff --git a/pkgs/sources/yt/src/help.str b/pkgs/sources/yt/src/help.str
new file mode 100644
index 00000000..130fe42a
--- /dev/null
+++ b/pkgs/sources/yt/src/help.str
@@ -0,0 +1,8 @@
+# Commands:
+# w, watch  = watch id
+# d, drop   = mark id as watched
+# u, url    = open the associated URL in the `timesinks.youtube` Firefox profile
+# p, pick   = leave id as is; This is a noop
+#
+# These lines can be re-ordered; they are executed from top to bottom.
+# vim: filetype=yts conceallevel=2 concealcursor=nc colorcolumn=
diff --git a/pkgs/sources/yt/src/lib.rs b/pkgs/sources/yt/src/lib.rs
new file mode 100644
index 00000000..b089c1a2
--- /dev/null
+++ b/pkgs/sources/yt/src/lib.rs
@@ -0,0 +1,185 @@
+use anyhow::{bail, Context};
+use downloader::Downloadable;
+use serde::Deserialize;
+use url::Url;
+
+pub mod constants;
+pub mod downloader;
+
+#[derive(Deserialize)]
+pub struct YtccListData {
+    pub url: String,
+    pub title: String,
+    pub description: String,
+    pub publish_date: String,
+    pub watch_date: Option<f64>,
+    pub duration: String,
+    pub thumbnail_url: Option<String>,
+    pub extractor_hash: String,
+    pub id: u32,
+    pub playlists: Vec<YtccPlaylistData>,
+}
+
+impl std::fmt::Display for YtccListData {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> {
+        write!(
+            f,
+            r#"pick {} "{}" "{}" "{}" "{}" "{}"{}"#,
+            self.id,
+            self.title.replace(['"', '„', '”'], "'"),
+            self.publish_date,
+            self.playlists
+                .iter()
+                .map(|p| p.name.replace('"', "'"))
+                .collect::<Vec<String>>()
+                .join(", "),
+            Duration::from(self.duration.trim()),
+            self.url.replace('"', "'"),
+            "\n"
+        )
+    }
+}
+
+#[derive(Deserialize)]
+pub struct YtccPlaylistData {
+    pub name: String,
+    pub url: String,
+    pub reverse: bool,
+}
+
+pub enum LineCommand {
+    Pick,
+    Drop,
+    Watch,
+    Url,
+}
+
+impl std::str::FromStr for LineCommand {
+    type Err = anyhow::Error;
+    fn from_str(v: &str) -> Result<Self, <Self as std::str::FromStr>::Err> {
+        match v {
+            "pick" | "p" => Ok(Self::Pick),
+            "drop" | "d" => Ok(Self::Drop),
+            "watch" | "w" => Ok(Self::Watch),
+            "url" | "u" => Ok(Self::Url),
+            other => bail!("'{}' is not a recognized command!", other),
+        }
+    }
+}
+
+pub struct Line {
+    pub cmd: LineCommand,
+    pub id: u32,
+    pub url: Url,
+}
+
+/// We expect that each line is correctly formatted, and simply use default ones if they are not
+impl From<&str> for Line {
+    fn from(v: &str) -> Self {
+        let buf: Vec<_> = v.split_whitespace().collect();
+        let url: Url = Url::parse(
+            buf.last()
+                .expect("This should always exists")
+                .trim_matches('"'),
+        )
+        .expect("This parsing should work,as the url is generated");
+
+        Line {
+            cmd: buf
+                .get(0)
+                .unwrap_or(&"pick")
+                .parse()
+                .unwrap_or(LineCommand::Pick),
+            id: buf.get(1).unwrap_or(&"0").parse().unwrap_or(0),
+            url,
+        }
+    }
+}
+
+pub struct Duration {
+    time: u32,
+}
+
+impl From<&str> for Duration {
+    fn from(v: &str) -> Self {
+        let buf: Vec<_> = v.split(':').take(2).collect();
+        Self {
+            time: (buf[0].parse::<u32>().expect("Should be a number") * 60)
+                + buf[1].parse::<u32>().expect("Should be a number"),
+        }
+    }
+}
+
+impl std::fmt::Display for Duration {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> {
+        const SECOND: u32 = 1;
+        const MINUTE: u32 = 60 * SECOND;
+        const HOUR: u32 = 60 * MINUTE;
+
+        let base_hour = self.time - (self.time % HOUR);
+        let base_min = (self.time % HOUR) - ((self.time % HOUR) % MINUTE);
+        let base_sec = (self.time % HOUR) % MINUTE;
+
+        let h = base_hour / HOUR;
+        let m = base_min / MINUTE;
+        let s = base_sec / SECOND;
+
+        if self.time == 0 {
+            write!(f, "[No Duration]")
+        } else if h > 0 {
+            write!(f, "[{h}h {m}m]")
+        } else {
+            write!(f, "[{m}m {s}s]")
+        }
+    }
+}
+#[cfg(test)]
+mod test {
+    use crate::Duration;
+
+    #[test]
+    fn test_display_duration_1h() {
+        let dur = Duration { time: 60 * 60 };
+        assert_eq!("[1h 0m]".to_owned(), dur.to_string());
+    }
+    #[test]
+    fn test_display_duration_30min() {
+        let dur = Duration { time: 60 * 30 };
+        assert_eq!("[30m 0s]".to_owned(), dur.to_string());
+    }
+}
+
+pub fn ytcc_drop(id: u32) -> anyhow::Result<()> {
+    let mut ytcc = std::process::Command::new("ytcc");
+    ytcc.args(["mark", &format!("{}", id)]);
+    if !ytcc.status().context("Failed to run ytcc")?.success() {
+        bail!("`ytcc mark {}` failed to execute", id)
+    }
+    Ok(())
+}
+
+pub fn filter_line(line: &str) -> anyhow::Result<Option<Downloadable>> {
+    // Filter out comments and empty lines
+    if line.starts_with('#') || line.trim().is_empty() {
+        return Ok(None);
+    }
+
+    let line = Line::from(line);
+    match line.cmd {
+        LineCommand::Pick => Ok(None),
+        LineCommand::Drop => ytcc_drop(line.id)
+            .with_context(|| format!("Failed to drop: {}", line.id))
+            .map(|_| None),
+        LineCommand::Watch => Ok(Some(Downloadable {
+            id: Some(line.id),
+            url: line.url,
+        })),
+        LineCommand::Url => {
+            let mut firefox = std::process::Command::new("firefox");
+            firefox.args(["-P", "timesinks.youtube"]);
+            firefox.arg(line.url.as_str());
+            let _handle = firefox.spawn().context("Failed to run firefox")?;
+            Ok(None)
+        }
+    }
+}
diff --git a/pkgs/sources/yt/todo b/pkgs/sources/yt/todo
new file mode 100644
index 00000000..3f22042c
--- /dev/null
+++ b/pkgs/sources/yt/todo
@@ -0,0 +1 @@
+subtitles
diff --git a/pkgs/sources/yt/update.sh b/pkgs/sources/yt/update.sh
new file mode 100755
index 00000000..e500bb23
--- /dev/null
+++ b/pkgs/sources/yt/update.sh
@@ -0,0 +1,8 @@
+#!/usr/bin/env sh
+
+nix flake update
+
+[ "$1" = "upgrade" ] && cargo upgrade
+cargo update
+
+# vim: ft=sh
diff --git a/pkgs/sources/yt/yt.nix b/pkgs/sources/yt/yt.nix
new file mode 100644
index 00000000..aaa971c3
--- /dev/null
+++ b/pkgs/sources/yt/yt.nix
@@ -0,0 +1,29 @@
+{
+  lib,
+  rustPlatform,
+  ytcc,
+  yt-dlp,
+  mpv,
+  makeWrapper,
+}:
+rustPlatform.buildRustPackage {
+  pname = "yt";
+  version = "0.1.0";
+
+  src = ./.;
+  cargoLock = {
+    lockFile = ./Cargo.lock;
+  };
+
+  buildNoDefaultFeatures = true;
+  buildFeatures = ["yt"];
+
+  nativeBuildInputs = [
+    makeWrapper
+  ];
+
+  postInstall = ''
+    wrapProgram $out/bin/yt \
+      --prefix PATH : ${lib.makeBinPath [mpv yt-dlp ytcc]}
+  '';
+}
diff --git a/pkgs/sources/yt/ytc.nix b/pkgs/sources/yt/ytc.nix
new file mode 100644
index 00000000..dff5bcf8
--- /dev/null
+++ b/pkgs/sources/yt/ytc.nix
@@ -0,0 +1,29 @@
+{
+  lib,
+  rustPlatform,
+  ytcc,
+  yt-dlp,
+  mpv,
+  makeWrapper,
+}:
+rustPlatform.buildRustPackage {
+  pname = "ytc";
+  version = "0.1.0";
+
+  src = ./.;
+  cargoLock = {
+    lockFile = ./Cargo.lock;
+  };
+
+  buildNoDefaultFeatures = true;
+  buildFeatures = ["ytc"];
+
+  nativeBuildInputs = [
+    makeWrapper
+  ];
+
+  postInstall = ''
+    wrapProgram $out/bin/ytc \
+      --set PATH ${lib.makeBinPath [mpv yt-dlp ytcc]}
+  '';
+}
diff --git a/pkgs/sources/yt/yts.nix b/pkgs/sources/yt/yts.nix
new file mode 100644
index 00000000..9a8b172e
--- /dev/null
+++ b/pkgs/sources/yt/yts.nix
@@ -0,0 +1,27 @@
+{
+  lib,
+  rustPlatform,
+  ytcc,
+  makeWrapper,
+}:
+rustPlatform.buildRustPackage {
+  pname = "yts";
+  version = "0.1.0";
+
+  src = ./.;
+  cargoLock = {
+    lockFile = ./Cargo.lock;
+  };
+
+  buildNoDefaultFeatures = true;
+  buildFeatures = ["yts"];
+
+  nativeBuildInputs = [
+    makeWrapper
+  ];
+
+  postInstall = ''
+    wrapProgram $out/bin/yts \
+      --prefix PATH : ${lib.makeBinPath [ytcc]}
+  '';
+}