From 35af4bab99da94845ebd6e9b2efae6a544d39b35 Mon Sep 17 00:00:00 2001 From: Collin Williams <96917990+bluedragon1221@users.noreply.github.com> Date: Fri, 22 May 2026 10:23:58 -0500 Subject: LOTS OF TEMP STUFF --- broken_stuff | 11 +- flake.lock | 96 +++++----- flake.nix | 2 +- hosts.toml | 7 + hosts/ganymede/caddy.nix | 2 +- hosts/ganymede/config.nix | 69 +++---- hosts/ganymede/nixos.nix | 5 + hosts/ganymede/secrets/wireguard-pk.age | 11 -- hosts/ganymede/secrets/wireguard-privkey.age | 8 + hosts/mercury/ai.nix | 8 - hosts/mercury/config.nix | 23 ++- hosts/mercury/disks.nix | 12 +- hosts/mercury/hjem.nix | 6 +- hosts/mercury/nixos.nix | 86 +++------ hosts/mercury/secrets/ts-key.age | Bin 0 -> 274 bytes hosts/mercury/secrets/wireguard-privkey.age | 6 + justfile | 7 +- modules/desktop/hjem/programs/firefox.nix | 8 +- modules/desktop/hjem/programs/foot.nix | 3 +- modules/desktop/hjem/wm/sway.nix | 9 +- modules/desktop/nixos/fonts.nix | 2 +- modules/desktop/nixos/programs/firefox.nix | 19 +- modules/desktop/options.nix | 12 +- modules/nix/nixos/default.nix | 4 +- modules/services/nixos/default.nix | 5 +- modules/services/nixos/ganyupload/AGENTS.md | 65 +++++++ modules/services/nixos/ganyupload/main.go | 212 +++++++++++++++++++++- modules/services/nixos/jta/main.go | 30 ++- modules/services/nixos/minecraft.nix | 12 +- modules/services/nixos/ngircd.nix | 8 +- modules/services/nixos/openssh.nix | 31 ++-- modules/services/nixos/polaris.nix | 26 --- modules/services/options.nix | 94 ++++------ modules/system/nixos/networking/tailscale.nix | 33 ---- modules/system/options.nix | 2 - modules/terminal/hjem/programs/broot.nix | 7 +- modules/terminal/hjem/programs/git.nix | 9 +- modules/terminal/hjem/programs/tmux.nix | 46 ++--- pkgs/obsidian.nix | 24 +++ secrets.nix | 3 +- services/caddy.nix | 104 +++++++++++ services/goaccess.nix | 136 ++++++++++++++ tmux-tsunami/flake.lock | 42 +++++ tmux-tsunami/flake.nix | 18 ++ tmux-tsunami/modules/common/bar.nix | 69 +++++++ tmux-tsunami/modules/common/defaultGlobalKeys.nix | 66 +++++++ tmux-tsunami/modules/common/defaultLeaderMenu.nix | 51 ++++++ tmux-tsunami/modules/common/findFile.nix | 66 +++++++ tmux-tsunami/modules/common/fzfExec.nix | 55 ++++++ tmux-tsunami/modules/common/keys.nix | 66 +++++++ tmux-tsunami/modules/common/main.nix | 23 +++ tmux-tsunami/modules/common/sessionizer.nix | 92 ++++++++++ tmux-tsunami/modules/common/sessionizer2.nix | 50 +++++ tmux-tsunami/modules/hjem.nix | 58 ++++++ tmux-tsunami/modules/installer.nix | 38 ++++ tmux-tsunami/planning.md | 56 ++++++ 56 files changed, 1592 insertions(+), 421 deletions(-) delete mode 100644 hosts/ganymede/secrets/wireguard-pk.age create mode 100644 hosts/ganymede/secrets/wireguard-privkey.age delete mode 100644 hosts/mercury/ai.nix create mode 100644 hosts/mercury/secrets/ts-key.age create mode 100644 hosts/mercury/secrets/wireguard-privkey.age create mode 100644 modules/services/nixos/ganyupload/AGENTS.md delete mode 100644 modules/services/nixos/polaris.nix delete mode 100644 modules/system/nixos/networking/tailscale.nix create mode 100644 pkgs/obsidian.nix create mode 100644 services/caddy.nix create mode 100644 services/goaccess.nix create mode 100644 tmux-tsunami/flake.lock create mode 100644 tmux-tsunami/flake.nix create mode 100644 tmux-tsunami/modules/common/bar.nix create mode 100644 tmux-tsunami/modules/common/defaultGlobalKeys.nix create mode 100644 tmux-tsunami/modules/common/defaultLeaderMenu.nix create mode 100644 tmux-tsunami/modules/common/findFile.nix create mode 100644 tmux-tsunami/modules/common/fzfExec.nix create mode 100644 tmux-tsunami/modules/common/keys.nix create mode 100644 tmux-tsunami/modules/common/main.nix create mode 100644 tmux-tsunami/modules/common/sessionizer.nix create mode 100644 tmux-tsunami/modules/common/sessionizer2.nix create mode 100644 tmux-tsunami/modules/hjem.nix create mode 100644 tmux-tsunami/modules/installer.nix create mode 100644 tmux-tsunami/planning.md diff --git a/broken_stuff b/broken_stuff index 84c3381..935a33b 100644 --- a/broken_stuff +++ b/broken_stuff @@ -1 +1,10 @@ -- automount filesganymede-dav (code removed) +idea: + - tmux + - projects are sessions (but ditch old sessionizer) + - new session with C-x C-n or whatever + - opens new session with broot window + - broot window controls entire session + - selecting dir changes session dir + - on file opens it in new tmux window + - use standard join-pane split-pane for splitting stuff + - alt-t creates new terminal pane within session with dir = session dir diff --git a/flake.lock b/flake.lock index bcb5040..01631b4 100644 --- a/flake.lock +++ b/flake.lock @@ -26,11 +26,11 @@ "betterfox": { "flake": false, "locked": { - "lastModified": 1774552373, - "narHash": "sha256-B9JG4GhjMguTUukinSrJWmEWmjU1EJ1UzlfQ9q+NTp8=", + "lastModified": 1777825377, + "narHash": "sha256-4d4S0DAqCjQFHoACAUSpltPqYrs83ZecuBU+m/x7xvM=", "owner": "yokoffing", "repo": "Betterfox", - "rev": "fd29d4d313eda7a93f45f2f66bb0e87ff35d1a72", + "rev": "392c62a03c0d63e323a9aae55bc9aff87454db16", "type": "github" }, "original": { @@ -47,11 +47,11 @@ ] }, "locked": { - "lastModified": 1775334496, - "narHash": "sha256-dUopEMj5KhH1vnv43o29kMsayp0Ki+9EfIn4UAE9sAU=", + "lastModified": 1778874672, + "narHash": "sha256-lL5SXrufwxu9sthDnTCieH8gskhOv5KOGj2wXx2xCsw=", "owner": "9001", "repo": "copyparty", - "rev": "ede692925edfc6fca7a52f483414d543e9303237", + "rev": "3b53a228b0e07912766d8dbf88b1dd01da57e2c5", "type": "github" }, "original": { @@ -62,11 +62,11 @@ }, "crane": { "locked": { - "lastModified": 1773189535, - "narHash": "sha256-E1G/Or6MWeP+L6mpQ0iTFLpzSzlpGrITfU2220Gq47g=", + "lastModified": 1778106249, + "narHash": "sha256-cM/AuKy5tMhwOOQIbha8ZRRMHVfNf7cv2aljIw+qoCg=", "owner": "ipetkov", "repo": "crane", - "rev": "6fa2fb4cf4a89ba49fc9dd5a3eb6cde99d388269", + "rev": "6d015ea29630b7ad2402841386da2cb617a470a7", "type": "github" }, "original": { @@ -104,11 +104,11 @@ ] }, "locked": { - "lastModified": 1773889306, - "narHash": "sha256-PAqwnsBSI9SVC2QugvQ3xeYCB0otOwCacB1ueQj2tgw=", + "lastModified": 1778958912, + "narHash": "sha256-6pvS9rIF9mZRj1ENwu9fDLHeG1JFDTCpRyy6vJhXkTA=", "owner": "nix-community", "repo": "disko", - "rev": "5ad85c82cc52264f4beddc934ba57f3789f28347", + "rev": "6e8dc7aa0e65fce67c76e18227a13a7d529f2cdf", "type": "github" }, "original": { @@ -120,11 +120,11 @@ "firefox-csshacks": { "flake": false, "locked": { - "lastModified": 1774892585, - "narHash": "sha256-TCbdj6ZedgPSDQWJWFBPzDlLdgRcnsby3DWNHxmQ+b4=", + "lastModified": 1778816467, + "narHash": "sha256-RuZ0ihlTNu3chVVYd5JZX7xrRl2+jtLmucAKROKFD3s=", "owner": "MrOtherGuy", "repo": "firefox-csshacks", - "rev": "d02c1a92fa86777f3ae52f19e0c338a67eabe908", + "rev": "1f477a50cf7a2996a0dbd7656c877f22df5bf968", "type": "github" }, "original": { @@ -183,11 +183,11 @@ "fx-autoconfig": { "flake": false, "locked": { - "lastModified": 1773035931, - "narHash": "sha256-vOsXpuU2XlbIfXGm1RJicY8ghbkU5FvnctQMBMXCXbs=", + "lastModified": 1777913309, + "narHash": "sha256-czNgt62fofg3hXw7F4wXSv/+ZAsGtO6bg3sUOiUXcu4=", "owner": "MrOtherGuy", "repo": "fx-autoconfig", - "rev": "8da9268f7afcbf5d438b099e976fe8aa46a111d0", + "rev": "d469a80f12e286c0e937d8b93c01dfc2d55dca8f", "type": "github" }, "original": { @@ -199,7 +199,7 @@ "geolite-db": { "flake": false, "locked": { - "narHash": "sha256-ea/ne52oFzES4jUjMri2OplkHpOJyOfR38beO4LRBgY=", + "narHash": "sha256-uA7ExafgNl03y3PW+dbm79iUncMDn62eUmMjCr2guZA=", "type": "file", "url": "https://github.com/P3TERX/GeoLite.mmdb/releases/latest/download/GeoLite2-City.mmdb" }, @@ -238,11 +238,11 @@ ] }, "locked": { - "lastModified": 1775569958, - "narHash": "sha256-W+kpLQ5J/DDaY1nUmvN9aIAfh5ixdyqYM76ZUdcvMBU=", + "lastModified": 1779044379, + "narHash": "sha256-DHV1vpUers1TbZxIuEGz4VENShZ/EXefiTXd79QIhs4=", "owner": "feel-co", "repo": "hjem", - "rev": "32bd6d54d805a3eb41efa62a940eeceaf263c4a8", + "rev": "4a2fd57f9e5d22cd3ef4c4230d245da159ab8dce", "type": "github" }, "original": { @@ -282,11 +282,11 @@ "rust-overlay": "rust-overlay" }, "locked": { - "lastModified": 1775510693, - "narHash": "sha256-gZfJ07j/oOciDi8mF/V8QTm7YCeDcusNSMZzBFi8OUM=", + "lastModified": 1778702031, + "narHash": "sha256-HJ4e4IQz7TJ1wDmEnkowXCM6SYXF9sfYg8nmUYHCnpI=", "owner": "nix-community", "repo": "lanzaboote", - "rev": "3fe0ae8cb285e0ad101a9675f4190d455fb05e85", + "rev": "f11608843ca4fa95049c096ec0a50c93b41080b8", "type": "github" }, "original": { @@ -303,11 +303,11 @@ ] }, "locked": { - "lastModified": 1775037210, - "narHash": "sha256-KM2WYj6EA7M/FVZVCl3rqWY+TFV5QzSyyGE2gQxeODU=", + "lastModified": 1777780666, + "narHash": "sha256-8wURyQMdDkGUarSTKOGdCuFfYiwa3HbzwscUfn3STDE=", "owner": "nix-darwin", "repo": "nix-darwin", - "rev": "06648f4902343228ce2de79f291dd5a58ee12146", + "rev": "8c62fba0854ba15c8917aed18894dbccb48a3777", "type": "github" }, "original": { @@ -323,11 +323,11 @@ ] }, "locked": { - "lastModified": 1775365369, - "narHash": "sha256-DgH5mveLoau20CuTnaU5RXZWgFQWn56onQ4Du2CqYoI=", + "lastModified": 1778999127, + "narHash": "sha256-V5GquqJvAqwFTcpN6hxKSQAtwuJFRUEHmyNKbeaTQDg=", "owner": "nix-community", "repo": "nix-index-database", - "rev": "cef5cf82671e749ac87d69aadecbb75967e6f6c3", + "rev": "f680e0d3c1dbefe298c423691662e238496890f2", "type": "github" }, "original": { @@ -368,11 +368,11 @@ }, "nixpkgs": { "locked": { - "lastModified": 1775423009, - "narHash": "sha256-vPKLpjhIVWdDrfiUM8atW6YkIggCEKdSAlJPzzhkQlw=", + "lastModified": 1778869304, + "narHash": "sha256-30sZNZoA1cqF5JNO9fVX+wgiQYjB7HJqqJ4ztCDeBZE=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "68d8aa3d661f0e6bd5862291b5bb263b2a6595c9", + "rev": "d233902339c02a9c334e7e593de68855ad26c4cb", "type": "github" }, "original": { @@ -392,11 +392,11 @@ ] }, "locked": { - "lastModified": 1772893680, - "narHash": "sha256-JDqZMgxUTCq85ObSaFw0HhE+lvdOre1lx9iI6vYyOEs=", + "lastModified": 1776796298, + "narHash": "sha256-PcRvlWayisPSjd0UcRQbhG8Oqw78AcPE6x872cPRHN8=", "owner": "cachix", "repo": "pre-commit-hooks.nix", - "rev": "8baab586afc9c9b57645a734c820e4ac0a604af9", + "rev": "3cfd774b0a530725a077e17354fbdb87ea1c4aad", "type": "github" }, "original": { @@ -432,11 +432,11 @@ ] }, "locked": { - "lastModified": 1773544328, - "narHash": "sha256-Iv+qez54LAz+isij4APBk31VWA//Go81hwFOXr5iWTw=", + "lastModified": 1778383025, + "narHash": "sha256-UK7s2LJS1YwIMFL7PSaNJvLXT9pyRgm7X+HNPgMXiEE=", "owner": "oxalica", "repo": "rust-overlay", - "rev": "4f977d776793c8bfbfdd7eca7835847ccc48874e", + "rev": "4568a557ca325ff81fb354382d4a9968daa1001a", "type": "github" }, "original": { @@ -468,18 +468,14 @@ ] }, "locked": { - "lastModified": 1768326969, - "narHash": "sha256-Mwo6C1u4UcVEAZOJa/CUSDx3he4CAfAx1Ns8l+blj3I=", - "owner": "bluedragon1221", - "repo": "tmux-tsunami", - "rev": "f379f00fec4912b539d3659f7c74da7669785adb", - "type": "github" + "path": "./tmux-tsunami", + "type": "path" }, "original": { - "owner": "bluedragon1221", - "repo": "tmux-tsunami", - "type": "github" - } + "path": "./tmux-tsunami", + "type": "path" + }, + "parent": [] }, "uc-css-js": { "flake": false, diff --git a/flake.nix b/flake.nix index 0343c67..12a2b81 100644 --- a/flake.nix +++ b/flake.nix @@ -44,7 +44,7 @@ }; tmux-tsunami = { - url = "github:bluedragon1221/tmux-tsunami"; + url = "./tmux-tsunami"; inputs.nixpkgs.follows = "nixpkgs"; }; diff --git a/hosts.toml b/hosts.toml index ea2f400..3f660a9 100644 --- a/hosts.toml +++ b/hosts.toml @@ -2,14 +2,21 @@ user_pubkey = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIAvxtKW0rmRBi8J67gLrLv8Zv338AcmZ3P20DePiUfnX collin@mercury" host_pubkey = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIIQtMAgPdWwrOzlZT/lEIRQZ+ajhafG9AEJCrF2/bsmN" +wg_pubkey = "merTd71QvuMVQiRix/0NPAjVDvwRyLor20t+N9dDSEU=" + [hosts.ganymede] hostnames = ["ganymede", "192.168.50.2", "williamsfam.us.com"] user_pubkey = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIFeAsRh11Jyv+7Fm4WhzV4XN0LMYQS1/yMbvT+pvFDcd collin@ganymede" host_pubkey = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAINlr+53UmlGVP1blkdNl6NFqn1w2umFJyjH1EVUPKIy9" +wg_pubkey = "ganyuu2I5DY1VQm3fPvQGBI2veW43Rgs3lAVzckWR2I=" + [hosts.terra] user_pubkey = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAINKwMBLWWxpf4jZN3R3iQ7XEAOMq0KAOE/6YN4iTBuJx collin@terra" +wg_pubkey = "ter/93FM8jf5b73+psVuU5cVNaoy00Z/wB6XGrvH1G8=" +wg_privkey = "yBiEnhSZZInc+irnVhZNHF5y7Z7tsR/IS6j32F4tPWc=" + [hosts.io] hostnames = ["io", "192.168.50.3"] host_pubkey = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIJXSvoVpMh/Y84THjKAYqkbuPmmv/yIU8DunMov7tKKT" diff --git a/hosts/ganymede/caddy.nix b/hosts/ganymede/caddy.nix index d10910c..a03fcdc 100644 --- a/hosts/ganymede/caddy.nix +++ b/hosts/ganymede/caddy.nix @@ -9,7 +9,7 @@ "github.com/tailscale/caddy-tailscale@v0.0.0-20251204171825-f070d146dd61" "github.com/caddy-dns/porkbun@v0.3.1" ]; - hash = "sha256-n0HVlpCkJITyEPc+ml7zCcYw9gy3r1YAXertd6zc2o0="; + hash = "sha256-FrAI7Fpz3bXclmKcizBMv/VI1hTAWT6DQnj7S09MwNY="; }); globalConfig = '' diff --git a/hosts/ganymede/config.nix b/hosts/ganymede/config.nix index 94d67ec..47b7f49 100644 --- a/hosts/ganymede/config.nix +++ b/hosts/ganymede/config.nix @@ -9,19 +9,6 @@ }; "caddy-env".file = ./secrets/caddy-env.age; - - # "collin-copyparty-password" = { - # file = ./secrets/collin-copyparty-password.age; - # owner = "copyparty"; - # }; - "collin-forgejo-password" = { - file = ./secrets/collin-forgejo-password.age; - owner = "forgejo"; - }; - "wireguard-pk" = { - file = ./secrets/wireguard-pk.age; - owner = "systemd-network"; - }; }; terminal = { @@ -41,6 +28,7 @@ ip = "192.168.50.2/24"; gateway = "192.168.50.1"; }; + wireless.static = { ssid = "williams"; pskFile = config.collinux.secrets."williams-psk".path; @@ -50,58 +38,45 @@ services = { sshd = { enable = true; - port = 22; - listenAddr = "0.0.0.0"; - rootLogin = true; - }; + public = true; - goaccess = { - enable = true; - privateUrl = "stats.ganymede"; - }; - btopweb = { - enable = true; - privateUrl = "btop.ganymede"; - }; - - cgit = { - enable = true; - privateUrl = "git.ganymede"; + conf.rootLogin = true; }; minecraft = { enable = true; - listenAddr = "0.0.0.0"; + public = true; }; - ngircd.enable = true; - - qbittorrent = { + ngircd = { enable = true; - privateUrl = "bittorrent.ganymede"; + public = true; }; - # copyparty = { - # enable = true; - # listenAddr = "0.0.0.0"; - # publicUrl = "up.williamsfam.us.com"; - # privateUrl = "files.ganymede"; - - # users.collin = { - # isAdmin = true; - # passwordFile = config.collinux.secrets."collin-copyparty-password".path; - # hasPublicDir = true; - # }; - # }; jta = { enable = true; publicUrl = "jta.williamsfam.us.com"; }; - ganyupload = { enable = true; publicUrl = "upld.williamsfam.us.com"; }; + goaccess = { + enable = true; + privateUrl = "stats.ganymede"; + }; + btopweb = { + enable = true; + privateUrl = "btop.ganymede"; + }; + qbittorrent = { + enable = true; + privateUrl = "bittorrent.ganymede"; + }; + cgit = { + enable = true; + privateUrl = "git.ganymede"; + }; caddy = { enable = true; diff --git a/hosts/ganymede/nixos.nix b/hosts/ganymede/nixos.nix index 415509a..77e3ad5 100644 --- a/hosts/ganymede/nixos.nix +++ b/hosts/ganymede/nixos.nix @@ -16,6 +16,11 @@ facter.reportPath = ./facter.json; + environment.systemPackages = [ + pkgs.python313 + pkgs.net-tools + ]; + # backup usb teather configuration systemd.network.networks."80-usb-teather" = { name = "enp0s20f0u2"; diff --git a/hosts/ganymede/secrets/wireguard-pk.age b/hosts/ganymede/secrets/wireguard-pk.age deleted file mode 100644 index c8e365a..0000000 --- a/hosts/ganymede/secrets/wireguard-pk.age +++ /dev/null @@ -1,11 +0,0 @@ ------BEGIN AGE ENCRYPTED FILE----- -YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IHNzaC1lZDI1NTE5IEpnNHlXUSBxZlFt -bWR5NFJ1N0FBMkc2eThudnI1TnQ2WFNzK2NsVzdtaVgzQXRYNFhzCkJXTGUzSU5E -U05jYnEvQXVwZEhIU1FMamt3bUM4UnB3bmNEZWQzZDlBSXcKLT4gc09lRVkkfTQt -Z3JlYXNlIHMgSHYsVTZxOzgKWVJHbG1Ua3NONE9QYjVCNEQzOXQrNlZXRTBKQ3JW -elpKeGwvYVgrOVVMRStrUGl0ay9HYmNOcW8xZFJER2tXWApPcUZxY2I5aDBoMVBO -bmNxWE9YakZqL2h2dlFVYVdra0JrZjMKLS0tIGJWQlRmaVREL242aVdudFVOZTd4 -V1BnTnM3aTdGY1lUcm81VUVkd1QzcFUKMMrCSdf0J/xuvhvpktJO/GbpBp8ZnXue -S0TTS/s3zCx8wCT70j/C6alX1CeMX+RpJwiba5BpvTuphGrgrMjMtA0oYDCRZzeQ -1EtdX+A= ------END AGE ENCRYPTED FILE----- diff --git a/hosts/ganymede/secrets/wireguard-privkey.age b/hosts/ganymede/secrets/wireguard-privkey.age new file mode 100644 index 0000000..ee9130d --- /dev/null +++ b/hosts/ganymede/secrets/wireguard-privkey.age @@ -0,0 +1,8 @@ +age-encryption.org/v1 +-> ssh-ed25519 azBilg tvscfH96l4w2AkLjaczmtAJ3CKeU1mEPJWT82tR2HiY +F83pJqPj+qlFw4nb+ANCMJ7lOufSMluDIeWbaYvJkEU +-> ssh-ed25519 Jg4yWQ SXz2kA4FODscXLqsNsCjuqKqBUXSkEx5SIpSdjGKpQU +mVg50RgaBauKlTCyHUYDTPZYnDJkppOthhMC+MAsiiE +--- dlTzxZODPVxlkdRP3HRJSK6BU3yEHrs1fo/sudC6EKo +dEV!Axr?z(YKمSukst]Mjzψ\D\Bs./2= \ No newline at end of file diff --git a/hosts/mercury/ai.nix b/hosts/mercury/ai.nix deleted file mode 100644 index 4615838..0000000 --- a/hosts/mercury/ai.nix +++ /dev/null @@ -1,8 +0,0 @@ -{pkgs, ...}: { - services.ollama.enable = true; - # environment.systemPackages = [pkgs.mcphost]; - - hjem.users."collin".files.".config/fish/conf.d/ollama.fish".text = '' - set -gx OLLAMA_NOHISTORY 1 - ''; -} diff --git a/hosts/mercury/config.nix b/hosts/mercury/config.nix index 852c393..b9882f6 100644 --- a/hosts/mercury/config.nix +++ b/hosts/mercury/config.nix @@ -1,13 +1,16 @@ -{ +{config, ...}: { collinux = { - theme = "catppuccin"; + theme = "adwaita"; secrets = {}; user.useRun0 = true; desktop = { - wallpaper = ./wallpapers/abstract-swirls.jpg; + wallpaper = + if (config.collinux.theme == "catppuccin") + then ./wallpapers/abstract-swirls.jpg + else ./wallpapers/hintergrund2.png; gtk.enable = true; qt.enable = true; @@ -23,14 +26,13 @@ components = { fuzzel.enable = true; dunst.enable = true; - # tofi.enable = true; }; }; programs = { firefox = { enable = true; - extensions.zotero.enable = false; + extensions.foxyproxy.enable = true; }; foot.enable = true; @@ -44,9 +46,7 @@ secureBoot.enable = true; }; - network = { - wireless.dynamic = true; - }; + network.wireless.dynamic = true; audio.enable = true; bluetooth.enable = true; @@ -54,8 +54,10 @@ }; terminal = { - shells.fish.enable = true; - shells.bash.enable = true; # for nix-shells + shells = { + fish.enable = true; + bash.enable = true; # for nix-shells + }; programs = { starship.enable = true; @@ -63,6 +65,7 @@ bat.enable = true; eza.enable = true; broot.enable = true; + tmux.enable = true; helix = { enable = true; hardMode = true; diff --git a/hosts/mercury/disks.nix b/hosts/mercury/disks.nix index 60187b1..7ef2d0d 100644 --- a/hosts/mercury/disks.nix +++ b/hosts/mercury/disks.nix @@ -3,17 +3,25 @@ device = "/dev/disk/by-uuid/3d80a86b-3268-4209-a833-b531b8bc0ebc"; fsType = "ext4"; }; - fileSystems."/boot" = { device = "/dev/disk/by-uuid/45A4-2E5B"; fsType = "vfat"; options = ["fmask=0022" "dmask=0022"]; }; - + swapDevices = [ + { + device = "/dev/disk/by-label/swap"; + priority = 10; + } + ]; zramSwap = { enable = true; priority = 100; algorithm = "lz4"; memoryPercent = 50; }; + + boot.kernelParams = [ + "resume=LABEL=swap" + ]; } diff --git a/hosts/mercury/hjem.nix b/hosts/mercury/hjem.nix index 6ecbb8e..3625e7b 100644 --- a/hosts/mercury/hjem.nix +++ b/hosts/mercury/hjem.nix @@ -1,9 +1,12 @@ {pkgs, ...}: { packages = with pkgs; [ - # obsidian anki libreoffice-qt musescore + (pkgs.callPackage ../../pkgs/obsidian.nix {}) + + lmms # (?) + eq10q prismlauncher mpv @@ -12,7 +15,6 @@ opencode (pkgs.callPackage ../../pkgs/yo {}) - (pkgs.callPackage ../../pkgs/yokey {}) captive-browser # https://words.filippo.io/captive-browser (pkgs.makeDesktopItem { diff --git a/hosts/mercury/nixos.nix b/hosts/mercury/nixos.nix index a47375a..9a89fca 100644 --- a/hosts/mercury/nixos.nix +++ b/hosts/mercury/nixos.nix @@ -11,20 +11,17 @@ inputs.nixos-facter-modules.nixosModules.facter inputs.lanzaboote.nixosModules.lanzaboote ]; - facter.reportPath = ./facter.json; + services.dbus.implementation = "broker"; environment.defaultPackages = lib.mkForce []; # im not a noob - services.syncthing = { - enable = true; - user = "collin"; - dataDir = "/home/collin/.local/syncthing"; - }; - programs.kdeconnect.enable = true; - - security.soteria.enable = true; + programs.ssh.extraConfig = '' + Host ganymede + HostName williamsfam.us.com + Port 22 + ''; virtualisation.podman = { enable = true; @@ -45,66 +42,34 @@ } ]; }; - - services.jupyter = { + virtualisation.waydroid = { + package = pkgs.waydroid-nftables; enable = true; - ip = "127.0.0.1"; - port = 8888; - - user = "collin"; # to access my files - - package = pkgs.python313Packages.jupyter; - command = "jupyter lab --ServerApp.token='' --ServerApp.password=''"; - password = ""; - - notebookDir = "~/brain/notes/schoolyear2025/physics"; - - kernels = { - python3 = let - python = pkgs.python313.withPackages (ps: - with ps; [ - numpy - pandas - matplotlib - sympy - ipywidgets - ipydatagrid - ipykernel - ]); - in { - language = "python"; - displayName = "Python (Physics)"; - argv = ["${python}/bin/python" "-m" "ipykernel_launcher" "-f" "{connection_file}"]; - }; - }; }; + programs.kdeconnect.enable = true; + + services.autossh.sessions = [ + { + name = "ganymede"; + user = "collin"; + monitoringPort = 20000; + extraArguments = "-N -D 9090 collin@ganymede"; + } + ]; + boot.supportedFilesystems."fuse.sshfs" = true; fileSystems."/home/collin/ganymede" = { device = "collin@ganymede:/media"; fsType = "fuse.sshfs"; options = [ "identityfile=/home/collin/.ssh/id_ed25519" "idmap=user" - "x-systemd.automount" #< mount the filesystem automatically on first access - "allow_other" #< don't restrict access to only the user which `mount`s it (because that's probably systemd who mounts it, not you) - "user" #< allow manual `mount`ing, as ordinary user. + "x-systemd.automount" # mount the filesystem automatically on first access + "allow_other" # don't restrict access to only the user which `mount`s it (because that's probably systemd who mounts it, not you) + "user" # allow manual `mount`ing, as ordinary user. ]; }; - boot.supportedFilesystems."fuse.sshfs" = true; - services.autossh.sessions = [ - { - name = "ganymede"; - user = "collin"; - monitoringPort = 20000; - extraArguments = "-N -D 9090 collin@williamsfam.us.com"; - } - ]; - programs.ssh.extraConfig = '' - Host ganymede - HostName williamsfam.us.com - Port 22 - ''; security.pki.certificates = [ '' -----BEGIN CERTIFICATE----- @@ -120,11 +85,4 @@ -----END CERTIFICATE----- '' ]; - - programs.firefox.policies.ExtensionSettings = { - "foxyproxy@eric.h.jung" = { - installation_mode = "force_installed"; - install_url = "https://addons.mozilla.org/firefox/downloads/latest/foxyproxy-standard/latest.xpi"; - }; - }; } diff --git a/hosts/mercury/secrets/ts-key.age b/hosts/mercury/secrets/ts-key.age new file mode 100644 index 0000000..57454fe Binary files /dev/null and b/hosts/mercury/secrets/ts-key.age differ diff --git a/hosts/mercury/secrets/wireguard-privkey.age b/hosts/mercury/secrets/wireguard-privkey.age new file mode 100644 index 0000000..614778d --- /dev/null +++ b/hosts/mercury/secrets/wireguard-privkey.age @@ -0,0 +1,6 @@ +age-encryption.org/v1 +-> ssh-ed25519 azBilg GS1BNOAVj8aZPePeR4teX45zKAyCMVOBvE+GNUGCX3I +rEzBojvbakMsQCKXROwbynFTxmlcPROD7DBsl/wzfO4 +--- ESZvgbmZ3UrE6CymN/Haej2rE+FrS13SCqZZX+gcsPw + 6T݈%]K2 + J-{;@ؠ`+E2GLF*-a$`gAmp \ No newline at end of file diff --git a/justfile b/justfile index e092f23..f20863d 100644 --- a/justfile +++ b/justfile @@ -1,9 +1,8 @@ +switch: + yo sw + build: yo build deploy: yo dep root@ganymede - -switch: - yo sw - diff --git a/modules/desktop/hjem/programs/firefox.nix b/modules/desktop/hjem/programs/firefox.nix index cf9c532..6198afb 100644 --- a/modules/desktop/hjem/programs/firefox.nix +++ b/modules/desktop/hjem/programs/firefox.nix @@ -1,5 +1,4 @@ { - pkgs, lib, config, inputs, @@ -44,18 +43,13 @@ in "${profileDir}/chrome/userChrome.css".text = mkCssHacks [ # Tabs - ( - if cfg.theme == "catppuccin" - then "hide_tabs_with_one_tab" - else "hide_tabs_with_one_tab_w_window_controls" - ) + "hide_tabs_with_one_tab" "tabs_on_bottom_v2" "tab_close_button_always_on_hover" "tabs_fill_available_width" # Icons! "iconized_main_menu" - # "iconized_menubar_items" "iconized_places_context_menu" "iconized_tabs_context_menu" "icon_only_context_menu_text_controls" diff --git a/modules/desktop/hjem/programs/foot.nix b/modules/desktop/hjem/programs/foot.nix index d10db24..a28a269 100644 --- a/modules/desktop/hjem/programs/foot.nix +++ b/modules/desktop/hjem/programs/foot.nix @@ -9,8 +9,7 @@ settings = { main = { font = "Iosevka Nerd Font:size=12"; - # shell = "${pkgs.fish}/bin/fish"; - shell = "${pkgs.fish}/bin/fish -c 'br'"; + shell = "fish -c '${pkgs.broot}/bin/broot'"; }; # key-bindings.spawn-terminal = "Control+Return"; diff --git a/modules/desktop/hjem/wm/sway.nix b/modules/desktop/hjem/wm/sway.nix index 441670b..33ea869 100644 --- a/modules/desktop/hjem/wm/sway.nix +++ b/modules/desktop/hjem/wm/sway.nix @@ -35,12 +35,15 @@ pkgs.sway ]; text = '' - selection=$(printf '%s\n' logout suspend reboot shutdown | fuzzel --dmenu --prompt "power: ") + selection=$(printf '%s\n' logout hibernate suspend reboot shutdown | fuzzel --dmenu --prompt "power: ") case "$selection" in logout) swaymsg exit ;; + hibernate) + systemctl hibernate + ;; suspend) systemctl suspend ;; @@ -61,6 +64,8 @@ ${pkgs.dbus}/bin/dbus-update-activation-environment --systemd WAYLAND_DISPLAY DISPLAY SWAYSOCK XDG_CURRENT_DESKTOP GTK_USE_PORTAL NIXOS_OZONE_WL MOZ_ENABLE_WAYLAND } + seat seat0 xcursor_theme ${config.collinux.desktop.gtk.cursor_data.name} + # target title bg text indicator border client.focused #${base07} #${base00} #${base05} #${base06} #${base13} client.focused_inactive #${base03} #${base00} #${base05} #${base06} #${base03} @@ -107,8 +112,8 @@ Mod4+Return exec foot Mod4+Space exec fuzzel - Mod4+Escape exec '${powerMenu}/bin/power-menu' Mod4+b exec firefox + Mod4+Escape exec '${powerMenu}/bin/power-menu' Mod4+k exec '${batteryNotify}/bin/battery-notify' Mod4+w exec '${pkgs.iwmenu}/bin/iwmenu -l fuzzel -i font -s 2' Mod4+e exec '${pkgs.bzmenu}/bin/bzmenu -l fuzzel -i font -s 2' diff --git a/modules/desktop/nixos/fonts.nix b/modules/desktop/nixos/fonts.nix index 5ac17ec..d2e8ba9 100644 --- a/modules/desktop/nixos/fonts.nix +++ b/modules/desktop/nixos/fonts.nix @@ -2,6 +2,6 @@ fonts = { enableDefaultPackages = false; fontconfig.enable = true; - packages = [pkgs.nerd-fonts.iosevka pkgs.ibm-plex]; # for terminal (blackbox or foot or ghostty) + packages = [pkgs.nerd-fonts.iosevka pkgs.ibm-plex pkgs.liberation_ttf]; # for terminal (blackbox or foot or ghostty) }; } diff --git a/modules/desktop/nixos/programs/firefox.nix b/modules/desktop/nixos/programs/firefox.nix index 61e7b7d..9e2b8ca 100644 --- a/modules/desktop/nixos/programs/firefox.nix +++ b/modules/desktop/nixos/programs/firefox.nix @@ -25,6 +25,8 @@ in NewTabPage = false; + SearchEngines.Default = "DuckDuckGo"; + Preferences = let opt = Value: { inherit Value; @@ -32,12 +34,9 @@ in }; in { "toolkit.legacyUserProfileCustomizations.stylesheets" = opt true; - "browser.tabs.inTitlebar" = opt ( - if cfg.theme == "catppuccin" - then 0 - else 1 - ); + "browser.tabs.inTitlebar" = opt 0; "browser.tabs.hoverPreview.enabled" = opt 0; + "browser.profiles.enabled" = opt false; "browser.theme.content-theme" = opt 0; "browser.theme.toolbar-theme" = opt 0; @@ -99,7 +98,6 @@ in "datareporting.policy.dataSubmissionPolicyBypassNotification" = opt true; "browser.startup.homepage" = opt "about:blank"; - "cookiebanners.ui.desktop.enabled" = opt false; "browser.compactmode.show" = opt true; "browser.uidensity" = opt 1; @@ -107,9 +105,6 @@ in # get that AI out of my browser "browser.ml.chat.enabled" = opt false; "browser.ml.enable" = opt false; - - # use system pki certificates - "security.enterprise_roots.enabled" = opt true; }; ExtensionSettings = let @@ -130,10 +125,10 @@ in // (lib.optionalAttrs (cfg.theme == "adwaita") { "{f1128560-8b23-46c1-aa6f-fb3e79f23cf3}" = ext "gnome-adwaita-gtk4-dark"; }) - // (lib.optionalAttrs (cfg.extensions.zotero.enable) { - "zotero@chnm.gmu.edu" = { + // (lib.optionalAttrs (cfg.extensions.foxyproxy.enable) { + "foxyproxy@eric.h.jung" = { installation_mode = "force_installed"; - install_url = "https://download.zotero.org/connector/firefox/release/Zotero_Connector-5.0.181.xpi"; + install_url = "https://addons.mozilla.org/firefox/downloads/latest/foxyproxy-standard/latest.xpi"; }; }); }; diff --git a/modules/desktop/options.nix b/modules/desktop/options.nix index 3e20643..9a601fe 100644 --- a/modules/desktop/options.nix +++ b/modules/desktop/options.nix @@ -133,16 +133,8 @@ in { programs = { firefox = { enable = mkEnableOption "firefox"; - profileName = mkOption { - type = types.str; - default = config.collinux.user.name; - internal = true; - }; theme = mkThemeOption "firefox"; - extensions.zotero.enable = mkOption { - description = "install Zotero Connector for Firefox"; - default = config.collinux.desktop.programs.research.enable; - }; + extensions.foxyproxy.enable = mkEnableOption "install FoxyProxy extension"; }; foot = mkProgramOption "foot"; @@ -150,7 +142,7 @@ in { ghostty.enable = mkEnableOption "ghostty"; alacritty.enable = mkEnableOption "alacritty"; - research.enable = mkEnableOption "zathura, Xournal++, Zotero"; + research.enable = mkEnableOption "zathura, Xournal++"; }; }; }; diff --git a/modules/nix/nixos/default.nix b/modules/nix/nixos/default.nix index f39f368..3d76e36 100644 --- a/modules/nix/nixos/default.nix +++ b/modules/nix/nixos/default.nix @@ -10,8 +10,6 @@ ]; nix = { - # package = inputs.determinate.packages.${pkgs.system}.default; - gc.automatic = false; # use nh cleaner instead # Make builds run with low priority so my system stays responsive @@ -43,5 +41,5 @@ environment.systemPackages = [pkgs.cached-nix-shell]; nixpkgs.hostPlatform = "x86_64-linux"; - system.stateVersion = "25.05"; + system.stateVersion = "25.11"; } diff --git a/modules/services/nixos/default.nix b/modules/services/nixos/default.nix index c3652b5..13cdcf4 100644 --- a/modules/services/nixos/default.nix +++ b/modules/services/nixos/default.nix @@ -6,11 +6,10 @@ ./forgejo.nix ./goaccess.nix ./btopweb.nix - ./cgit - ./jta ./ganyupload - ./polaris.nix + + ./jta ./agate.nix ./minecraft.nix ./ngircd.nix diff --git a/modules/services/nixos/ganyupload/AGENTS.md b/modules/services/nixos/ganyupload/AGENTS.md new file mode 100644 index 0000000..c4d9192 --- /dev/null +++ b/modules/services/nixos/ganyupload/AGENTS.md @@ -0,0 +1,65 @@ +# Ganyupload - Agent Instructions + +## Quick Context +- Simple Go file upload service using standard `net/http`. +- Single `main.go`, two Nix files (`default.nix`, `pkg.nix`), one README. +- Embedded README is served on GET /. +- Used as a NixOS module: `collinux.services.ganyupload` enables it, configures port and upload directory via systemd environment. + +## How to Build and Test + +### Build +```bash +nix build +``` + +Alternatively, from the repo root: +```bash +nix build .#nixosConfigurations..config.system.build.toplevel +``` + +### Test +No dedicated test file. Verify locally by running with environment variables: +```bash +PORT=8080 UPLOAD_DIR=/tmp/upload go run main.go +``` + +Then test uploads: +```bash +curl -X PUT --data-binary @file.txt http://localhost:8080/file.txt +curl http://localhost:8080/ # Read embedded README +``` + +## Code Style + +**Go:** +- Keep `main.go` clean; use `gofmt -w .`. +- Match existing error handling (early return, `log.Printf` for warnings, `log.Fatalf` for fatal errors). +- Path traversal prevention is critical; do not weaken `filepath.Clean` + `..` prefix checks. + +**Nix:** +- Follow `/home/collin/nixos/AGENTS.md` conventions (2-space indent, common arg pattern, `lib.mkIf` for gating). +- `default.nix` imports `mkCaddyCfg.nix` to integrate with Caddy reverse proxy; do not remove that import. + +## Key Implementation Notes + +1. **Embedded README:** The README file is embedded at compile time using `//go:embed README`. If you update README, rebuild to reflect changes. +2. **Environment Variables:** + - `UPLOAD_DIR`: Defaults to `.`, overridden by systemd service to `/media/ganyupload`. + - `PORT`: Defaults to `8080`, set by systemd to the configured `cfg.port`. +3. **NixOS Integration:** The service runs as user/group `ganyupload` with home `/var/lib/ganyupload`. Ensure the service has write permission to `UPLOAD_DIR`. +4. **Caddy Integration:** `default.nix` imports `mkCaddyCfg.nix` to configure Caddy as a reverse proxy. Changes to hostname or port must be reflected in both Nix config and the Caddy rule. + +## Common Tasks + +- **Update Go code:** Edit `main.go`, test locally, then nix build to verify. +- **Change upload directory or port:** Update `default.nix` (systemd environment or `cfg.port`). +- **Update README:** Edit README file, then rebuild (`nix build`) so the embedded version updates. +- **Debug NixOS module:** Check `/etc/systemd/system/ganyupload.service` for actual service config after rebuild. + +## Validation + +After changes: +1. Run `nix build` from this directory or `nix build .#nixosConfigurations..config.system.build.toplevel` from repo root. +2. If applicable, test via `PORT=8080 UPLOAD_DIR=/tmp go run main.go`. +3. Verify no Nix lint issues (use repo-level `yo test` or `nix run nixpkgs#alejandra -- .` for formatting). diff --git a/modules/services/nixos/ganyupload/main.go b/modules/services/nixos/ganyupload/main.go index 6aa219e..75852c2 100644 --- a/modules/services/nixos/ganyupload/main.go +++ b/modules/services/nixos/ganyupload/main.go @@ -2,11 +2,14 @@ package main import ( _ "embed" + "fmt" + "html" "io" "log" "net/http" "os" "path/filepath" + "sort" "strings" ) @@ -15,6 +18,42 @@ var readme string var uploadDir = "." +// cleanPath safely cleans and validates a path to prevent traversal attacks +func cleanPath(filename string) (string, error) { + // Remove leading slash + filename = strings.TrimPrefix(filename, "/") + if filename == "" { + return "", nil + } + + // Clean the path + filename = filepath.Clean(filename) + + // Reject if it tries to go up + if strings.HasPrefix(filename, "..") || strings.Contains(filename, "/../") { + return "", fmt.Errorf("invalid filename: path traversal not allowed") + } + + return filename, nil +} + +// isPathSafe checks that the resolved fullPath stays within uploadDir +func isPathSafe(uploadDir, fullPath string) bool { + // Resolve both paths to absolute to catch symlink attacks + absUploadDir, err := filepath.Abs(uploadDir) + if err != nil { + return false + } + absFullPath, err := filepath.Abs(fullPath) + if err != nil { + return false + } + + // Ensure the resolved path starts with uploadDir + return strings.HasPrefix(absFullPath, absUploadDir+string(filepath.Separator)) || + absFullPath == absUploadDir +} + func handleRoot(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodGet { http.Error(w, "method not allowed", http.StatusMethodNotAllowed) @@ -32,27 +71,37 @@ func handleUpload(w http.ResponseWriter, r *http.Request) { return } + // Handle GET requests for downloads and directory listing + if r.Method == http.MethodGet { + handleDownload(w, r) + return + } + if r.Method != http.MethodPut { http.Error(w, "method not allowed", http.StatusMethodNotAllowed) return } // Extract filename from URL path, preventing path traversal - filename := strings.TrimPrefix(r.URL.Path, "/") - if filename == "" { - http.Error(w, "filename required", http.StatusBadRequest) + filename, err := cleanPath(r.URL.Path) + if err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) return } - // Prevent path traversal - filename = filepath.Clean(filename) - if strings.HasPrefix(filename, "..") { - http.Error(w, "invalid filename", http.StatusBadRequest) + if filename == "" { + http.Error(w, "filename required", http.StatusBadRequest) return } fullPath := filepath.Join(uploadDir, filename) + // Verify the path is safe + if !isPathSafe(uploadDir, fullPath) { + http.Error(w, "invalid path", http.StatusBadRequest) + return + } + // Ensure the directory exists dir := filepath.Dir(fullPath) if err := os.MkdirAll(dir, 0755); err != nil { @@ -85,6 +134,155 @@ func handleUpload(w http.ResponseWriter, r *http.Request) { log.Printf("uploaded %s (%d bytes)", filename, written) } +func handleDownload(w http.ResponseWriter, r *http.Request) { + // Extract filename from URL path + filename, err := cleanPath(r.URL.Path) + if err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + + fullPath := filepath.Join(uploadDir, filename) + + // Verify the path is safe + if !isPathSafe(uploadDir, fullPath) { + http.Error(w, "invalid path", http.StatusBadRequest) + return + } + + fileInfo, err := os.Stat(fullPath) + if err != nil { + if os.IsNotExist(err) { + http.Error(w, "not found", http.StatusNotFound) + } else { + log.Printf("failed to stat: %v", err) + http.Error(w, "internal server error", http.StatusInternalServerError) + } + return + } + + // If it's a directory, list its contents + if fileInfo.IsDir() { + handleDirList(w, r, uploadDir, filename, fullPath) + return + } + + // Serve the file + http.ServeFile(w, r, fullPath) +} + +func handleDirList(w http.ResponseWriter, r *http.Request, uploadDir, relPath, fullPath string) { + entries, err := os.ReadDir(fullPath) + if err != nil { + log.Printf("failed to read directory: %v", err) + http.Error(w, "internal server error", http.StatusInternalServerError) + return + } + + // Sort entries by name + sort.Slice(entries, func(i, j int) bool { + return entries[i].Name() < entries[j].Name() + }) + + // Build breadcrumb navigation + pathParts := strings.Split(strings.Trim(relPath, "/"), "/") + if relPath == "" { + pathParts = []string{} + } + + w.Header().Set("Content-Type", "text/html; charset=utf-8") + w.WriteHeader(http.StatusOK) + + fmt.Fprintf(w, ` + + + + + Directory: %s + + + +

Directory: %s

+`, html.EscapeString(relPath), html.EscapeString(relPath)) + + // Breadcrumb navigation + fmt.Fprint(w, ` `) + + // Directory listing + fmt.Fprint(w, ` +`) + for _, entry := range entries { + name := entry.Name() + safeName := html.EscapeString(name) + + if entry.IsDir() { + // For directories, add trailing slash to the link + linkPath := relPath + "/" + name + if relPath == "" { + linkPath = "/" + name + } + linkPath = strings.TrimPrefix(linkPath, "/") + fmt.Fprintf(w, ` +`, html.EscapeString(linkPath), safeName) + } else { + info, _ := entry.Info() + linkPath := relPath + "/" + name + if relPath == "" { + linkPath = "/" + name + } + linkPath = strings.TrimPrefix(linkPath, "/") + size := formatSize(info.Size()) + fmt.Fprintf(w, ` +`, html.EscapeString(linkPath), safeName, size) + } + } + fmt.Fprint(w, `
%s/-
%s%s
+ + +`) +} + +func formatSize(bytes int64) string { + const ( + KB = 1024 + MB = KB * 1024 + GB = MB * 1024 + ) + + switch { + case bytes < KB: + return fmt.Sprintf("%d B", bytes) + case bytes < MB: + return fmt.Sprintf("%.1f KB", float64(bytes)/KB) + case bytes < GB: + return fmt.Sprintf("%.1f MB", float64(bytes)/MB) + default: + return fmt.Sprintf("%.1f GB", float64(bytes)/GB) + } +} + func main() { if envUploadDir := os.Getenv("UPLOAD_DIR"); envUploadDir != "" { uploadDir = envUploadDir diff --git a/modules/services/nixos/jta/main.go b/modules/services/nixos/jta/main.go index 1390d7f..e7b5a0f 100644 --- a/modules/services/nixos/jta/main.go +++ b/modules/services/nixos/jta/main.go @@ -36,19 +36,35 @@ var pageTmpl = template.Must(template.New("md_template.html").Parse(mdTemplate)) const authCookieName = "jta_auth" -func isAuthenticated(r *http.Request) bool { +func isAuthenticated(w http.ResponseWriter, r *http.Request) bool { expectedHash := strings.TrimSpace(os.Getenv("AUTH_PASSWORD_HASH")) if expectedHash == "" { return false } - cookie, err := r.Cookie(authCookieName) - if err != nil { - return false + // Try cookie first + if cookie, err := r.Cookie(authCookieName); err == nil { + providedHash := strings.TrimSpace(cookie.Value) + if providedHash != "" && strings.EqualFold(providedHash, expectedHash) { + return true + } + } + + // Fallback to URL parameter + paramHash := strings.TrimSpace(r.URL.Query().Get(authCookieName)) + if paramHash != "" && strings.EqualFold(paramHash, expectedHash) { + http.SetCookie(w, &http.Cookie{ + Name: authCookieName, + Value: paramHash, + Path: "/", + HttpOnly: true, // Security: Protects against XSS cookie theft + Secure: false, // Set to true if you are hosting over HTTPS + SameSite: http.SameSiteLaxMode, // Prevents CSRF vulnerabilities + }) + return true } - providedHash := strings.TrimSpace(cookie.Value) - return strings.EqualFold(providedHash, expectedHash) + return false } func serveMarkdown(w http.ResponseWriter, r *http.Request) { @@ -101,7 +117,7 @@ func main() { return } - if !isAuthenticated(r) { + if !isAuthenticated(w, r) { http.Error(w, "unauthorized", http.StatusUnauthorized) return } diff --git a/modules/services/nixos/minecraft.nix b/modules/services/nixos/minecraft.nix index 1b8b047..7e12a61 100644 --- a/modules/services/nixos/minecraft.nix +++ b/modules/services/nixos/minecraft.nix @@ -6,7 +6,7 @@ cfg = config.collinux.services.minecraft; in lib.mkIf cfg.enable { - networking.firewall.allowedUDPPorts = lib.optional (cfg.listenAddr == "0.0.0.0") cfg.port; + networking.firewall.allowedUDPPorts = lib.optional cfg.public cfg.port; virtualisation.oci-containers.containers."Minecraft" = { environment = { @@ -15,14 +15,20 @@ in SERVER_NAME = "YServer"; TZ = config.time.timeZone; - VERSION = "1.26.3.1"; + VERSION = "1.26.20.5"; CONTENT_LOG_FILE_ENABLED = "false"; ALLOW_CHEATS = "false"; DIFFICULTY = "1"; }; image = "itzg/minecraft-bedrock-server"; - ports = ["${cfg.listenAddr}:${toString cfg.port}:19132/udp"]; + ports = [ + "${ + if cfg.public + then "0.0.0.0" + else "127.0.0.1" + }:${toString cfg.port}:19132/udp" + ]; volumes = ["/var/lib/minecraft/:/data"]; extraOptions = ["--no-healthcheck"]; diff --git a/modules/services/nixos/ngircd.nix b/modules/services/nixos/ngircd.nix index d06a497..ac93f06 100644 --- a/modules/services/nixos/ngircd.nix +++ b/modules/services/nixos/ngircd.nix @@ -6,7 +6,7 @@ cfg = config.collinux.services.ngircd; in lib.mkIf cfg.enable { - networking.firewall.allowedTCPPorts = [cfg.port]; + networking.firewall.allowedTCPPorts = lib.optional cfg.public cfg.port; services.ngircd = { enable = true; @@ -16,7 +16,11 @@ in Info = Ganymede IRC Chat AdminInfo1 = Collin - Listen = 0.0.0.0 + Listen = ${ + if cfg.public + then "0.0.0.0" + else "127.0.0.1" + } Ports = ${toString cfg.port} [Channel] diff --git a/modules/services/nixos/openssh.nix b/modules/services/nixos/openssh.nix index 1ce1abc..98f8b87 100644 --- a/modules/services/nixos/openssh.nix +++ b/modules/services/nixos/openssh.nix @@ -6,15 +6,9 @@ ... }: let cfg = config.collinux.services.sshd; - - authorizedKeys = - hosts - |> builtins.mapAttrs (_: data: data.user_pubkey or null) - |> builtins.attrValues - |> builtins.filter (x: x != null); in { config = lib.mkIf cfg.enable { - networking.firewall.allowedTCPPorts = [cfg.port]; + networking.firewall.allowedTCPPorts = lib.optional cfg.public cfg.port; services.openssh = { enable = true; @@ -29,7 +23,10 @@ in { listenAddresses = [ { - addr = cfg.listenAddr; + addr = + if cfg.public + then "0.0.0.0" + else "127.0.0.1"; port = cfg.port; } ]; @@ -46,7 +43,7 @@ in { extraConfig = lib.concatStringsSep "\n" [ "Match LocalPort ${toString cfg.port}" ( - if cfg.otp + if cfg.conf.otp then '' ChallengeResponseAuthentication yes PubkeyAuthentication yes @@ -58,11 +55,11 @@ in { AuthenticationMethods publickey '' ) - (lib.optionalString cfg.rootLogin "PermitRootLogin yes") + (lib.optionalString cfg.conf.rootLogin "PermitRootLogin yes") ]; }; - security.pam.services = lib.optionalAttrs cfg.otp { + security.pam.services = lib.optionalAttrs cfg.conf.otp { login.googleAuthenticator.enable = true; sshd.text = '' @@ -78,8 +75,16 @@ in { ''; }; - users.users.${config.collinux.user.name}.openssh.authorizedKeys.keys = authorizedKeys; - users.users."root".openssh.authorizedKeys.keys = lib.mkIf cfg.rootLogin authorizedKeys; + users.users = let + k.openssh.authorizedKeys.keys = + hosts + |> builtins.mapAttrs (_: data: data.user_pubkey or null) + |> builtins.attrValues + |> builtins.filter (x: x != null); + in { + ${config.collinux.user.name} = k; + "root" = lib.mkIf cfg.conf.rootLogin k; + }; systemd.services."openssh" = { after = lib.mkAfter ["network-online.target"]; diff --git a/modules/services/nixos/polaris.nix b/modules/services/nixos/polaris.nix deleted file mode 100644 index 89f5ef3..0000000 --- a/modules/services/nixos/polaris.nix +++ /dev/null @@ -1,26 +0,0 @@ -{ - lib, - config, - ... -}: let - cfg = config.collinux.services.polaris; -in { - imports = [ - (import ./mkCaddyCfg.nix cfg) - ]; - - config = lib.mkIf cfg.enable { - services.polaris = { - enable = true; - port = cfg.port; - settings = { - mount_dirs = [ - { - name = "Ganymede Library"; - source = "/media/library/music"; - } - ]; - }; - }; - }; -} diff --git a/modules/services/options.nix b/modules/services/options.nix index 8918781..1888f5b 100644 --- a/modules/services/options.nix +++ b/modules/services/options.nix @@ -55,70 +55,73 @@ in { options.collinux.services = { sshd = { enable = mkEnableOption "OpenSSH server"; - port = mkOption { description = "Port to run on"; type = lib.types.port; + default = 22; }; + public = mkEnableOption "whether to make this service accessable over the internet"; - listenAddr = mkOption { - description = "Address to listen on"; - type = lib.types.str; - default = "127.0.0.1"; + conf = { + otp = mkEnableOption "Whether to require TOTP (Google Authenticator) 2fa codes to login"; + rootLogin = mkEnableOption "Whether to allow root login"; + }; + }; + minecraft = { + enable = mkEnableOption "Minecraft bedrock server"; + port = mkOption { + description = "port to run on"; + type = lib.types.port; + default = 19132; + }; + + public = mkEnableOption "whether to make this service accessable over the internet"; + }; + ngircd = { + enable = mkEnableOption "ncircd IRC server"; + port = mkOption { + type = lib.types.port; + default = 6667; }; - otp = mkEnableOption "Whether to require TOTP (Google Authenticator) 2fa codes"; - rootLogin = mkEnableOption "Whether to allow root login"; + public = mkEnableOption "whether to make this service accessable over the internet"; + }; + agate = { + enable = mkEnableOption "Agate Gemini Protocol Server"; + publicUrl = mkOption { + description = "domain to run this gemini server on"; + type = lib.types.str; + }; }; + jta = webserviceOptions { + service_name = "jta"; + default_port = 8072; + }; + ganyupload = webserviceOptions { + service_name = "ganyupload"; + default_port = 8073; + }; forgejo = webserviceOptions { service_name = "forgejo"; default_port = 8010; }; - btopweb = webserviceOptions { service_name = "btopweb"; default_port = 8017; }; - goaccess = webserviceOptions { service_name = "goaccess"; reverse_proxy = false; }; - cgit = webserviceOptions { service_name = "cgit"; reverse_proxy = false; }; - - agate = { - enable = mkEnableOption "Agate Gemini Protocol Server"; - publicUrl = mkOption { - description = "domain to run this gemini server on"; - type = lib.types.str; - }; - }; - - polaris = webserviceOptions { - service_name = "polaris"; - default_port = 8079; - }; - qbittorrent = webserviceOptions { service_name = "qbittorrent"; default_port = 8076; }; - - jta = webserviceOptions { - service_name = "jta"; - default_port = 8072; - }; - - ganyupload = webserviceOptions { - service_name = "ganyupload"; - default_port = 8073; - }; - copyparty = (webserviceOptions { service_name = "copyparty"; @@ -146,27 +149,6 @@ in { }; }; - ngircd = { - enable = mkEnableOption "ncircd IRC server"; - port = mkOption { - type = lib.types.port; - default = 6667; - }; - }; - - minecraft = { - enable = mkEnableOption "Minecraft bedrock server"; - listenAddr = mkOption { - description = "Address to listen on"; - type = ipAddr; - default = "127.0.0.1"; - }; - port = mkOption { - type = lib.types.port; - default = 19132; - }; - }; - caddy = { enable = mkEnableOption "caddy https server"; envFile = mkOption { diff --git a/modules/system/nixos/networking/tailscale.nix b/modules/system/nixos/networking/tailscale.nix deleted file mode 100644 index 552ec8b..0000000 --- a/modules/system/nixos/networking/tailscale.nix +++ /dev/null @@ -1,33 +0,0 @@ -{ - config, - pkgs, - lib, - ... -}: let - cfg = config.collinux.system.network.tailscale; -in - lib.mkIf cfg.enable { - services.tailscale = { - enable = true; - useRoutingFeatures = "both"; - }; - - networking.firewall = { - trustedInterfaces = ["tailscale0"]; - allowedUDPPorts = [config.services.tailscale.port]; - }; - - systemd.services."tailscaled" = - if config.collinux.services.selfhost.headscale.enable - then { - # don't start tailscale until after headscale starts - wants = lib.mkForce ["network.target" "headscale.target"]; - after = lib.mkForce ["network.target" "headscale.target"]; - } - else { - wants = lib.mkForce ["network.target"]; - after = lib.mkForce ["network.target"]; - }; - - environment.systemPackages = [pkgs.tailscale]; - } diff --git a/modules/system/options.nix b/modules/system/options.nix index 8126b6a..a6db575 100644 --- a/modules/system/options.nix +++ b/modules/system/options.nix @@ -64,8 +64,6 @@ in { }; dynamic = lib.mkEnableOption "Enable dynamically joining wireless networks with iwd"; }; - - tailscale.enable = mkEnableOption "tailscale"; }; audio.enable = mkEnableOption "pipewire and wireplumber"; diff --git a/modules/terminal/hjem/programs/broot.nix b/modules/terminal/hjem/programs/broot.nix index 59f16aa..cd425bd 100644 --- a/modules/terminal/hjem/programs/broot.nix +++ b/modules/terminal/hjem/programs/broot.nix @@ -7,9 +7,10 @@ cfg = config.collinux.terminal.programs.broot; conf = { - imports = [ - "skins/catppuccin-mocha.hjson" - ]; + imports = + if config.collinux.theme == "catppuccin" + then ["skins/catppuccin-mocha.hjson"] + else ["skins/dark-gruvbox.hjson"]; default_flags = "-g"; icon_theme = "nerdfont"; diff --git a/modules/terminal/hjem/programs/git.nix b/modules/terminal/hjem/programs/git.nix index 918381c..85db4ab 100644 --- a/modules/terminal/hjem/programs/git.nix +++ b/modules/terminal/hjem/programs/git.nix @@ -8,15 +8,16 @@ git_config = lib.mkMerge [ { + user = { + email = cfg.userEmail; + name = cfg.userName; + }; alias = { stage = "add"; unstage = "restore --staged"; }; init.defaultBranch = "main"; - user = { - email = cfg.userEmail; - name = cfg.userName; - }; + push.autoSetupRemote = true; } (lib.mkIf cfg.installKey { # commit signing diff --git a/modules/terminal/hjem/programs/tmux.nix b/modules/terminal/hjem/programs/tmux.nix index 2c87212..2d7155e 100644 --- a/modules/terminal/hjem/programs/tmux.nix +++ b/modules/terminal/hjem/programs/tmux.nix @@ -7,30 +7,30 @@ }: let cfg = config.collinux.terminal.programs.tmux; in { - imports = [ - inputs.tmux-tsunami.hjemModules.tsunami - ]; + # imports = [ + # inputs.tmux-tsunami.hjemModules.tsunami + # ]; - config = lib.mkIf cfg.enable { - tsunami = { - enable = true; + # config = lib.mkIf cfg.enable { + # tsunami = { + # enable = true; - theme = { - bg = "#${config.collinux.palette.base00}"; - bg_dark = "#${config.collinux.palette.base01}"; - }; + # theme = { + # bg = "#${config.collinux.palette.base00}"; + # bg_dark = "#${config.collinux.palette.base01}"; + # }; - keys.leader = [ - { - name = "Lazygit"; - key = "C-g"; - exec = "display-popup -E -w 80% -h 80% -x C -y C -d '#{?@default-path,#{@default-path},#{pane_current_path}}' ${lib.getExe pkgs.lazygit}"; - } - ]; - confs."main" = '' - set -g mouse on - set-window-option -g mode-keys vi - ''; - }; - }; + # keys.leader = [ + # { + # name = "Lazygit"; + # key = "C-g"; + # exec = "display-popup -E -w 80% -h 80% -x C -y C -d '#{?@default-path,#{@default-path},#{pane_current_path}}' ${lib.getExe pkgs.lazygit}"; + # } + # ]; + # confs."main" = '' + # set -g mouse on + # set-window-option -g mode-keys vi + # ''; + # }; + # }; } diff --git a/pkgs/obsidian.nix b/pkgs/obsidian.nix new file mode 100644 index 0000000..b54f6a7 --- /dev/null +++ b/pkgs/obsidian.nix @@ -0,0 +1,24 @@ +{pkgs, ...}: let + version = "1.10.3"; +in + pkgs.stdenv.mkDerivation { + pname = "obsidian"; + inherit version; + + src = pkgs.fetchurl { + url = "https://github.com/obsidianmd/obsidian-releases/releases/download/v${version}/obsidian-${version}.tar.gz"; + hash = "sha256-zI8CEhHt6VIGfh4kj4YxdfxJPJaf7mhMVnM45TmaoI0="; + }; + nativeBuildInputs = [pkgs.makeWrapper]; + + installPhase = '' + runHook preInstall + mkdir -p $out/bin + makeWrapper ${pkgs.electron_39}/bin/electron $out/bin/obsidian \ + --add-flags $out/share/obsidian/app.asar \ + --add-flags "\''${NIXOS_OZONE_WL:+\''${WAYLAND_DISPLAY:+--ozone-platform=wayland --enable-wayland-ime=true --wayland-text-input-version=3}}" + install -m 444 -D resources/app.asar $out/share/obsidian/app.asar + install -m 444 -D resources/obsidian.asar $out/share/obsidian/obsidian.asar + runHook postInstall + ''; + } diff --git a/secrets.nix b/secrets.nix index 58b0931..e8a737c 100644 --- a/secrets.nix +++ b/secrets.nix @@ -7,5 +7,6 @@ in { "hosts/ganymede/secrets/williams-psk.age".publicKeys = [ganymede.host_pubkey]; "hosts/ganymede/secrets/collin-copyparty-password.age".publicKeys = [mercury.host_pubkey ganymede.host_pubkey]; "hosts/ganymede/secrets/collin-forgejo-password.age".publicKeys = [mercury.host_pubkey ganymede.host_pubkey]; - "hosts/ganymede/secrets/wireguard-pk.age".publicKeys = [ganymede.host_pubkey]; + + "hosts/mercury/secrets/ts-key.age".publicKeys = [mercury.host_pubkey]; } diff --git a/services/caddy.nix b/services/caddy.nix new file mode 100644 index 0000000..9870af0 --- /dev/null +++ b/services/caddy.nix @@ -0,0 +1,104 @@ +{ + lib, + config, + ... +}: let + cfg = config.caddy; +in { + options = { + caddy = { + globalConfig = lib.mkOption { + description = "Configuration for top level stuff (ex. dns server api keys)"; + type = lib.types.lines; + default = ""; + }; + tld = lib.mkOption { + description = "Your publically accessable domain name"; + type = lib.types.str; + }; + hostname = lib.mkOption { + description = "Your computers hostname"; + type = lib.types.str; + }; + virtualHosts = lib.mkOption { + description = "Magically configure caddy for services"; + type = lib.types.attrsOf (lib.types.submodule ({config, ...}: { + options = { + serviceName = lib.mkOption { + type = lib.types.str; + default = config._module.args.name; + internal = true; + }; + access = lib.mkOption { + type = lib.types.enum ["public" "private"]; + description = "public: accessable at {serviceName}.my.tld. private: accessable at {serviceName}.{hostname}"; + }; + logFile = lib.mkOption { + type = lib.types.str; + description = "what to name the log file under /var/log/caddy"; + default = "${config.serviceName}.log"; + }; + reverseProxy = lib.mkOption { + type = with lib.types; nullOr str; + description = "configure caddy to reverse-proxy this port or unix domain socket (ex. 127.0.0.1:8080 or unix//var/run/my_socket)"; + default = null; + }; + virtualHostConfig = lib.mkOption { + type = lib.types.lines; + description = "manual config lines to add to the caddy config"; + default = ""; + }; + }; + })); + default = {}; + }; + }; + }; + config = { + configData."caddyfile".text = + '' + { + ${cfg.globalConfig} + } + '' + ++ (cfg.virtualHosts + |> map (vhost_cfg: let + target = + if vhost_cfg.access == "public" + then + ( + if vhost_cfg.serviceName != "root" + then "${vhost_cfg.serviceName}.${cfg.tld}" + else cfg.tld + ) + else + ( + if vhost_cfg.serviceName != "root" + then "${vhost_cfg.serviceName}.${cfg.hostName}" + else cfg.hostname + ); + in '' + log { + output file /var/log/caddy/${vhost_cfg.logFile} + } + + ${target} { + ${ + if vhost_cfg.access == "private" + then "tls internal" + else "" + } + ${ + if vhost_cfg.reverseProxy != null + then "reverse_proxy ${vhost_cfg.reverseProxy}" + else "" + } + ${vhost_cfg.virtualHostConfig} + } + '') + |> builtins.concatStringsSep "\n\n"); + + services."caddy".systemd.service = { + }; + }; +} diff --git a/services/goaccess.nix b/services/goaccess.nix new file mode 100644 index 0000000..dd6b26d --- /dev/null +++ b/services/goaccess.nix @@ -0,0 +1,136 @@ +{ + lib, + pkgs, + config, + inputs, + ... +}: let + cfg = config.goaccess; +in { + options."caddy-goaccess" = { + timeZone = lib.mkOption { + type = lib.types.str; + description = "time zone"; + }; + logFile = lib.mkOption { + type = lib.types.str; + description = "caddy log file for goaccess to read (must be chmod 775)"; + }; + websocketUrl = lib.mkOption { + type = lib.types.str; + description = "url that the frontend should use to attach to the service websocket"; + }; + }; + config = let + settings = { + unix-socket = "/run/caddy-goaccess/socket"; + ws-url = cfg.websocketUrl; + + date-format = "%s"; + log-format = "CADDY"; + tz = cfg.timeZone; + log-file = cfg.logFile; + geoip-database = inputs.geolite-db; + + output = "/run/caddy-goaccess/index.html"; + real-time-html = "true"; + external-assets = "true"; + all-static-files = "false"; + html-report-title = "stats@ganymede"; + hl-header = "true"; + agent-list = "false"; + with-output-resolver = "false"; + http-method = "yes"; + http-protocol = "yes"; + "4xx-to-unique-count" = "false"; + ignore-crawlers = "false"; + crawlers-only = "false"; + unknowns-as-crawlers = "false"; + real-os = "true"; + }; + in { + configData."goaccess.conf".text = settings |> builtins.mapAttrs (k: v: "${k} ${toString v}") |> builtins.attrValues |> lib.concatStringsSep "\n"; + services."caddy-goaccess" = { + systemd.socket = { + description = "caddy-goaccess uds"; + socketConfig = { + ListenStream = "/run/caddy-goaccess/socket"; + SocketMode = "0660"; + SocketUser = "goaccess"; + SocketGroup = "caddy"; + }; + wantedBy = ["sockets.target"]; + }; + + systemd.service = { + description = "GoAccess Real-Time Log Analyzer"; + restartIfChanged = true; + wants = ["network-online.target" "caddy.service"]; + after = ["network-online.target" "caddy.service"]; + requires = ["caddy-goaccess.socket"]; + + serviceConfig = { + Type = "simple"; + + DynamicUser = true; + SupplimentaryGroup = "caddy"; # to read caddy log files + RuntimeDirectory = "caddy-goaccess"; # /run/caddy-goaccess + ExecStart = "${pkgs.goaccess}/bin/goaccess -p ${config.configData."goaccess.conf".path}"; + + # hardening stuff + AmbientCapabilities = []; + CapabilityBoundingSet = [ + "~CAP_RAWIO" + "~CAP_MKNOD" + "~CAP_AUDIT_CONTROL" + "~CAP_AUDIT_READ" + "~CAP_AUDIT_WRITE" + "~CAP_SYS_BOOT" + "~CAP_SYS_TIME" + "~CAP_SYS_MODULE" + "~CAP_SYS_PACCT" + "~CAP_LEASE" + "~CAP_LINUX_IMMUTABLE" + "~CAP_IPC_LOCK" + "~CAP_BLOCK_SUSPEND" + "~CAP_WAKE_ALARM" + "~CAP_SYS_TTY_CONFIG" + "~CAP_MAC_ADMIN" + "~CAP_MAC_OVERRIDE" + "~CAP_NET_ADMIN" + "~CAP_NET_BROADCAST" + "~CAP_NET_RAW" + "~CAP_SYS_ADMIN" + "~CAP_SYS_PTRACE" + "~CAP_SYSLOG" + ]; + DevicePolicy = "closed"; + KeyringMode = "private"; + LockPersonality = true; + NoNewPrivileges = true; + PrivateDevices = true; + PrivateMounts = true; + PrivateTmp = true; + ProtectClock = true; + ProtectControlGroups = true; + ProtectHome = true; + ProtectHostname = true; + ProtectKernelLogs = true; + ProtectKernelModules = true; + ProtectKernelTunables = true; + ProtectSystem = "full"; + RemoveIPC = true; + RestrictAddressFamilies = [ + "AF_UNIX" + "AF_INET" + "AF_INET6" + ]; + RestrictNamespaces = true; + RestrictRealtime = true; + }; + + wantedBy = ["multi-user.target"]; + }; + }; + }; +} diff --git a/tmux-tsunami/flake.lock b/tmux-tsunami/flake.lock new file mode 100644 index 0000000..63066a7 --- /dev/null +++ b/tmux-tsunami/flake.lock @@ -0,0 +1,42 @@ +{ + "nodes": { + "nix-std": { + "locked": { + "lastModified": 1710870712, + "narHash": "sha256-e+7MJF2gsgTBuOWv4mCimSP0D9+naeFSw9a7N3yEmv4=", + "owner": "chessai", + "repo": "nix-std", + "rev": "31bbc925750cc9d8f828fe55cee1a2bd985e0c00", + "type": "github" + }, + "original": { + "owner": "chessai", + "repo": "nix-std", + "type": "github" + } + }, + "nixpkgs": { + "locked": { + "lastModified": 1759578544, + "narHash": "sha256-GFvucgx8PDytuftANATwu7IQMydblpMDWNZwjg2Oyb4=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "b0bb1a55163139b0b24228143567f0c1c8c178f4", + "type": "github" + }, + "original": { + "owner": "NixOS", + "repo": "nixpkgs", + "type": "github" + } + }, + "root": { + "inputs": { + "nix-std": "nix-std", + "nixpkgs": "nixpkgs" + } + } + }, + "root": "root", + "version": 7 +} diff --git a/tmux-tsunami/flake.nix b/tmux-tsunami/flake.nix new file mode 100644 index 0000000..e74feb7 --- /dev/null +++ b/tmux-tsunami/flake.nix @@ -0,0 +1,18 @@ +{ + inputs = { + nixpkgs.url = "github:NixOS/nixpkgs"; + nix-std.url = "github:chessai/nix-std"; + }; + outputs = inputs: { + hjemModules.tsunami = import ./modules/hjem.nix { + std = inputs.nix-std.lib; + }; + + packages."x86_64-linux".tsunamiInstaller = let + installer = import ./modules/installer.nix { + pkgs = import inputs.nixpkgs {system = "x86_64-linux";}; + }; + in + installer.mkInstaller; + }; +} diff --git a/tmux-tsunami/modules/common/bar.nix b/tmux-tsunami/modules/common/bar.nix new file mode 100644 index 0000000..cbb162a --- /dev/null +++ b/tmux-tsunami/modules/common/bar.nix @@ -0,0 +1,69 @@ +{ + config, + lib, + tsunamiLib, + ... +}: let + inherit (tsunamiLib) scriptPath; +in { + options = let + hexColor = lib.types.strMatching "^#?([0-9a-fA-F]{6}|[0-9a-fA-F]{3})$"; + in { + tsunami.theme = { + bg = lib.mkOption { + type = hexColor; + default = "#fff"; + }; + bg_dark = lib.mkOption { + type = hexColor; + default = "#000"; + }; + }; + }; + config = { + tsunami = { + scripts = { + "battery" = '' + energy_now=$(cat /sys/class/power_supply/BAT0/energy_now) + energy_full=$(cat /sys/class/power_supply/BAT0/energy_full) + percentage=$((energy_now * 100 / energy_full)) + printf "%.0f%%" "$percentage" + ''; + + "bar" = '' + tmux set -g status on + + tmux set -g status-position bottom + tmux set -g status-justify absolute-centre + tmux set -g status-bg "${config.tsunami.theme.bg}" + + tmux set -g status-left-style fg=green,bold + tmux set -g status-left " #{client_session}" + + tmux set -g window-status-style fg=color243 + tmux set -g window-status-format " #I " + tmux set -g window-status-current-style fg=color12,bold + tmux set -g window-status-current-format " #I " + tmux set -g window-status-separator "" + + tmux set -g status-right "#[fg=color243]%l:%M #[fg=red]#(${scriptPath "battery"}) " + + ## pane borders + tmux set -g pane-border-style fg=${config.tsunami.theme.bg_dark},bg=${config.tsunami.theme.bg_dark} + tmux set -g pane-active-border-style fg=${config.tsunami.theme.bg_dark},bg=${config.tsunami.theme.bg_dark} + + ## pane backgrounds + # Set the foreground/background color for the active window + tmux set -g window-active-style bg=${config.tsunami.theme.bg} + + # Set the foreground/background color for all other windows + tmux set -g window-style bg=${config.tsunami.theme.bg_dark} + ''; + }; + + confs."bar" = '' + run-shell ${scriptPath "bar"} + ''; + }; + }; +} diff --git a/tmux-tsunami/modules/common/defaultGlobalKeys.nix b/tmux-tsunami/modules/common/defaultGlobalKeys.nix new file mode 100644 index 0000000..fc3c669 --- /dev/null +++ b/tmux-tsunami/modules/common/defaultGlobalKeys.nix @@ -0,0 +1,66 @@ +{ + pkgs, + tsunamiLib, + ... +}: let + inherit (tsunamiLib) scriptPath; +in { + tsunami.scripts."split" = '' + width=$(tmux display -p "#{pane_width}") + height=$(tmux display -p "#{pane_height}") + + if (( $(echo "$width / $height > 2.5" | ${pkgs.bc}/bin/bc -l) )); then + tmux split-window -h "$@" + else + tmux split-window -v "$@" + fi + ''; + + tsunami.scripts."clean-sessions" = '' + current="$(tmux display -p '#{session_name}')" + + tmux list-sessions -F '#{session_name}' | while read -r line; do + if [[ "$line" != "$current" && "$line" =~ ^[[:digit:]]+$ ]]; then + tmux kill-session -t "$line" + fi + done + ''; + + tsunami.keys.global = [ + { + key = "C-w"; + exec = "kill-pane"; + } + { + key = "M-z"; + exec = "resize-pane -Z"; + } + { + key = "C-Enter"; + exec = ''run-shell "${scriptPath "split"} -c '#{?@default-path,#{@default-path},#{pane_current_path}}'"''; + } + { + key = "C-t"; + exec = "new-window -c '#{?@default-path,#{@default-path},#{pane_current_path}}'"; + } + { + key = "C-Tab"; + exec = "next-window"; + } + { + key = "C-S-Tab"; + exec = "previous-window"; + } + ]; + + tsunami.confs."windows" = '' + unbind -n MouseDown3Pane + set -g allow-rename on + set -g automatic-rename off + set -g renumber-windows on + set -g base-index 1 + + set-hook -ag client-detached 'run-shell ${scriptPath "clean-sessions"}' + set-hook -ag client-session-changed 'run-shell ${scriptPath "clean-sessions"}' + ''; +} diff --git a/tmux-tsunami/modules/common/defaultLeaderMenu.nix b/tmux-tsunami/modules/common/defaultLeaderMenu.nix new file mode 100644 index 0000000..71489e7 --- /dev/null +++ b/tmux-tsunami/modules/common/defaultLeaderMenu.nix @@ -0,0 +1,51 @@ +{tsunamiLib, ...}: let + inherit (tsunamiLib) scriptPath; +in { + tsunami.scripts = { + "find-pane" = '' + display_format="#{window_name} #{pane_title} #{pane_current_path} #{pane_current_command}" + hidden_format="#{session_name}:#{window_id}:#{pane_id}" + + # select pane + selected=$(tmux list-panes -a -F "$hidden_format:$display_format" | fzf --delimiter=: --with-nth 4 --color=hl:2) + [ -z "$selected" ] || exit + + # switch to selected + args=(''\${selected//:/ }) + tmux select-pane -t ''\${args[2]} && tmux select-window -t ''\${args[1]} && tmux switch-client -t ''\${args[0]} + ''; + + "search-pane" = '' + trap 'rm -f -- "''\${scrollback:-}"' EXIT + scrollback="$(mktemp)" + + tmux capture-pane -e -p -S - > "$scrollback" + cat "$scrollback" | fzf --ansi + + exit 0 + ''; + }; + + tsunami.keys.leader = [ + { + name = "Find Pane"; + key = "C-w"; + exec = ''run-shell "${scriptPath "minibuffer"} '${scriptPath "find-pane"}'"''; + } + { + name = "Search Pane"; + key = "C-/"; + exec = ''run-shell "${scriptPath "minibuffer"} '${scriptPath "search-pane"}'"''; + } + { + name = "Detach"; + key = "C-d"; + exec = "detach"; + } + { + name = "Reload"; + key = "C-r"; + exec = "source-file ~/.config/tmux/tmux.conf"; + } + ]; +} diff --git a/tmux-tsunami/modules/common/findFile.nix b/tmux-tsunami/modules/common/findFile.nix new file mode 100644 index 0000000..3d08603 --- /dev/null +++ b/tmux-tsunami/modules/common/findFile.nix @@ -0,0 +1,66 @@ +{ + pkgs, + config, + lib, + tsunamiLib, + std, + ... +}: let + inherit (tsunamiLib) scriptPath filePath; + cfg = config.tsunami.findFile; +in { + options = { + tsunami.findFile = { + enable = lib.mkOption { + type = lib.types.bool; + default = false; + }; + }; + }; + + config = lib.mkIf cfg.enable { + tsunami.keys.leader = [ + { + name = "Find File"; + key = "C-f"; + exec = ''run-shell "${scriptPath "minibuffer"} -d '#{?@default-path,#{@default-path},#{pane_current_path}}' '${scriptPath "find-file"}'"''; + } + ]; + tsunami.scripts = { + "minibuffer" = '' + window_height="$(tmux display -p '#{window_height}')" + tmux display-popup -EB \ + -w 100% -h 16 \ + -x 0 -y "$(($window_height + 1))" \ + "$@" + ''; + + "find-file" = ''${pkgs.broot}/bin/broot --conf ${filePath "broot_find_file.toml"}''; + }; + + tsunami.files."broot_find_file.toml" = std.serde.toTOML { + imports = ["~/.config/broot/conf.hjson"]; + quit_on_last_cancel = true; + verbs = [ + { + invocation = "tmux-split"; + external = ["bash" "-c" ''${scriptPath "split"} -c "#{?@default-path,#{@default-path},#{pane_current_path}}" "$EDITOR '{file}'"'']; + key = "ctrl-s"; + apply_to = "file"; + leave_broot = true; + } + { + invocation = "tmux-window"; + external = ["tmux" "new-window" "-c" "#{?@default-path,#{@default-path},#{pane_current_path}}" "$EDITOR '{file}'"]; + key = "ctrl-w"; + apply_to = "file"; + leave_broot = true; + } + { + key = "enter"; + cmd = ":tmux-window"; + } + ]; + }; + }; +} diff --git a/tmux-tsunami/modules/common/fzfExec.nix b/tmux-tsunami/modules/common/fzfExec.nix new file mode 100644 index 0000000..2de3741 --- /dev/null +++ b/tmux-tsunami/modules/common/fzfExec.nix @@ -0,0 +1,55 @@ +{ + tsunamiLib, + lib, + pkgs, + config, + ... +}: let + inherit (tsunamiLib) scriptPath; + cfg = config.tsunami.fzfExec; +in { + options = { + tsunami.fzfExec = { + enable = lib.mkOption { + type = lib.types.bool; + default = true; + }; + }; + }; + + config = lib.mkIf cfg.enable { + tsunami.scripts = { + # this is buggy af; it only works half the time. + # But it's the only way I know of to get docs on a specific tmux command + "tmux-doc" = '' + usage=$(tmux list-commands -F "#{command_list_name} #{command_list_usage}" "$1") + + man tmux | ${pkgs.gawk}/bin/awk -v usage="$usage" ' + index($0, usage) > 0 { found=1 } + found { + print + if ($0 == "") exit + } + ' + ''; + + "fzf-exec" = '' + all_cmds() { + tmux list-commands -F $'#{command_list_name}#{?command_list_alias,\n#{command_list_alias},}' + } + + selected_cmd=$(all_cmds | ${pkgs.fzf}/bin/fzf --bind 'enter:accept-or-print-query,tab:replace-query,alt-backspace:clear-query' --prompt : --preview "~/.config/tmux/scripts/tmux-doc.sh $(printf '{}' | cut -d' ' -f1)") + test -z "$selected_cmd" && exit + + tmux $(echo "$selected_cmd" | sed "s@~@$HOME@g") + ''; + }; + + tsunami.keys.global = [ + { + key = "M-x"; + exec = "run-shell '${scriptPath "minibuffer"} -h 10 ${scriptPath "fzf-exec"}'"; + } + ]; + }; +} diff --git a/tmux-tsunami/modules/common/keys.nix b/tmux-tsunami/modules/common/keys.nix new file mode 100644 index 0000000..9e3c1a2 --- /dev/null +++ b/tmux-tsunami/modules/common/keys.nix @@ -0,0 +1,66 @@ +{ + lib, + config, + tsunamiLib, + ... +}: let + inherit (tsunamiLib) scriptPath; + inherit (lib.strings) escapeShellArg; + cfg = config.tsunami.keys; +in { + options = let + tmuxKey = lib.types.strMatching "^((C|M|S)-)*(.|Enter|Escape|Tab|Up|Down|Left|Right)$"; + inherit (lib) mkOption; + in { + tsunami.keys = { + leader = mkOption { + type = with lib.types; + listOf (submodule { + options = { + name = mkOption {type = str;}; + key = mkOption {type = tmuxKey;}; + exec = mkOption {type = str;}; + }; + }); + }; + + global = mkOption { + type = with lib.types; + listOf (submodule { + options = { + key = mkOption {type = tmuxKey;}; + exec = mkOption {type = str;}; + }; + }); + }; + }; + }; + + config = { + tsunami.scripts = { + "menu" = '' + tmux display-menu \ + -x "#{window_width}" -y S \ + -b none \ + -s "bg=#313244,fg=#9399b2" \ + -S "bg=#313244" \ + -H "bg=#45475a fg=#b4befe" \ + "$@" + ''; + + "leader-menu" = let + args = lib.concatStringsSep " " (builtins.map (f: "${escapeShellArg f.name} ${f.key} ${escapeShellArg f.exec}") cfg.leader); + in '' + ${scriptPath "menu"} ${args} + ''; + }; + tsunami.keys.global = [ + { + key = "C-x"; + exec = "run-shell ${scriptPath "leader-menu"}"; + } + ]; + + tsunami.confs."global-keys" = cfg.global |> builtins.map (f: "bind-key -n ${f.key} ${escapeShellArg f.exec}") |> lib.concatStringsSep "\n"; + }; +} diff --git a/tmux-tsunami/modules/common/main.nix b/tmux-tsunami/modules/common/main.nix new file mode 100644 index 0000000..25366d9 --- /dev/null +++ b/tmux-tsunami/modules/common/main.nix @@ -0,0 +1,23 @@ +{lib, ...}: let + inherit (lib) mkOption mkEnableOption; +in { + options = { + tsunami = { + enable = mkEnableOption "tsunami tmux distro"; + scripts = mkOption { + type = lib.types.attrsOf (lib.types.str); + default = []; + }; + + files = mkOption { + type = lib.types.attrsOf (lib.types.str); + default = []; + }; + + confs = mkOption { + type = lib.types.attrsOf (lib.types.str); + default = []; + }; + }; + }; +} diff --git a/tmux-tsunami/modules/common/sessionizer.nix b/tmux-tsunami/modules/common/sessionizer.nix new file mode 100644 index 0000000..09aa65a --- /dev/null +++ b/tmux-tsunami/modules/common/sessionizer.nix @@ -0,0 +1,92 @@ +{ + pkgs, + config, + lib, + tsunamiLib, + std, + ... +}: let + inherit (tsunamiLib) scriptPath filePath; + cfg = config.tsunami.sessionizer; +in { + options = { + tsunami.sessionizer = { + enable = lib.mkOption { + type = lib.types.bool; + default = false; + }; + }; + }; + + config = lib.mkIf cfg.enable { + tsunami.keys.leader = [ + { + name = "Sessions+"; + key = "C-s"; + exec = "run-shell ${scriptPath "sessions-menu"}"; + } + ]; + + tsunami.scripts = { + "sessionizer" = + # bash + '' + [ -z "$1" ] && exit + + selected="$1" + session_name="$(basename "$selected" | tr '.' '_')" + + # if the session doesn't exist, create it + if ! tmux has-session -t "$session_name"; then + tmux new-session -ds "$session_name" -c "$selected" + tmux set-option -t "$session_name" @default-path "$selected" + fi + + # switch to it + tmux switch -t "$session_name" + ''; + "launch-sessionizer" = ''${pkgs.broot}/bin/broot --only-folders --conf ${filePath "broot_sessionizer.toml"}''; + "new-session" = ''tmux switch-client -t "$(tmux new-session -dP)"''; + "sessions-menu" = + # bash + '' + menu_items=( + "Switch Session" s "run-shell '${scriptPath "minibuffer"} -d $HOME ${scriptPath "launch-sessionizer"}'" + "New Unnamed Session" n "run-shell ${scriptPath "new-session"}" + ) + + # put tmux session names into a list + mapfile -t sessions < <(tmux list-sessions -F "#{session_name}") + + # ...but limit at 9 sessions + for i in "''\${!sessions[@]}"; do + (( i >= 9 )) && break # Stop after 9 sessions + + session="''\${sessions[$i]}" + key=$((i + 1)) # 1-based key + + # and put those 9 in the list of menu items + menu_items+=("$session" "$key" "switch-client -t $session") + done + + # Display the menu + ${scriptPath "menu"} -T "#[align=centre]Sessions" "''\${menu_items[@]}" + ''; + }; + tsunami.files = { + "broot_sessionizer.toml" = std.serde.toTOML { + imports = ["~/.config/broot/conf.hjson"]; # inherit from user's config + quit_on_cancel = true; + verbs = [ + { + invocation = "session"; + external = ''bash -c -- "${scriptPath "sessionizer"} '{file}'"''; + key = "enter"; + apply_to = "directory"; + leave_broot = true; + } + ]; + }; + }; + }; +} diff --git a/tmux-tsunami/modules/common/sessionizer2.nix b/tmux-tsunami/modules/common/sessionizer2.nix new file mode 100644 index 0000000..5f159df --- /dev/null +++ b/tmux-tsunami/modules/common/sessionizer2.nix @@ -0,0 +1,50 @@ +{ + pkgs, + config, + lib, + tsunamiLib, + std, + ... +}: let + inherit (tsunamiLib) scriptPath filePath; + cfg = config.tsunami.sessionizer2; +in { + options.tsunami.sessionizer2 = { + enable = lib.mkOption { + type = lib.types.bool; + default = true; + }; + }; + + config = lib.mkIf cfg.enable { + tsunami.keys.leader = [ + { + name = "New Session"; + key = "C-n"; + exec = "run-shell ${scriptPath "sessions-menu"}"; + } + ]; + + tsunami.files = { + "broot_sessionizer2_launcher" = std.serde.toTOML { + imports = ["~/.config/broot/conf.hjson"]; # inherit from user's config + verbs = [ + { + invocation = "tmux-window"; + external = ["tmux" "new-window" "-c" "#{?@default-path,#{@default-path},#{pane_current_path}}" "$EDITOR '{file}'"]; + key = "enter"; + apply_to = "file"; + leave_broot = false; + } + { + invocation = "tmux-window"; + external = ["tmux" "new-window" "-c" "#{?@default-path,#{@default-path},#{pane_current_path}}" "$EDITOR '{file}'"]; + key = "enter"; + apply_to = "file"; + leave_broot = false; + } + ]; + }; + }; + }; +} diff --git a/tmux-tsunami/modules/hjem.nix b/tmux-tsunami/modules/hjem.nix new file mode 100644 index 0000000..019eada --- /dev/null +++ b/tmux-tsunami/modules/hjem.nix @@ -0,0 +1,58 @@ +{std, ...}: { + pkgs, + lib, + config, + ... +}: let + cfg = config.tsunami; +in { + imports = lib.filesystem.listFilesRecursive ./common; + + _module.args = { + inherit std; + tsunamiLib = { + scriptPath = f: "~/.config/tmux/scripts/${f}.sh"; + filePath = f: "~/.config/tmux/files/${f}"; + }; + }; + + files = lib.mkIf cfg.enable (lib.mkMerge [ + ( + cfg.scripts + |> builtins.mapAttrs (name: value: { + name = ".config/tmux/scripts/${name}.sh"; + value = { + text = value; + executable = true; + }; + }) + |> lib.attrValues + |> builtins.listToAttrs + ) + ( + cfg.files + |> builtins.mapAttrs (name: value: { + name = ".config/tmux/files/${name}"; + value.text = value; + }) + |> lib.attrValues + |> builtins.listToAttrs + ) + ( + cfg.confs + |> builtins.mapAttrs (name: value: { + name = ".config/tmux/conf.d/${name}.conf"; + value.text = value; + }) + |> lib.attrValues + |> builtins.listToAttrs + ) + { + ".config/tmux/tmux.conf".text = '' + run-shell "find ~/.config/tmux/conf.d -print0 | xargs -0 -n1 tmux source-file" + ''; + } + ]); + + packages = lib.optional cfg.enable pkgs.tmux; +} diff --git a/tmux-tsunami/modules/installer.nix b/tmux-tsunami/modules/installer.nix new file mode 100644 index 0000000..9cff811 --- /dev/null +++ b/tmux-tsunami/modules/installer.nix @@ -0,0 +1,38 @@ +{pkgs, ...}: let + tsunamiLib = { + scriptPath = f: "$HOME/.config/tmux/scripts/${f}.sh"; + filePath = f: "$HOME/.config/tmux/files/${f}"; + }; + config = pkgs.lib.evalModules { + modules = + [ + {_module.args = {inherit pkgs tsunamiLib;};} + ] + ++ (pkgs.lib.filesystem.listFilesRecursive ./common); + }; + + moduleConfig = pkgs.writeText "tsunami-installer-config.json" (builtins.toJSON { + inherit (config.config.tsunami) scripts files confs; + }); +in { + mkInstaller = + pkgs.runCommand "tsunami-installer" { + buildInputs = [pkgs.jq]; + } '' + mkdir -p $out + { + cat ${moduleConfig} | jq -r ' + (.scripts | to_entries[] | "cat <<\"EOF\" > $HOME/.config/tmux/scripts/\(.key).sh\n\(.value)\nEOF\n"), + (.files | to_entries[] | "cat <<\"EOF\" > $HOME/.config/tmux/files/\(.key)\n\(.value)\nEOF\n"), + (.confs | to_entries[] | "cat <<\"EOF\" > $HOME/.config/tmux/conf.d/\(.key).conf\n\(.value)\nEOF\n") + ' + printf ' + cat < $HOME/.config/tmux/tmux.conf + run-shell "find $out/conf.d -print0 | xargs -0 -n1 tmux source-file" + EOF + ' + } > $out/tsunami-installer + + chmod +x $out/tsunami-installer + ''; +} diff --git a/tmux-tsunami/planning.md b/tmux-tsunami/planning.md new file mode 100644 index 0000000..872bbb5 --- /dev/null +++ b/tmux-tsunami/planning.md @@ -0,0 +1,56 @@ +## Pieces: +sessionizer +- leaderkey keybinding C-p+ +- script launch-sessionizer +- script clean-sessions +- script new-session +- script projects-menu +- extrafile broot_sessionizer + +```nix +{pkgs, tsunamiLib, ...}: let + inherit (tsunamiLib) scriptPath filePath; +in { + tsunami.keys.leader = [{ + name = "Projects+"; + key = "C-p"; + exec = "run-shell ${scriptPath "projects-menu"}"; + }]; + + tsunami.scripts = { + "launch-sessionizer" = ''${pkgs.broot}/bin/broot --conf ${filePath "broot_sessionizer.toml"}''; + "clean-sessions" = ''...''; + "new-session" = ''...''; + }; + tsunami.files = { + "broot_sessionizer.toml" = (pkgs.formats.toml {}).generate "file.toml" { + ... + }; + }; +} +``` + +find-file +- leaderkey keybinding C-f +- script find-file +- extrafile broot_find_file +fzf-exec +- keybinding M-x +- script tmux-doc +- script fzf-exec +search-buffer +- leaderkey keybinding C-/ +- script search-buffer +find-window +- leaderkey keybinding C-w +- script find-buffer +leader-key +- keybinding C-x +- script leader-menu +bar +- script bar +- script battery +main config +- script minibuffer +- script split +- -- cgit v1.3.1