Compare commits

1 Commits

Author SHA1 Message Date
Miguel Palhas 89664d8f17 feat: package the claude ACP adapter for aoe
aoe's structured view spawns `claude-agent-acp` off $PATH. npm's global
prefix here is the read-only nodejs store path, so `npm install -g` cannot
provide it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-18 21:40:34 +01:00
57 changed files with 2869 additions and 1674 deletions
Generated
+145 -227
View File
@@ -7,26 +7,27 @@
"nixpkgs": "nixpkgs"
},
"locked": {
"lastModified": 1787387938,
"narHash": "sha256-113jGEYKSeTwYgLdlVuRyqPBHYHJcU1kbRKiAUhj04Q=",
"ref": "refs/heads/main",
"rev": "96472052106cb5c448e9fa39244b0d0ccae9be72",
"revCount": 2192,
"type": "git",
"url": "https://git.naps.pt/yolo/agent-of-empires.git"
"lastModified": 1786554876,
"narHash": "sha256-akMknOIdX0QsBsM4lgreXSNNym7TNu9iOrDBqrQuTuQ=",
"owner": "agent-of-empires",
"repo": "agent-of-empires",
"rev": "9b0d691a6cdaa6d2451f6eda05e83628c54eb95a",
"type": "github"
},
"original": {
"type": "git",
"url": "https://git.naps.pt/yolo/agent-of-empires.git"
"owner": "agent-of-empires",
"ref": "v1.14.1",
"repo": "agent-of-empires",
"type": "github"
}
},
"agent-skills": {
"locked": {
"lastModified": 1787576215,
"narHash": "sha256-x1ftVVEcq8p6phZEl4NC4F3RcPqpgx6wEMODMI5QHN8=",
"lastModified": 1786957004,
"narHash": "sha256-lUyQGAN6Kd5onhPa+ptOiwtIaY6Y02q+nyRt9NWkdDg=",
"ref": "refs/heads/main",
"rev": "0900e5fea1245c7d5e962e48a809ff7481c87f95",
"revCount": 99,
"rev": "7cb03f879600e87281356fdc411b2690f09a9175",
"revCount": 47,
"type": "git",
"url": "https://git.naps.pt/yolo/agent-skills.git"
},
@@ -55,11 +56,11 @@
]
},
"locked": {
"lastModified": 1786464219,
"narHash": "sha256-WKqWL8r7CyTDYueTr2ffJ9ya50dellv6IR1JX5PghDY=",
"lastModified": 1784368054,
"narHash": "sha256-zF1iJkBQSDWmRO4/LEeHR1SpKY0lqZaxkoQJpPS9K9U=",
"owner": "hyprwm",
"repo": "aquamarine",
"rev": "f3d1804205e8158c15595cdda1b566f93349ffae",
"rev": "9b5f14d9483445e766294eb8fbe0b8f370269ed0",
"type": "github"
},
"original": {
@@ -70,15 +71,15 @@
},
"claude-code": {
"inputs": {
"nixpkgs": "nixpkgs_2",
"systems": "systems"
"flake-utils": "flake-utils",
"nixpkgs": "nixpkgs_2"
},
"locked": {
"lastModified": 1787189302,
"narHash": "sha256-cUM75Rob89qTxJvqVRF8GUewOriqQtLiyiy5xfd1fMM=",
"lastModified": 1785285785,
"narHash": "sha256-r1cMlvXjRVKe+uQ/GKwy4TpionpmbNgksFP2758D/MM=",
"owner": "sadjow",
"repo": "claude-code-nix",
"rev": "8fb8cbab6345583e0b68ca7dbbbad08a0a348145",
"rev": "da78262708d858861afbe1f68ea65fedda4054c4",
"type": "github"
},
"original": {
@@ -89,17 +90,17 @@
},
"codex-cli": {
"inputs": {
"flake-utils": "flake-utils",
"flake-utils": "flake-utils_2",
"nixpkgs": [
"nixpkgs"
]
},
"locked": {
"lastModified": 1787094470,
"narHash": "sha256-UiHfeA8umiWByi/ijDnfnlGcVSEAhwNv1Gue3h6ey/U=",
"lastModified": 1785294467,
"narHash": "sha256-mylUWHK2wIw8S07K5hbkrLELnflJkDwsSWz2YFbJzLI=",
"owner": "sadjow",
"repo": "codex-cli-nix",
"rev": "cfbeeca2f4dfbbfcc3c1a5b21e61a051ff43533d",
"rev": "e4e3b0672bbb8fba7f32fe53cd9c604990970374",
"type": "github"
},
"original": {
@@ -111,17 +112,17 @@
},
"codex-desktop-linux": {
"inputs": {
"flake-utils": "flake-utils_2",
"flake-utils": "flake-utils_3",
"nixpkgs": [
"nixpkgs"
]
},
"locked": {
"lastModified": 1787079200,
"narHash": "sha256-BYo8mVEqRj6y8PzpFd3rX6TQTknB3Ynp/3ykMTm56D8=",
"lastModified": 1786822283,
"narHash": "sha256-RdoFKwNQ2m2zbHnq/QbgLf2lUI9cohsnlMwLfJuaTh0=",
"owner": "ilysenko",
"repo": "codex-desktop-linux",
"rev": "1875dc2eaab6f9448617d18d0eda58902edf0f1a",
"rev": "e6b51d96ac2b9b5d9adafa852241bd0982972557",
"type": "github"
},
"original": {
@@ -145,21 +146,6 @@
"type": "github"
}
},
"crane_2": {
"locked": {
"lastModified": 1787326676,
"narHash": "sha256-lWhBbBvC05/xwivKBBiM2YNizpmgqCgyOIzomvRuwxs=",
"owner": "ipetkov",
"repo": "crane",
"rev": "692f7e9ef2ece8125b466f66f2af532b3edaed0d",
"type": "github"
},
"original": {
"owner": "ipetkov",
"repo": "crane",
"type": "github"
}
},
"ethui": {
"inputs": {
"nixpkgs": "nixpkgs_3",
@@ -219,24 +205,6 @@
"inputs": {
"nixpkgs-lib": "nixpkgs-lib_2"
},
"locked": {
"lastModified": 1785627969,
"narHash": "sha256-4dtXQk/NMePegK/nWp5NSeuZKLATItOq61lpEvmXqGw=",
"owner": "hercules-ci",
"repo": "flake-parts",
"rev": "427bf4bd9435fdf21321c8cc628c24efc14c0f7a",
"type": "github"
},
"original": {
"owner": "hercules-ci",
"repo": "flake-parts",
"type": "github"
}
},
"flake-parts_3": {
"inputs": {
"nixpkgs-lib": "nixpkgs-lib_3"
},
"locked": {
"lastModified": 1775087534,
"narHash": "sha256-91qqW8lhL7TLwgQWijoGBbiD4t7/q75KTi8NxjVmSmA=",
@@ -253,7 +221,7 @@
},
"flake-utils": {
"inputs": {
"systems": "systems_2"
"systems": "systems"
},
"locked": {
"lastModified": 1731533236,
@@ -271,7 +239,7 @@
},
"flake-utils_2": {
"inputs": {
"systems": "systems_3"
"systems": "systems_2"
},
"locked": {
"lastModified": 1731533236,
@@ -288,6 +256,24 @@
}
},
"flake-utils_3": {
"inputs": {
"systems": "systems_3"
},
"locked": {
"lastModified": 1731533236,
"narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "11707dc2f618dd54ca8739b309ec4fc024de578b",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "flake-utils",
"type": "github"
}
},
"flake-utils_4": {
"locked": {
"lastModified": 1644229661,
"narHash": "sha256-1YdnJAsNy69bpcjuoKdOYQX0YxZBiCYZo4Twxerqv7k=",
@@ -302,7 +288,7 @@
"type": "github"
}
},
"flake-utils_4": {
"flake-utils_5": {
"inputs": {
"systems": "systems_8"
},
@@ -322,17 +308,17 @@
},
"foundry": {
"inputs": {
"flake-utils": "flake-utils_3",
"flake-utils": "flake-utils_4",
"nixpkgs": [
"nixpkgs"
]
},
"locked": {
"lastModified": 1787123762,
"narHash": "sha256-gKUrkWZRS2BxEcJDyVSBZJ3fi1rN51PPe8U5hCjFNIY=",
"lastModified": 1785311662,
"narHash": "sha256-lC5ZlyNMf8wcQnrAzUhYb1p6ndzwEovnXKiMqSTLgUo=",
"owner": "shazow",
"repo": "foundry.nix",
"rev": "a194d90ec9f6a4d70d79220e644c531da8324266",
"rev": "0eb3d1233237eb4e6f4f45f31c087ee1a16245f1",
"type": "github"
},
"original": {
@@ -348,11 +334,11 @@
]
},
"locked": {
"lastModified": 1787144466,
"narHash": "sha256-HHfv2/HkNSKbbSyU9iD/g8lbP6r4tl33sSw1W4rXCk0=",
"lastModified": 1785232496,
"narHash": "sha256-65EQYIRRpTdpH8lUiB6Mvo5uBkG60aBIzAJuALfx+O0=",
"owner": "NixOS",
"repo": "nixos-hardware",
"rev": "0471accf8d0a8210b31d947497d179ecc99e0021",
"rev": "2e790b0a6be8ec2b76174ac0931b8ff11919ec98",
"type": "github"
},
"original": {
@@ -369,11 +355,11 @@
]
},
"locked": {
"lastModified": 1787176219,
"narHash": "sha256-djoRr6jBpe35q/0JwAvXpFXg1Ktf+X54NU2RLR7wnHw=",
"lastModified": 1785306346,
"narHash": "sha256-DScBkW0fOgpGPK2trNoX3ryLTlaC14+gglFo/BhGJ4g=",
"owner": "nix-community",
"repo": "home-manager",
"rev": "c53d643b3737e2fcd04e6cb3b3580ef50b2087a0",
"rev": "e705714e918c3b11affcdd15db2cbe3a070420a0",
"type": "github"
},
"original": {
@@ -390,11 +376,11 @@
]
},
"locked": {
"lastModified": 1786719456,
"narHash": "sha256-B74DLQs/VjlqyhnnX3tguWwghqJHWSJX30TKZuAljIg=",
"lastModified": 1783963347,
"narHash": "sha256-r376E2XpakiXwModDHIxlvB6qLq4iFVEq730vxOO4JY=",
"owner": "nix-community",
"repo": "home-manager",
"rev": "83b7606dcf44abe3a94b86e8bb2b3355d22e8797",
"rev": "a45a7c451455a51ae740ec3bce4024b312809c29",
"type": "github"
},
"original": {
@@ -419,11 +405,11 @@
]
},
"locked": {
"lastModified": 1786464181,
"narHash": "sha256-2alOMkLjXANh7unkZnYnCF2K2rApZaOLMoQ3o+VX2CY=",
"lastModified": 1776511930,
"narHash": "sha256-fCpwFiTW0rT7oKJqr3cqHMnkwypSwQKpbtUEtxdkgrM=",
"owner": "hyprwm",
"repo": "hyprcursor",
"rev": "e4ed7c08123df5af460a0a70961380cbfb872f76",
"rev": "39435900785d0c560c6ae8777d29f28617d031ef",
"type": "github"
},
"original": {
@@ -448,11 +434,11 @@
]
},
"locked": {
"lastModified": 1786464367,
"narHash": "sha256-k58p4wbzIXWyRWrW84pP8tD+iaZSSYiiM+fr0Auk4oU=",
"lastModified": 1782566056,
"narHash": "sha256-haEZcHzYrePnjFOYSWTbxm/Nrla0aPslJfmvdCvqtVc=",
"owner": "hyprwm",
"repo": "hyprgraphics",
"rev": "7c895c44e3ca6d28ed68ddd80ec02b02b925e7fc",
"rev": "c6e7b9f673f4360bc813d3dc75028f75ee88d3f8",
"type": "github"
},
"original": {
@@ -478,11 +464,11 @@
"xdph": "xdph"
},
"locked": {
"lastModified": 1787138659,
"narHash": "sha256-/3fIl+/xZoZqliAfwYB8ahCZK04Qr/zwdOQgiR41dy8=",
"lastModified": 1785334807,
"narHash": "sha256-bN0KRhDfwSPqbpgUgWVcQMkuDje+8ghWlm7uUQZzaxc=",
"ref": "refs/heads/main",
"rev": "52b368f1d7fdee1b0e96ef3dc655b5a577df4fdb",
"revCount": 7745,
"rev": "7803e68638c1bebce57cf9ea8da51fd9a1e19edd",
"revCount": 7669,
"submodules": true,
"type": "git",
"url": "https://github.com/hyprwm/Hyprland"
@@ -526,11 +512,11 @@
]
},
"locked": {
"lastModified": 1786464504,
"narHash": "sha256-7sHwM86KILQyHDHDuE2SDBlQ2jvZ0EW3hY7sW009/cg=",
"lastModified": 1784196523,
"narHash": "sha256-ahtKMGXFJdlQNhatQm1+BBU/pGfGYnAqQt3vWvq4p8s=",
"owner": "hyprwm",
"repo": "hyprland-guiutils",
"rev": "4c30cf3097ea963c0e250749ee0c59f8b08816d6",
"rev": "a6ccb6cb112ed5a244c0191fb972347ecfa893e0",
"type": "github"
},
"original": {
@@ -580,11 +566,11 @@
]
},
"locked": {
"lastModified": 1786464129,
"narHash": "sha256-339AkTlpMYSIvFuG0rnR+8Yg4/AZKeJalshJavlnKfg=",
"lastModified": 1777320127,
"narHash": "sha256-Qu+Wf2Bp5qUjyn2YpZNq8a7JyzTGowhT1knrwE38a9U=",
"owner": "hyprwm",
"repo": "hyprlang",
"rev": "9508458be316a0d70d37ebed1ab725ccd10411ff",
"rev": "090117506ddc3d7f26e650ff344d378c2ec329cc",
"type": "github"
},
"original": {
@@ -654,11 +640,11 @@
]
},
"locked": {
"lastModified": 1785930473,
"narHash": "sha256-DitTu625BhEYpZjtjxtGpjrEJwPwW+X/+jJvhSZNSJM=",
"lastModified": 1782554491,
"narHash": "sha256-+p3MlyN/nqRefcf2IckPlGRUn9+hielqpS9XClbLleM=",
"owner": "hyprwm",
"repo": "hyprtoolkit",
"rev": "af515b69dfbe366dc7873aa1475cb2f4db3ebad7",
"rev": "bdba25ced39ea39ab004a8f31593ba0b0ff1ca35",
"type": "github"
},
"original": {
@@ -679,11 +665,11 @@
]
},
"locked": {
"lastModified": 1786464080,
"narHash": "sha256-W1hxvumEM57yV+QwsZ4QdAHEqOkr7e4S8VAkLX60qDE=",
"lastModified": 1784323413,
"narHash": "sha256-XnAVV+H4f8Xdv0yZcSwJ5kCjLyE8fHxPeLX6a3HSrAU=",
"owner": "hyprwm",
"repo": "hyprutils",
"rev": "c157fe1e3092b980cc69315a6631f89aff09dcce",
"rev": "5f03477ab3a005ff27c527486f551883535aea2f",
"type": "github"
},
"original": {
@@ -704,11 +690,11 @@
]
},
"locked": {
"lastModified": 1786464033,
"narHash": "sha256-QM8Qe4/L8lpdVN4bgwahmi+jyyc4fisseDMe4afcDxA=",
"lastModified": 1777159683,
"narHash": "sha256-Jxixw6wZphUp+nHYxOKUYSckL17QMBx2d5Zp0rJHr1g=",
"owner": "hyprwm",
"repo": "hyprwayland-scanner",
"rev": "62e62c1ca23da17612c6890d4ad2064f575643db",
"rev": "b8632713a6beaf28b56f2a7b0ab2fb7088dbb404",
"type": "github"
},
"original": {
@@ -733,11 +719,11 @@
]
},
"locked": {
"lastModified": 1786464294,
"narHash": "sha256-ZQsZ2WvBdkboCIyh8LStDPdAIARmxzn0XMNxxoOhjPE=",
"lastModified": 1778410714,
"narHash": "sha256-o6RzFj4nJXaPRY7EM01siuCQeT41RfwwmcmFQqwFJJg=",
"owner": "hyprwm",
"repo": "hyprwire",
"rev": "4ce7cd6b6128c1ac41caf23c58a30a26b327f9dd",
"rev": "85148a8e612808cf5ddb25d0b3c5840f3498a7dc",
"type": "github"
},
"original": {
@@ -746,27 +732,6 @@
"type": "github"
}
},
"maestro": {
"inputs": {
"crane": "crane_2",
"flake-parts": "flake-parts_2",
"nixpkgs": "nixpkgs_6",
"process-compose-flake": "process-compose-flake"
},
"locked": {
"lastModified": 1787610419,
"narHash": "sha256-YRDvlaiEOpzRC5JHMlm/S39EGfrGsD4kmEWwnyHz4Tg=",
"ref": "refs/heads/main",
"rev": "80a93999051d41b29544258dca272d40f3fc0c98",
"revCount": 443,
"type": "git",
"url": "https://git.naps.pt/naps62/maestro.git"
},
"original": {
"type": "git",
"url": "https://git.naps.pt/naps62/maestro.git"
}
},
"nix-index-database": {
"inputs": {
"nixpkgs": [
@@ -774,11 +739,11 @@
]
},
"locked": {
"lastModified": 1786852476,
"narHash": "sha256-IM5CYtf86W4w8eUPpKcY/LpdHElmVBtJhaKnoTKxZEA=",
"lastModified": 1785046085,
"narHash": "sha256-UiK+mmZJuLWQVhJ5b2wDzogIYWAesyRm6LA3h3Ulh3Y=",
"owner": "nix-community",
"repo": "nix-index-database",
"rev": "c7962dc97b45129df8d751bedaf37beb5a17706e",
"rev": "11665045df8b9938ef811a3bfdc65cffb02b4b70",
"type": "github"
},
"original": {
@@ -819,21 +784,6 @@
}
},
"nixpkgs-lib_2": {
"locked": {
"lastModified": 1785031560,
"narHash": "sha256-OmshNvn2vupOFpYinLUu+1Dnpu4n7Q5N3ggGVNHpkUI=",
"owner": "nix-community",
"repo": "nixpkgs.lib",
"rev": "0e79af5e3d4dcfcd676ab5ba3f95d2e3352e078c",
"type": "github"
},
"original": {
"owner": "nix-community",
"repo": "nixpkgs.lib",
"type": "github"
}
},
"nixpkgs-lib_3": {
"locked": {
"lastModified": 1774748309,
"narHash": "sha256-+U7gF3qxzwD5TZuANzZPeJTZRHS29OFQgkQ2kiTJBIQ=",
@@ -850,11 +800,11 @@
},
"nixpkgs_2": {
"locked": {
"lastModified": 1787111413,
"narHash": "sha256-sFosWtq21eHGJRnTc/hvf4M1obRgLEUMNm/IzllkHMA=",
"lastModified": 1784783405,
"narHash": "sha256-4IHyyLgLBdKefkljdKod4IMn023pQiDXAWJA187cmdY=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "afe3d8ac4395617bdcdac9f188ac8717a062e014",
"rev": "7525d999cd850b9a488817abc89c75dc733acf17",
"type": "github"
},
"original": {
@@ -898,11 +848,11 @@
},
"nixpkgs_5": {
"locked": {
"lastModified": 1786247143,
"narHash": "sha256-8S3Kcxs7D4UtxJxSJZz0m14CGhuW0MxfrIwJxeGWGnQ=",
"lastModified": 1784356753,
"narHash": "sha256-12KrbMiWLcf8m7pCvAtZh1ZrgF85ZXDXvfR/fWTKy84=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "279b4a8275f032c566576b3f181fa0f27197f588",
"rev": "61b7c44c4073f0b827768aff0049561b5110ea5a",
"type": "github"
},
"original": {
@@ -914,37 +864,21 @@
},
"nixpkgs_6": {
"locked": {
"lastModified": 1787135253,
"narHash": "sha256-RD2kNWCG+Bjo6h+JVjWVNntZs2GtRoeY2xHjts/FNkA=",
"owner": "NixOS",
"lastModified": 1785090369,
"narHash": "sha256-m0pDuRJG7EDo9ri+4Ksu83VsI+PlxNC9lNBfydejce4=",
"owner": "nixos",
"repo": "nixpkgs",
"rev": "ffb3c9b700e759be2ef13237c9d8f953b32a1e46",
"rev": "624af665418d3c65d544145b4d34ad696439570e",
"type": "github"
},
"original": {
"owner": "NixOS",
"owner": "nixos",
"ref": "nixos-unstable",
"repo": "nixpkgs",
"type": "github"
}
},
"nixpkgs_7": {
"locked": {
"lastModified": 1787070829,
"narHash": "sha256-vXNVDVtvfiQuXthP0NHPFdNvvMTkGpx0UP8oddIWbNk=",
"owner": "nixos",
"repo": "nixpkgs",
"rev": "0ae2bc1419c3f345984c2629e72e7a631820fa4d",
"type": "github"
},
"original": {
"owner": "nixos",
"ref": "nixos-unstable",
"repo": "nixpkgs",
"type": "github"
}
},
"nixpkgs_8": {
"locked": {
"lastModified": 1775036866,
"narHash": "sha256-ZojAnPuCdy657PbTq5V0Y+AHKhZAIwSIT2cb8UgAz/U=",
@@ -960,7 +894,7 @@
"type": "github"
}
},
"nixpkgs_9": {
"nixpkgs_8": {
"locked": {
"lastModified": 1744536153,
"narHash": "sha256-awS2zRgF4uTwrOKwwiJcByDzDOdo3Q1rPZbiHQg/N38=",
@@ -976,6 +910,26 @@
"type": "github"
}
},
"noctalia": {
"inputs": {
"nixpkgs": [
"nixpkgs"
]
},
"locked": {
"lastModified": 1785335804,
"narHash": "sha256-oN3I6Kr71lOCE3/ou4GgY2cQYh4B6xQxZXzomFgYn0s=",
"owner": "noctalia-dev",
"repo": "noctalia-shell",
"rev": "aded31799eb0dffdbcf436243083f1ebce26f95d",
"type": "github"
},
"original": {
"owner": "noctalia-dev",
"repo": "noctalia-shell",
"type": "github"
}
},
"nvchad-starter": {
"flake": false,
"locked": {
@@ -1002,11 +956,11 @@
]
},
"locked": {
"lastModified": 1786797655,
"narHash": "sha256-qAWI5Vf0xaRmb8/l9XWYrvEgrIaqKBv8Dg81JO7wkrY=",
"lastModified": 1785121894,
"narHash": "sha256-AWbstkwe4twd+5iWx8QBVzTgwGAUvn4vVdobGKW5BRc=",
"owner": "nix-community",
"repo": "nix4nvchad",
"rev": "b878640ae13c8f70f41aa1159234206051a4378c",
"rev": "8135d402aa554b5f8e5dd2ec341c5a440fd18496",
"type": "github"
},
"original": {
@@ -1037,41 +991,6 @@
"type": "github"
}
},
"process-compose-flake": {
"locked": {
"lastModified": 1782060835,
"narHash": "sha256-Q8HH2t1l3MITtWCWY4ytcH5F/Q7aAHo3Iq/QTMAKTe0=",
"owner": "Platonic-Systems",
"repo": "process-compose-flake",
"rev": "464ff6880737f063c3f0d3d2c7781fda9190868f",
"type": "github"
},
"original": {
"owner": "Platonic-Systems",
"repo": "process-compose-flake",
"type": "github"
}
},
"rev": {
"inputs": {
"nixpkgs": [
"nixpkgs"
]
},
"locked": {
"lastModified": 1787648458,
"narHash": "sha256-YO8lx85XorD2cnq5QNBvpj3JH60OrDV04GA3R2Oaih0=",
"ref": "refs/heads/main",
"rev": "88bf55a737046768adcea15b2475096519341494",
"revCount": 234,
"type": "git",
"url": "https://git.naps.pt/yolo/rev.git"
},
"original": {
"type": "git",
"url": "https://git.naps.pt/yolo/rev.git"
}
},
"root": {
"inputs": {
"agent-of-empires": "agent-of-empires",
@@ -1084,12 +1003,11 @@
"hardware": "hardware",
"home-manager": "home-manager",
"hyprland": "hyprland",
"maestro": "maestro",
"nix-index-database": "nix-index-database",
"nixpkgs": "nixpkgs_7",
"nixpkgs": "nixpkgs_6",
"noctalia": "noctalia",
"nvchad-starter": "nvchad-starter",
"nvchad4nix": "nvchad4nix",
"rev": "rev",
"rose-pine-hyprcursor": "rose-pine-hyprcursor",
"sem": "sem",
"spicetify-nix": "spicetify-nix",
@@ -1139,7 +1057,7 @@
},
"rust-overlay": {
"inputs": {
"nixpkgs": "nixpkgs_9"
"nixpkgs": "nixpkgs_8"
},
"locked": {
"lastModified": 1775445266,
@@ -1157,9 +1075,9 @@
},
"sem": {
"inputs": {
"flake-parts": "flake-parts_3",
"flake-utils": "flake-utils_4",
"nixpkgs": "nixpkgs_8",
"flake-parts": "flake-parts_2",
"flake-utils": "flake-utils_5",
"nixpkgs": "nixpkgs_7",
"rust-overlay": "rust-overlay"
},
"locked": {
@@ -1184,11 +1102,11 @@
"systems": "systems_9"
},
"locked": {
"lastModified": 1786855359,
"narHash": "sha256-yeKMWFCPeoIKmPBLLvP1/15ulOJO8i9ZIlSSXGuW6aw=",
"lastModified": 1785048222,
"narHash": "sha256-LjFVfwxensz76SjnvofF1Jsw+xTLd8Qp31W0J6nh2XI=",
"owner": "Gerg-L",
"repo": "spicetify-nix",
"rev": "0f478ff79b82abb785160cd4531293f61d21be86",
"rev": "17e524f330c282d31c32dfe076222d4a12277886",
"type": "github"
},
"original": {
@@ -1411,11 +1329,11 @@
]
},
"locked": {
"lastModified": 1786464334,
"narHash": "sha256-/TBQT5rhBB2Dm4HoZzhGDaCwYmRTs3W3DPhMXFWc/BU=",
"lastModified": 1784371182,
"narHash": "sha256-S8A1lezEalltWcCp3gAic5lssS0xTSISK6fKODefhOk=",
"owner": "hyprwm",
"repo": "xdg-desktop-portal-hyprland",
"rev": "9f0e9ff02739cd538d39bd706422dc50e9ca60dd",
"rev": "08d99f727944dd15e4740090305e31c5fb92a50a",
"type": "github"
},
"original": {
@@ -1432,11 +1350,11 @@
]
},
"locked": {
"lastModified": 1787160391,
"narHash": "sha256-5dRTU55IlWABqnWiXDgtzrc3wT54nPoZwDQ91VOY4mE=",
"lastModified": 1785224333,
"narHash": "sha256-8DNJUVM10ktK02V9ZIV064CH+fuPF272VCAZBn6arig=",
"owner": "0xc000022070",
"repo": "zen-browser-flake",
"rev": "228a12a50971a115caa1c87b7de3d87b08e35275",
"rev": "5f1a4be4d280e1ae06d61a75775a234b1714da3f",
"type": "github"
},
"original": {
+5 -11
View File
@@ -49,6 +49,10 @@
url = "https://github.com/hyprwm/Hyprland";
submodules = true;
};
noctalia = {
url = "github:noctalia-dev/noctalia-shell";
inputs.nixpkgs.follows = "nixpkgs";
};
spicetify-nix = {
url = "github:Gerg-L/spicetify-nix";
inputs.nixpkgs.follows = "nixpkgs";
@@ -64,22 +68,12 @@
};
# Pinned to the tag, and pins its own nixpkgs for the same reason ethui does
# — it is a verified Rust build. Bump deliberately, not via `flake update`.
agent-of-empires.url = "git+https://git.naps.pt/yolo/agent-of-empires.git";
agent-of-empires.url = "github:agent-of-empires/agent-of-empires/v1.14.1";
# Semantic-diff tool rev calls via REV_SEM_BIN. NOT nixpkgs' `sem`, which is
# the unrelated Semaphore CI cli.
sem.url = "github:Ataraxy-Labs/sem";
# Claude Code + Codex skills, commands, hooks and CLAUDE.md fragments.
agent-skills.url = "git+https://git.naps.pt/yolo/agent-skills.git";
# Terminal-session orchestrator. Pins its own nixpkgs for the same reason
# ethui and agent-of-empires do — it is a verified Rust build.
maestro.url = "git+https://git.naps.pt/naps62/maestro.git";
# Always-on local code review server. Follows nixpkgs, unlike the Rust
# inputs above: it is a plain node bundle, and a second nixpkgs would put a
# second node 26 in the closure for nothing.
rev = {
url = "git+https://git.naps.pt/yolo/rev.git";
inputs.nixpkgs.follows = "nixpkgs";
};
nix-index-database = {
url = "github:nix-community/nix-index-database";
inputs.nixpkgs.follows = "nixpkgs";
-4
View File
@@ -5,10 +5,6 @@
imports = [
../common/programs/default.nix
../common/programs/desktop
../common/programs/workstation-apps.nix
../common/programs/editors.nix
../common/programs/dev-workstation.nix
../common/programs/ai-gui.nix
../common/programs/zen-browser.nix
../common/programs/hyprland
../common/programs/kitty
+105
View File
@@ -0,0 +1,105 @@
{
config,
lib,
pkgs,
self,
...
}:
let
cfg = config.home.mutableFiles;
repoPath = config.home.mutableFilesRepoPath;
flakePrefix = self.outPath;
fileEntries = lib.attrsToList cfg;
toRepoPath = storePath: repoPath + lib.removePrefix flakePrefix (toString storePath);
checkScript = lib.concatMapStringsSep "\n" (
{ name, value }:
let
target = "${config.home.homeDirectory}/${name}";
storePath = value.source;
# toRepoPath only works for sources that live in the flake tree. A
# generated source is not under flakePrefix, so removePrefix is a no-op
# and the hint would print a /nix/store path to copy back onto.
originalPath =
if value.upstreamPath != null then "${repoPath}/${value.upstreamPath}" else toRepoPath value.source;
in
''
if [ -f "${target}" ] && ! ${lib.getExe' pkgs.diffutils "diff"} -q "${storePath}" "${target}" > /dev/null 2>&1; then
echo ""
echo "!! mutable file changed: ${name}"
echo " To bring changes upstream:"
echo " cp ${target} ${originalPath}"
echo ""
${lib.getExe' pkgs.diffutils "diff"} -u "${storePath}" "${target}" || true
_mutable_changed=1
fi
''
) fileEntries;
copyScript = lib.concatMapStringsSep "\n" (
{ name, value }:
let
target = "${config.home.homeDirectory}/${name}";
inherit (value) source;
dirName = builtins.dirOf target;
in
''
mkdir -p "${dirName}"
cp -f "${source}" "${target}"
chmod ${if value.executable then "755" else "644"} "${target}"
''
) fileEntries;
in
{
options.home.mutableFilesRepoPath = lib.mkOption {
type = lib.types.str;
description = "Absolute path to the nixos-config repo on disk.";
};
options.home.mutableFiles = lib.mkOption {
type = lib.types.attrsOf (
lib.types.submodule {
options = {
source = lib.mkOption {
type = lib.types.path;
description = "Path to the source file.";
};
executable = lib.mkOption {
type = lib.types.bool;
default = false;
description = "Whether the file should be executable.";
};
upstreamPath = lib.mkOption {
type = lib.types.nullOr lib.types.str;
default = null;
example = "home/yolo/claude-settings.json";
description = ''
Repo-relative file to name in the "bring changes upstream" hint.
Required when `source` is generated rather than a file in the
flake tree, since the store path cannot be mapped back.
'';
};
};
}
);
default = { };
description = "Files to copy (not symlink) into the home directory, with change detection.";
};
config = lib.mkIf (cfg != { }) {
home.activation.mutableFiles = lib.hm.dag.entryAfter [ "writeBoundary" ] ''
_mutable_changed=0
${checkScript}
if [ "$_mutable_changed" -eq 1 ]; then
echo ""
echo "!! Aborting: mutable files have been modified outside of nix."
echo " Bring the changes upstream first, then re-run."
exit 1
fi
${copyScript}
'';
};
}
+11 -34
View File
@@ -1,43 +1,20 @@
{
config,
lib,
pkgs,
...
}:
let
cfg = config.custom.blender;
in
{
options.custom.blender = {
cuda = lib.mkOption {
type = lib.types.bool;
default = false;
description = ''
Build Blender with CUDA/OptiX so Cycles renders on the GPU instead of
the CPU. Only useful on an NVIDIA host; enabling it on arrakis (Intel)
would mean a long build for nothing.
home.packages = with pkgs; [
blender
prusa-slicer
];
No binary cache serves this cuda-maintainers was measured to make no
difference so flipping this on rebuilds Blender, OpenUSD, OpenSubdiv
and OpenImageDenoise from source.
'';
};
};
fonts.fontconfig.enable = true;
config = {
home.packages = [
(if cfg.cuda then pkgs.blender.override { cudaSupport = true; } else pkgs.blender)
pkgs.prusa-slicer
];
fonts.fontconfig.enable = true;
xdg.desktopEntries.prusaslicer-url-handler = {
name = "PrusaSlicer Protocol Handler";
exec = "prusa-slicer %u";
type = "Application";
noDisplay = true;
mimeType = [ "x-scheme-handler/prusaslicer" ];
};
xdg.desktopEntries.prusaslicer-url-handler = {
name = "PrusaSlicer Protocol Handler";
exec = "prusa-slicer %u";
type = "Application";
noDisplay = true;
mimeType = [ "x-scheme-handler/prusaslicer" ];
};
}
-29
View File
@@ -1,29 +0,0 @@
{
config,
pkgs,
inputs,
lib,
...
}:
let
claude-desktop = pkgs.callPackage ../../../pkgs/claude-desktop/package.nix {
inherit (config.custom.aiApps) deviceScaleFactor;
};
codex-cli = inputs.codex-cli.packages.${pkgs.stdenv.hostPlatform.system}.default;
t3-code = pkgs.callPackage ../../../pkgs/t3-code/package.nix {
inherit (config.custom.aiApps) deviceScaleFactor;
};
in
{
imports = [ inputs.codex-desktop-linux.homeManagerModules.default ];
home.packages = lib.optionals pkgs.stdenv.hostPlatform.isx86_64 [
claude-desktop
t3-code
];
programs.codexDesktopLinux = {
enable = true;
cliPackage = codex-cli;
};
}
+28 -18
View File
@@ -1,14 +1,18 @@
{
config,
pkgs,
inputs,
lib,
...
}:
let
# Official Anthropic Claude desktop app (Linux build, x86_64 only).
claude-desktop = pkgs.callPackage ../../../../pkgs/claude-desktop/package.nix {
inherit (config.custom.aiApps) deviceScaleFactor;
};
in
{
imports = [
inputs.agent-skills.homeModules.default
./synthetic.nix
];
imports = [ inputs.agent-skills.homeModules.default ];
# Shared UI-scale knob for the Electron AI desktop apps (Claude Desktop, T3 Code).
# Set per-host (e.g. konishi's 4K@1x monitors want ~"1.5"); null = native scale.
@@ -19,24 +23,24 @@
description = "--force-device-scale-factor value for Claude Desktop and T3 Code.";
};
# ~/.claude/settings.json stays unmanaged: Claude Code rewrites it itself
# (model pins, permission grants, plugin state), so any nix copy drifts within
# a session and every `nh home switch` then aborts on the diff.
config.home = {
packages = with pkgs; [
inputs.claude-code.packages.${pkgs.stdenv.hostPlatform.system}.default
packages =
with pkgs;
[
inputs.claude-code.packages.${pkgs.stdenv.hostPlatform.system}.default
# sandbox
bubblewrap
socat
libseccomp
# sandbox
bubblewrap
socat
libseccomp
# voice
sox
# voice
sox
# beads
dolt
];
# beads
dolt
]
++ lib.optional pkgs.stdenv.hostPlatform.isx86_64 claude-desktop;
file = {
".default-npm-packages".text = ''
@@ -44,11 +48,17 @@
@beads/bd
'';
".claude/statusline.sh" = {
source = ./statusline.sh;
executable = true;
};
};
mutableFiles.".claude/settings.json".source = ./settings.json;
};
}
+86
View File
@@ -0,0 +1,86 @@
{
"$schema": "https://json.schemastore.org/claude-code-settings.json",
"permissions": {
"allow": [
"Edit",
"Write",
"Bash(ls:*)",
"Bash(tree:*)",
"Bash(cat:*)",
"Bash(head:*)",
"Bash(tail:*)",
"Bash(find:*)",
"Bash(fd:*)",
"Bash(grep:*)",
"Bash(rg:*)",
"Bash(wc:*)",
"Bash(sort:*)",
"Bash(uniq:*)",
"Bash(diff:*)",
"Bash(pwd:*)",
"Bash(which:*)",
"Bash(jq:*)",
"Bash(git:*)",
"Bash(gh:*)",
"Bash(sed:*)",
"Bash(cp:*)",
"Bash(chmod:*)",
"Bash(mkdir:*)",
"Bash(kitty @ set-tab-title:*)"
],
"additionalDirectories": [
"/home/naps62/projects",
"/home/naps62/ethui",
"/home/naps62/labs",
"/home/naps62/subvisual"
]
},
"model": "opus[1m]",
"statusLine": {
"type": "command",
"command": "~/.claude/statusline.sh"
},
"enabledPlugins": {
"typescript-lsp@claude-plugins-official": true
},
"extraKnownMarketplaces": {
"impeccable": {
"source": {
"source": "github",
"repo": "pbakaus/impeccable"
}
}
},
"sandbox": {
"enabled": true,
"autoAllowBashIfSandboxed": false,
"allowedNetworkHosts": [
"github.com",
"api.github.com",
"raw.githubusercontent.com",
"gist.github.com",
"release-assets.githubusercontent.com"
]
},
"tui": "fullscreen",
"voiceEnabled": true,
"defaultMode": "acceptEdits",
"feedbackSurveyState": {
"lastShownTime": 1754052643456
},
"mcpServers": {
"herd-mcp": {
"type": "http",
"url": "https://mcp.herd.eco/v1"
},
"linear": {
"type": "http",
"url": "https://mcp.linear.app/mcp"
},
"home-assistant": {
"type": "http",
"url": "https://ha-mcp.n62.casa/mcp"
}
},
"agentPushNotifEnabled": true
}
-76
View File
@@ -1,76 +0,0 @@
{
pkgs,
inputs,
...
}:
# Claude Code pointed at synthetic.new. The three model aliases below are bound
# to three Synthetic models, so `/model opus|sonnet|haiku` swaps between them
# live; anything else there is reachable by full id (`/model hf:...`).
let
claude = inputs.claude-code.packages.${pkgs.stdenv.hostPlatform.system}.default;
# `hf:` ids pin one exact model. The `syn:large:*` aliases in Synthetic's own
# docs move under you whenever they rotate what "large" means.
kimi = "hf:moonshotai/Kimi-K3";
glm = "hf:zai-org/GLM-5.2";
# Not Qwen3.6-27B for the haiku slot: its chat template rejects Claude Code's
# multi-block system prompt with "System message must be at the beginning."
glmFlash = "hf:zai-org/GLM-4.7-Flash";
in
{
home.packages = [
(pkgs.writeShellScriptBin "synclaude" ''
set -euo pipefail
if [ -z "''${SYNTHETIC_API_KEY:-}" ] && [ -r "$HOME/.env.claude" ]; then
set +u
# Silenced: the file greets missing tooling on stderr, which would
# otherwise print before every session.
. "$HOME/.env.claude" >/dev/null 2>&1
set -u
fi
if [ -z "''${SYNTHETIC_API_KEY:-}" ]; then
echo "synclaude: SYNTHETIC_API_KEY is not set" >&2
exit 1
fi
export ANTHROPIC_BASE_URL="https://api.synthetic.new/anthropic"
export ANTHROPIC_AUTH_TOKEN="$SYNTHETIC_API_KEY"
export ANTHROPIC_DEFAULT_OPUS_MODEL="${kimi}"
export ANTHROPIC_DEFAULT_SONNET_MODEL="${glm}"
export ANTHROPIC_DEFAULT_HAIKU_MODEL="${glmFlash}"
export CLAUDE_CODE_SUBAGENT_MODEL="${glm}"
export CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC=1
export CLAUDE_CODE_ATTRIBUTION_HEADER=0
# ~/.env.claude exports both. ANTHROPIC_MODEL outranks the DEFAULT_* trio
# above, and ANTHROPIC_API_KEY would send Anthropic's credential to
# Synthetic.
unset ANTHROPIC_MODEL ANTHROPIC_API_KEY
# These two dokploy tools spell ttl's bound `"exclusiveMinimum": true`,
# which is draft-04. Synthetic validates against draft-2020, where the key
# must be a number, and rejects the whole request a bare 500 with no body
# when streaming. Every session fails, not just ones that call these.
broken_tools=(
mcp__dokploy__dnsProvider-createRecord
mcp__dokploy__dnsProvider-updateRecord
)
# ~/.claude/settings.json pins model "opus[1m]". The [1m] context-window
# suffix survives alias resolution and reaches Synthetic as part of the id,
# which 404s there so pick the alias explicitly unless the caller did.
for arg in "$@"; do
case "$arg" in
--model | --model=*)
exec ${claude}/bin/claude --disallowedTools "''${broken_tools[@]}" "$@"
;;
esac
done
exec ${claude}/bin/claude --model sonnet --disallowedTools "''${broken_tools[@]}" "$@"
'')
];
}
+9
View File
@@ -7,5 +7,14 @@ let
codex-cli = inputs.codex-cli.packages.${pkgs.stdenv.hostPlatform.system}.default;
in
{
imports = [ inputs.codex-desktop-linux.homeManagerModules.default ];
# cliPackage only tells the desktop app where to find codex; it does not put
# it on PATH, so the cli needs installing separately.
home.packages = [ codex-cli ];
programs.codexDesktopLinux = {
enable = true;
cliPackage = codex-cli;
};
}
+11
View File
@@ -1,13 +1,16 @@
{
lib,
pkgs,
config,
...
}:
{
imports = [
../features/mutable-file.nix
./zsh.nix
./nix.nix
./neovim
./editors.nix
./rust.nix
./elixir.nix
./nodejs.nix
@@ -27,6 +30,14 @@
};
home = {
# mkDefault: hosts whose clone lives elsewhere (yolo, under ~/tea) override
# this with a plain assignment.
mutableFilesRepoPath = lib.mkDefault "${config.home.homeDirectory}/projects/nixos-config";
packages = with pkgs; [
impala
];
username = lib.mkDefault "naps62";
homeDirectory = lib.mkDefault "/home/${config.home.username}";
stateVersion = lib.mkDefault "24.05";
+6
View File
@@ -27,6 +27,9 @@ in
# Update Hyprland cursor theme
${pkgs.hyprland}/bin/hyprctl setcursor ${cursorTheme.dark} ${toString cursorSize}
'';
noctalia-theme = ''
noctalia msg theme-mode-set dark
'';
};
lightModeScripts = {
gtk-theme = ''
@@ -46,6 +49,9 @@ in
# Update Hyprland cursor theme
${pkgs.hyprland}/bin/hyprctl setcursor ${cursorTheme.light} ${toString cursorSize}
'';
noctalia-theme = ''
noctalia msg theme-mode-set light
'';
};
};
+42 -20
View File
@@ -1,13 +1,13 @@
{
config,
lib,
pkgs,
...
}:
{
imports = [
./darkman.nix
# mpv is installed by programs.mpv there, not as a bare package here.
../mpv.nix
./spicetify.nix
];
home = {
@@ -15,21 +15,43 @@
# various
google-chrome
thunar
obsidian
mpv
# screen recording — evaluating these
obs-studio
kooha # simple Wayland-native screen+audio recorder
vokoscreen-ng # GUI recorder with webcam overlay
gimp
font-manager
imv
pavucontrol
zathura
libsForQt5.qt5ct
kdePackages.qt6ct
nwg-look
xournalpp
jq
ffmpeg
unzip
# remote desktop
remmina
# networking
networkmanagerapplet
mtr
dnsutils
# communication
slack
ferdium
signal-desktop
zoom-us
# dev tools
yaak
bun
# themes
tela-icon-theme
];
@@ -61,36 +83,36 @@
};
xdg.configFile = {
# zathura: include noctalia-generated theme
"zathura/zathurarc".text = ''
set default-bg "#2e3440"
set default-fg "#eceff4"
set statusbar-bg "#3b4252"
set statusbar-fg "#eceff4"
set inputbar-bg "#3b4252"
set inputbar-fg "#eceff4"
set highlight-color "#ebcb8b"
set highlight-active-color "#88c0d0"
set recolor-lightcolor "#2e3440"
set recolor-darkcolor "#eceff4"
include noctaliarc
'';
# qt: dark color scheme shipped by qt5ct/qt6ct themselves. Applied via
# QT_QPA_PLATFORMTHEME=qt6ct, set in the Hyprland env block (and imported
# into the dbus activation environment so the xdph screen-share picker
# inherits it too). custom_palette=true is required for qt5ct/qt6ct to
# actually apply the color scheme; Fusion is used because it fully honors a
# custom palette (Breeze/native styles partly ignore it).
# gtk: include noctalia-generated css (mkForce to override gtk module)
"gtk-3.0/gtk.css".text = lib.mkForce ''
@import url("noctalia.css");
'';
"gtk-4.0/gtk.css".text = lib.mkForce ''
@import url("noctalia.css");
'';
# qt: use noctalia color scheme. Applied via QT_QPA_PLATFORMTHEME=qt6ct,
# set in the Hyprland env block (and imported into the dbus activation
# environment so the xdph screen-share picker inherits it too).
# custom_palette=true is required for qt5ct/qt6ct to actually apply the
# color scheme; Fusion is used because it fully honors a custom palette
# (Breeze/native styles partly ignore it).
"qt5ct/qt5ct.conf".text = ''
[Appearance]
style=Fusion
custom_palette=true
color_scheme_path=${pkgs.libsForQt5.qt5ct}/share/qt5ct/colors/darker.conf
color_scheme_path=${config.home.homeDirectory}/.config/qt5ct/colors/noctalia.conf
'';
"qt6ct/qt6ct.conf".text = ''
[Appearance]
style=Fusion
custom_palette=true
color_scheme_path=${pkgs.kdePackages.qt6ct}/share/qt6ct/colors/darker.conf
color_scheme_path=${config.home.homeDirectory}/.config/qt6ct/colors/noctalia.conf
'';
};
}
-12
View File
@@ -1,12 +0,0 @@
{ pkgs, ... }:
{
home.packages = with pkgs; [
doctl
awscli2
terraform
bruno
coturn
usbutils
ktlint
];
}
+8 -1
View File
@@ -5,8 +5,8 @@
{
imports = [
./codex.nix
./pi.nix
./ralph-claude-code.nix
./t3-code.nix
];
programs = {
@@ -25,12 +25,19 @@
home.packages = with pkgs; [
imagemagick
doctl
awscli2
terraform
inotify-tools
devenv
bruno
sshfs
coturn
file
usbutils
nmap
lsof
ktlint
croc
opencode
process-compose
-3
View File
@@ -19,9 +19,6 @@ tmp/**/*
*.log
*.tmp
# Local git worktree checkouts
worktrees/
# Latex
_inputs.tex
_sections.tex
+23 -48
View File
@@ -10,8 +10,6 @@ let
hyprPkgs = inputs.hyprland.packages.${pkgs.stdenv.hostPlatform.system};
osd = "${pkgs.swayosd}/bin/swayosd-client";
# xdph runs this instead of hyprland-share-picker (screencopy:custom_picker_binary).
# The contract is just "print [SELECTION] to stdout"; a leading "r" grants a
# restore token. Printing it immediately means the dialog never renders.
@@ -92,21 +90,6 @@ in
description = "Cursor theme darkman selects in light mode.";
};
};
panelScale = lib.mkOption {
type = lib.types.float;
default = 1.0;
example = 1.5;
description = "Multiplier for every length in the eww dashboard stylesheet.";
};
verticalOutputs = lib.mkOption {
type = lib.types.listOf lib.types.str;
default = [ ];
example = [ "DP-2" ];
description = ''
Outputs that get the portrait wallpaper set. Everything else gets the
landscape one.
'';
};
shareOutput = lib.mkOption {
type = lib.types.nullOr lib.types.str;
default = null;
@@ -121,9 +104,8 @@ in
imports = [
./cursor.nix
./eww
./kbptr.nix
./shell.nix
./noctalia
./wallpapers.nix
];
@@ -245,8 +227,8 @@ in
hl.env("HYPRCURSOR_THEME", "${cfg.cursorTheme.dark}")
hl.env("HYPRCURSOR_SIZE", "${toString cfg.cursorSize}")
-- Route Qt6 apps (incl. the xdph screen-share picker) through qt6ct so
-- the dark color scheme actually applies. Without this var the
-- qt6ct.conf is never read.
-- the noctalia-generated color scheme actually applies. Without this
-- var the qt6ct.conf is never read.
hl.env("QT_QPA_PLATFORMTHEME", "qt6ct")
-- autostart
@@ -256,15 +238,14 @@ in
hl.exec_cmd("dbus-update-activation-environment --systemd WAYLAND_DISPLAY XDG_CURRENT_DESKTOP QT_QPA_PLATFORMTHEME")
hl.exec_cmd("hyprctl setcursor ${cfg.cursorTheme.dark} ${toString cfg.cursorSize}")
hl.exec_cmd("hyprsunset")
-- eww, wpaperd, mako and the polkit agent are systemd user services
-- bound to graphical-session.target; nothing to start here.
hl.exec_cmd("noctalia")
-- kdeconnectd is only DBus-activated; nothing starts it at login,
-- so clipboard sync stays dead until the indicator runs.
hl.exec_cmd("kdeconnect-indicator")
end)
-- layer rules
hl.layer_rule({ match = { namespace = "eww-panel" }, blur = true, ignore_alpha = 0.5 })
hl.layer_rule({ match = { namespace = "noctalia-wallpaper" }, blur = true, ignore_alpha = 0.5 })
-- workspace rules
hl.workspace_rule({ workspace = "w[t1]", gaps_out = 0, gaps_in = 0 }) -- no gaps when only window
@@ -292,11 +273,7 @@ in
})
hl.window_rule({ match = { class = "thunar" }, float = true, size = "1800 1200", move = "(cursor_x-(window_w*0.5)) (cursor_y-(window_h*0.5))" })
-- nixpkgs wraps blueman with --inherit-argv0, so the class is
-- blueman-manager, not the .blueman-manager-wrapped the old rule expected.
hl.window_rule({ match = { class = [[.*[Bb]lueman.*]] }, float = true, size = "1200 800", move = "(cursor_x-(window_w*0.5)) (cursor_y-(window_h*0.5))" })
hl.window_rule({ match = { class = "nm-connection-editor" }, float = true, size = "1200 800", move = "(cursor_x-(window_w*0.5)) (cursor_y-(window_h*0.5))" })
hl.window_rule({ match = { class = "waypaper" }, float = true, size = "1600 1000", move = "(cursor_x-(window_w*0.5)) (cursor_y-(window_h*0.5))" })
hl.window_rule({ match = { class = [[\.blueman-manager-wrapped]] }, float = true, size = "1200 800", move = "(cursor_x-(window_w*0.5)) (cursor_y-(window_h*0.5))" })
hl.window_rule({ match = { class = [[org\.pulseaudio\.pavucontrol]] }, float = true, size = "1200 1200", move = "(cursor_x-(window_w*0.5)) (cursor_y-(window_h*0.5))" })
-- wine / game installers (Inno Setup temp windows, e.g. Heroic/GOG).
@@ -319,7 +296,7 @@ in
hl.window_rule({ match = { workspace = "special:yazi" }, center = true })
-- keybinds
hl.bind(mod .. " + N", hl.dsp.exec_cmd("eww-panel"))
hl.bind(mod .. " + N", hl.dsp.exec_cmd("noctalia msg panel-toggle control-center"))
hl.bind(mod .. " + T", hl.dsp.exec_cmd("kitty"))
hl.bind(mod .. " + V", hl.dsp.window.float({ action = "toggle" }))
hl.bind(mod .. " + Q", hl.dsp.window.close())
@@ -329,7 +306,7 @@ in
-- lock (routes through logind -> hypridle lock_cmd -> hyprlock)
hl.bind(mod .. " + CTRL + L", hl.dsp.exec_cmd("loginctl lock-session"))
hl.bind(mod .. " + space", hl.dsp.exec_cmd("fuzzel"))
hl.bind(mod .. " + space", hl.dsp.exec_cmd("noctalia msg panel-toggle launcher"))
-- printscreen
hl.bind("Print", hl.dsp.exec_cmd("hyprshot -m region --raw | satty --filename - --output-filename ~/downloads/screenshots/$(date +%Y-%m-%d_%H-%M-%S).png"))
@@ -367,21 +344,18 @@ in
hl.dispatch(hl.dsp.window.center())
end)
-- volume / brightness / media (locked so they work on the lock screen;
-- volume and brightness repeat on hold). Everything goes through
-- swayosd-client, which applies the change and draws the OSD popup;
-- store paths because Hyprland execs these with the login PATH, which
-- doesn't pick up profile changes made after the session started.
hl.bind("XF86AudioRaiseVolume", hl.dsp.exec_cmd("${osd} --output-volume raise"), { locked = true, repeating = true })
hl.bind("XF86AudioLowerVolume", hl.dsp.exec_cmd("${osd} --output-volume lower"), { locked = true, repeating = true })
hl.bind("XF86MonBrightnessUp", hl.dsp.exec_cmd("${osd} --brightness raise"), { locked = true, repeating = true })
hl.bind("XF86MonBrightnessDown", hl.dsp.exec_cmd("${osd} --brightness lower"), { locked = true, repeating = true })
-- volume / brightness (locked so they work on the lock screen; repeat on hold)
hl.bind("XF86AudioRaiseVolume", hl.dsp.exec_cmd("wpctl set-volume @DEFAULT_AUDIO_SINK@ 5%+"), { locked = true, repeating = true })
hl.bind("XF86AudioLowerVolume", hl.dsp.exec_cmd("wpctl set-volume @DEFAULT_AUDIO_SINK@ 5%-"), { locked = true, repeating = true })
hl.bind("XF86MonBrightnessUp", hl.dsp.exec_cmd("brightnessctl set 5%+"), { locked = true, repeating = true })
hl.bind("XF86MonBrightnessDown", hl.dsp.exec_cmd("brightnessctl set 5%-"), { locked = true, repeating = true })
hl.bind("XF86AudioMute", hl.dsp.exec_cmd("${osd} --output-volume mute-toggle"), { locked = true })
hl.bind("XF86AudioMicMute", hl.dsp.exec_cmd("${osd} --input-volume mute-toggle"), { locked = true })
hl.bind("XF86AudioNext", hl.dsp.exec_cmd("${osd} --playerctl next"), { locked = true })
hl.bind("XF86AudioPrev", hl.dsp.exec_cmd("${osd} --playerctl prev"), { locked = true })
hl.bind("XF86AudioPlay", hl.dsp.exec_cmd("${osd} --playerctl play-pause"), { locked = true })
-- media keys (locked)
hl.bind("XF86AudioMute", hl.dsp.exec_cmd("pactl set-sink-mute @DEFAULT_SINK@ toggle"), { locked = true })
hl.bind("XF86AudioMicMute", hl.dsp.exec_cmd("volumectl -m toggle-mute"), { locked = true })
hl.bind("XF86AudioNext", hl.dsp.exec_cmd("playerctl next"), { locked = true })
hl.bind("XF86AudioPrev", hl.dsp.exec_cmd("playerctl previous"), { locked = true })
hl.bind("XF86AudioPlay", hl.dsp.exec_cmd("playerctl play-pause"), { locked = true })
-- move/resize windows with mod + LMB/RMB drag
hl.bind(mod .. " + mouse:272", hl.dsp.window.drag(), { mouse = true })
@@ -391,9 +365,10 @@ in
services.hyprpaper.enable = false;
# hyprlock: the lock screen. Clean minimal dark look; tweak colours/clock/
# font to taste. hypridle below drives it (idle + before-sleep), and
# $mod CTRL+L locks manually.
# hyprlock: the lock screen, replacing noctalia's built-in locker (which is
# ugly and — unlike hyprlock — opaque to scripts/lock-state detection).
# Clean minimal dark look; tweak colours/clock/font to taste. hypridle below
# drives it (idle + before-sleep), and $mod CTRL+L locks manually.
programs.hyprlock = {
enable = true;
settings = {
@@ -1,292 +0,0 @@
{
config,
lib,
pkgs,
inputs,
...
}:
let
cfg = config.custom.hyprland;
# Every length in the stylesheet goes through this, so one option resizes the
# whole dashboard for 4K hosts.
px = n: "${toString (builtins.ceil (n * cfg.panelScale))}px";
mkScript =
name: runtimeInputs:
pkgs.writeShellApplication {
inherit name runtimeInputs;
text = builtins.readFile (./scripts + "/${name}.sh");
};
battery = mkScript "battery" [
pkgs.coreutils
pkgs.findutils
pkgs.jq
];
network = mkScript "network" [
pkgs.networkmanager
pkgs.gawk
pkgs.gnused
pkgs.jq
];
bluetooth = mkScript "bluetooth" [
pkgs.bluez
pkgs.coreutils
pkgs.findutils
pkgs.gawk
pkgs.jq
];
audio = mkScript "audio" [
pkgs.gawk
pkgs.jq
pkgs.wireplumber
];
panelToggle = mkScript "eww-panel" [
config.programs.eww.package
pkgs.jq
inputs.hyprland.packages.${pkgs.stdenv.hostPlatform.system}.hyprland
];
eww = lib.getExe config.programs.eww.package;
bluetoothctl = "${pkgs.bluez}/bin/bluetoothctl";
wpctl = "${pkgs.wireplumber}/bin/wpctl";
wpaperctl = "${pkgs.wpaperd}/bin/wpaperctl";
# setsid, and before the close rather than after it: eww kills the onclick
# handler's process group when the window it came from goes away, so a bare
# `close panel; cmd` never reaches cmd.
act = cmd: "${pkgs.util-linux}/bin/setsid --fork ${cmd}; ${eww} close panel";
in
{
home.packages = [
panelToggle
pkgs.waypaper
];
programs.eww = {
enable = true;
systemd.enable = true;
yuckConfig = ''
;; Each script prints one JSON object; widgets read .icon / .label / .sub
;; off it.
(defpoll clock-time :interval "10s" :initial "--:--" `date +%H:%M`)
(defpoll clock-date :interval "1h" :initial "" `date "+%A, %d %B"`)
(defpoll battery
:interval "30s"
:initial '{"present":false,"icon":"","label":"","sub":""}'
`${lib.getExe battery}`)
(defpoll network
:interval "10s"
:initial '{"icon":"","label":"...","sub":""}'
`${lib.getExe network}`)
(defpoll bluetooth
:interval "10s"
:initial '{"powered":false,"icon":"","label":"...","sub":""}'
`${lib.getExe bluetooth}`)
(defpoll speaker
:interval "2s"
:initial '{"present":false,"volume":0,"muted":true,"icon":""}'
`${lib.getExe audio} sink`)
(defpoll mic
:interval "2s"
:initial '{"present":false,"volume":0,"muted":true,"icon":""}'
`${lib.getExe audio} source`)
(defwidget card [title]
(box :class "card" :orientation "v" :space-evenly false
(label :class "card-title" :halign "start" :text title :visible {title != ""})
(box :orientation "v" :space-evenly false
(children))))
(defwidget row [icon label sub onclick]
(eventbox :class "row" :cursor "pointer" :onclick onclick
(box :space-evenly false
(label :class "row-icon" :text icon)
(box :orientation "v" :space-evenly false :hexpand true
(label :class "row-label" :halign "start" :text label)
(label :class "row-sub" :halign "start" :text sub :visible {sub != ""})))))
;; Mute button, slider, readout. `state` is the speaker/mic json object.
(defwidget volume [state node]
(box :space-evenly false :visible {state.present}
(button :class {state.muted ? "mute muted" : "mute"}
:onclick "${wpctl} set-mute ''${node} toggle"
{state.icon})
(scale :class "slider" :hexpand true
:value {state.volume} :min 0 :max 101
:onchange "${wpctl} set-volume ''${node} {}%")
(label :class "row-sub" :text {"''${state.volume}%"})))
(defwidget action [icon tooltip onclick]
(button :class "action" :tooltip tooltip :onclick onclick icon))
(defwidget dashboard []
(box :class "dash" :orientation "v" :space-evenly false
(box :space-evenly false
(card :title ""
(label :class "clock-time" :halign "start" :text clock-time)
(label :class "clock-date" :halign "start" :text clock-date))
(card :title "System"
(box :visible {battery.present} :space-evenly false
(row :icon {battery.icon} :label {battery.label}
:sub {battery.sub} :onclick ""))
(row :icon {network.icon} :label {network.label} :sub {network.sub}
:onclick "${act "${pkgs.networkmanagerapplet}/bin/nm-connection-editor"}")
(row :icon {bluetooth.icon} :label {bluetooth.label} :sub {bluetooth.sub}
:onclick "${act "${pkgs.blueman}/bin/blueman-manager"}")))
(box :space-evenly false
(card :title "Audio"
(volume :state speaker :node "@DEFAULT_AUDIO_SINK@")
(volume :state mic :node "@DEFAULT_AUDIO_SOURCE@"))
(card :title "Actions"
(box :space-evenly false :halign "start"
(action :icon "󰑐" :tooltip "Next wallpaper"
:onclick "${wpaperctl} next")
(action :icon "󰸉" :tooltip "Pick wallpaper"
:onclick "${act "${lib.getExe pkgs.waypaper}"}")
(action :icon "󰂯" :tooltip "Toggle bluetooth"
:onclick {bluetooth.powered
? "${bluetoothctl} power off"
: "${bluetoothctl} power on"})
(action :icon "󰌾" :tooltip "Lock"
:onclick "${act "${pkgs.systemd}/bin/loginctl lock-session"}"))))))
;; Sized to its content. A full-screen backdrop would give click-outside-
;; to-close, but its handler also fires for clicks on the sliders.
(defwindow panel
:namespace "eww-panel"
:geometry (geometry :anchor "center")
:stacking "overlay"
(dashboard))
'';
scssConfig = ''
$bg: rgba(46, 52, 64, 0.96);
$card: rgba(59, 66, 82, 0.72);
$border: #4c566a;
$fg: #eceff4;
$muted: #81a1c1;
$accent: #88c0d0;
$red: #bf616a;
* {
all: unset;
font-family: "FiraCode Nerd Font", monospace;
}
window {
background-color: transparent;
}
.dash {
background-color: $bg;
border: ${px 1} solid $border;
border-radius: ${px 20};
padding: ${px 14};
color: $fg;
}
.card {
background-color: $card;
border-radius: ${px 14};
padding: ${px 16};
margin: ${px 6};
min-width: ${px 300};
}
.card-title {
font-size: ${px 11};
color: $muted;
margin-bottom: ${px 8};
}
.clock-time {
font-size: ${px 56};
font-weight: 600;
}
.clock-date {
font-size: ${px 14};
color: $muted;
}
.row {
border-radius: ${px 10};
padding: ${px 10} ${px 12};
&:hover {
background-color: rgba(136, 192, 208, 0.14);
}
}
.row-icon {
font-size: ${px 22};
color: $accent;
min-width: ${px 34};
}
.row-label {
font-size: ${px 14};
}
.row-sub {
font-size: ${px 11};
color: $muted;
}
.mute {
font-size: ${px 20};
color: $accent;
min-width: ${px 34};
&.muted {
color: $red;
}
}
.slider {
margin: 0 ${px 12};
min-width: ${px 180};
trough {
background-color: rgba(76, 86, 106, 0.6);
border-radius: ${px 8};
min-height: ${px 8};
highlight {
background-color: $accent;
border-radius: ${px 8};
}
}
}
.action {
background-color: rgba(76, 86, 106, 0.45);
border-radius: ${px 12};
padding: ${px 12} ${px 18};
margin-right: ${px 8};
font-size: ${px 20};
color: $fg;
&:hover {
background-color: $accent;
color: #2e3440;
}
}
'';
};
}
@@ -1,37 +0,0 @@
# One JSON object per audio node. $1 is "sink" or "source". wpctl prints
# "Volume: 0.45", with " [MUTED]" appended, and errors when there is no node.
case "$1" in
sink) node="@DEFAULT_AUDIO_SINK@" ;;
source) node="@DEFAULT_AUDIO_SOURCE@" ;;
*) echo "usage: audio sink|source" >&2; exit 2 ;;
esac
raw=$(wpctl get-volume "$node" 2>/dev/null || true)
case "$raw" in
*Volume:*) ;;
*)
jq -nc '{ present: false, volume: 0, muted: true, icon: "󰖁" }'
exit 0
;;
esac
volume=$(printf '%s' "$raw" | awk '{ printf "%d", $2 * 100 }')
muted=false
case "$raw" in *MUTED*) muted=true ;; esac
if [ "$1" = "source" ]; then
if [ "$muted" = true ]; then icon="󰍭"; else icon="󰍬"; fi
elif [ "$muted" = true ]; then icon="󰖁"
elif [ "$volume" -ge 66 ]; then icon="󰕾"
elif [ "$volume" -ge 33 ]; then icon="󰖀"
else icon="󰕿"
fi
jq -nc \
--argjson volume "$volume" \
--argjson muted "$muted" \
--arg icon "$icon" \
'{ present: true, volume: $volume, muted: $muted, icon: $icon }'
@@ -1,32 +0,0 @@
# One JSON object for the panel's battery row. `present: false` on the desktops,
# which have no BAT* at all — the row hides itself on that.
bat=$(find /sys/class/power_supply -maxdepth 1 -name 'BAT*' 2>/dev/null | sort | head -1)
if [ -z "$bat" ]; then
jq -nc '{ present: false, icon: "", label: "", sub: "" }'
exit 0
fi
capacity=$(cat "$bat/capacity")
status=$(cat "$bat/status")
if [ "$status" = "Charging" ] || [ "$status" = "Full" ]; then
icon="󰂄"
elif [ "$capacity" -ge 90 ]; then icon="󰁹"
elif [ "$capacity" -ge 80 ]; then icon="󰂂"
elif [ "$capacity" -ge 70 ]; then icon="󰂁"
elif [ "$capacity" -ge 60 ]; then icon="󰂀"
elif [ "$capacity" -ge 50 ]; then icon="󰁿"
elif [ "$capacity" -ge 40 ]; then icon="󰁾"
elif [ "$capacity" -ge 30 ]; then icon="󰁽"
elif [ "$capacity" -ge 20 ]; then icon="󰁼"
elif [ "$capacity" -ge 10 ]; then icon="󰁻"
else icon="󰁺"
fi
jq -nc \
--arg icon "$icon" \
--arg label "$capacity%" \
--arg sub "$status" \
'{ present: true, icon: $icon, label: $label, sub: $sub }'
@@ -1,22 +0,0 @@
# One JSON object for the panel's bluetooth row. bluetoothctl blocks forever
# instead of erroring when there is no adapter, hence the /sys check + timeouts.
if [ -z "$(find /sys/class/bluetooth -mindepth 1 -maxdepth 1 2>/dev/null)" ]; then
jq -nc '{ powered: false, icon: "󰂲", label: "No adapter", sub: "" }'
exit 0
fi
powered=$(timeout 2 bluetoothctl show 2>/dev/null | awk '/Powered:/ { print $2; exit }' || true)
if [ "$powered" != "yes" ]; then
jq -nc '{ powered: false, icon: "󰂲", label: "Off", sub: "" }'
exit 0
fi
connected=$(timeout 2 bluetoothctl devices Connected 2>/dev/null | cut -d' ' -f3- | paste -sd', ' - || true)
if [ -n "$connected" ]; then
jq -nc --arg sub "$connected" '{ powered: true, icon: "󰂱", label: "On", sub: $sub }'
else
jq -nc '{ powered: true, icon: "󰂯", label: "On", sub: "Nothing connected" }'
fi
@@ -1,11 +0,0 @@
# Toggle the panel on whichever monitor has focus. `eww open --toggle` can't do
# this: it ignores --screen when the window is already open elsewhere, so the
# panel would stay stuck on the monitor it first appeared on.
if eww active-windows | grep -q '^panel'; then
exec eww close panel
fi
screen=$(hyprctl -j monitors | jq -r 'map(select(.focused))[0].id // 0')
exec eww open panel --screen "$screen"
@@ -1,43 +0,0 @@
# One JSON object for the panel's network row. State must match "connected"
# exactly: NetworkManager reports docker0 and br-* as "connected (externally)".
status=$(nmcli -t -f TYPE,STATE,CONNECTION device status 2>/dev/null || true)
# nmcli's terse output backslash-escapes colons inside connection names, so
# rejoin fields 3..NF and unescape rather than taking $3.
pick() {
printf '%s\n' "$status" | awk -F: -v want="$1" '
$1 == want && $2 == "connected" {
name = $3
for (i = 4; i <= NF; i++) name = name ":" $i
print name
exit
}' | sed 's/\\:/:/g'
}
wifi=$(pick wifi)
ethernet=$(pick ethernet)
if [ -n "$wifi" ]; then
signal=$(nmcli -t -f IN-USE,SIGNAL dev wifi 2>/dev/null | awk -F: '$1 == "*" { print $2; exit }' || true)
if [ -z "$signal" ]; then icon="󰤨"
elif [ "$signal" -ge 75 ]; then icon="󰤨"
elif [ "$signal" -ge 50 ]; then icon="󰤥"
elif [ "$signal" -ge 25 ]; then icon="󰤢"
else icon="󰤟"
fi
if [ -n "$signal" ]; then
sub="Wi-Fi · $signal%"
else
sub="Wi-Fi"
fi
jq -nc --arg icon "$icon" --arg label "$wifi" --arg sub "$sub" \
'{ icon: $icon, label: $label, sub: $sub }'
elif [ -n "$ethernet" ]; then
jq -nc --arg label "$ethernet" '{ icon: "󰈀", label: $label, sub: "Ethernet" }'
else
jq -nc '{ icon: "󰤭", label: "Offline", sub: "" }'
fi
@@ -0,0 +1,104 @@
{
config,
lib,
inputs,
pkgs,
...
}:
let
avatar = pkgs.fetchurl {
url = "https://github.com/naps62.png";
sha256 = "1ck11xg4rwcvn8dcc06ax7i9yfzy21v1n6h5mcjrkrici5sznlsv";
};
in
{
imports = [ inputs.noctalia.homeModules.default ];
# Noctalia v5 rewrote the config schema (TOML, snake_case, flat sections).
# The v4 settings (appLauncher.*, bar.widgets.*, controlCenter.shortcuts.*, etc.)
# have no v5 equivalents — most must now be tweaked through the in-shell
# Settings UI. See docs.noctalia.dev/v5 and example.toml in the upstream repo.
programs.noctalia = {
enable = true;
settings = {
shell = {
font_family = "Sans Serif";
avatar_path = "${config.home.homeDirectory}/.face";
telemetry_enabled = false;
polkit_agent = true;
};
wallpaper = {
enabled = true;
fill_mode = "crop";
directory = "${config.home.homeDirectory}/.cache/wallpapers/3840x2160";
transition = [ "fade" ];
transition_duration = 1500;
automation = {
enabled = true;
interval_minutes = 360;
order = "random";
recursive = true;
};
};
theme = {
mode = "dark";
source = "builtin";
builtin = "Nord";
templates = {
enable_builtin_templates = true;
builtin_ids = [
"btop"
"gtk3"
"gtk4"
"hyprland"
"qt"
];
};
};
notification.enable_daemon = true;
weather = {
enabled = true;
unit = "celsius";
};
location = {
auto_locate = false;
address = "Braga, Portugal";
};
nightlight = {
enabled = true;
temperature_day = 6500;
temperature_night = 4000;
};
bar.main = {
position = "top";
background_opacity = 0.93;
radius = 12;
start = [ "launcher" "wallpaper" "workspaces" ];
center = [ "media" "clock" ];
end = [ "tray" "notifications" "network" "bluetooth" "volume" "brightness" "battery" "control-center" "session" ];
};
};
};
home.file.".face".source = avatar;
# force overwrite files that noctalia may have turned into regular files
# (Hyprland 0.55+ is Lua-only, so the managed file is now hyprland.lua)
xdg.configFile."hypr/hyprland.lua".force = true;
xdg.configFile."gtk-3.0/gtk.css".force = true;
# NOTE: noctalia still writes its color theme to ~/.config/hypr/noctalia.conf
# in *hyprlang* syntax ($primary = rgb(...), general { col.active_border = ... }).
# Hyprland 0.55+ dropped the hyprlang parser entirely, so that file can no
# longer be `source`d — there is no hl.source, and the lua config can't parse
# hyprlang. Hyprland therefore uses the static border colors from
# ../default.nix instead of noctalia's themed ones. To restore live theming,
# noctalia would need to emit a Lua snippet we can require() here.
}
-84
View File
@@ -1,84 +0,0 @@
{ pkgs, ... }:
{
programs.fuzzel = {
enable = true;
settings = {
main = {
terminal = "${pkgs.kitty}/bin/kitty";
layer = "overlay";
font = "FiraCode Nerd Font:size=12";
icon-theme = "Tela black";
lines = 12;
width = 40;
inner-pad = 8;
};
border = {
radius = 12;
width = 1;
};
colors = {
background = "2e3440f0";
text = "eceff4ff";
match = "88c0d0ff";
selection = "434c5eff";
selection-text = "eceff4ff";
border = "4c566aff";
};
};
};
services.mako = {
enable = true;
settings = {
font = "FiraCode Nerd Font 11";
background-color = "#2e3440f0";
text-color = "#eceff4";
border-color = "#4c566a";
border-radius = 12;
border-size = 1;
padding = "14";
margin = "12";
default-timeout = 6000;
anchor = "top-right";
layer = "overlay";
};
};
# swayosd: the on-screen volume/brightness/caps-lock popup. Noctalia drew this
# before; without it the XF86 keys still work but give no feedback, which reads
# as "the keys are dead". swayosd-client does the change *and* the popup, so the
# binds in default.nix call it instead of wpctl/brightnessctl directly.
services.swayosd = {
enable = true;
topMargin = 0.85;
stylePath = pkgs.writeText "swayosd-style.css" ''
window#osd {
padding: 12px 20px;
border-radius: 12px;
border: 1px solid #4c566a;
background: alpha(#2e3440, 0.94);
}
window#osd #container { margin: 12px; }
window#osd image,
window#osd label { color: #eceff4; }
window#osd progressbar:disabled { opacity: 0.5; }
window#osd trough {
min-height: 6px;
border-radius: 6px;
background: #434c5e;
}
window#osd progress {
min-height: 6px;
border-radius: 6px;
background: #88c0d0;
}
'';
};
services.hyprpolkitagent.enable = true;
}
+1 -27
View File
@@ -1,10 +1,5 @@
{ config, pkgs, ... }:
{ pkgs, ... }:
let
cfg = config.custom.hyprland;
horizontalDir = "${config.home.homeDirectory}/.cache/wallpapers/3840x2160";
verticalDir = "${config.home.homeDirectory}/.cache/wallpapers/2160x3840";
# Horizontal wallpapers (3840x2160) for HDMI-A-1 and DP-1
# Just add any image URL you want here
horizontal = [
@@ -112,25 +107,4 @@ let
in
{
home.file.".local/bin/wallpaper-fetch".source = fetchScript;
# `any` covers every output not named in custom.hyprland.verticalOutputs,
# which get the portrait set instead.
services.wpaperd = {
enable = true;
settings =
{
default = {
duration = "6h";
sorting = "random";
mode = "center";
};
any.path = horizontalDir;
}
// builtins.listToAttrs (
map (output: {
name = output;
value.path = verticalDir;
}) cfg.verticalOutputs
);
};
}
+1 -1
View File
@@ -117,7 +117,7 @@ in
{
programs.kitty = {
enable = true;
# Fixed theme — nothing toggles kitty with dark/light mode.
# Fixed theme — noctalia no longer toggles kitty with dark/light mode.
themeFile = "Catppuccin-Mocha";
settings = {
confirm_os_window_close = 0;
-7
View File
@@ -1,7 +0,0 @@
{ inputs, ... }:
# Maestro: the terminal-session orchestrator. Module only — no `enable` here,
# because the daemon owns every interactive shell on the host it runs on, and
# only one box should be doing that. Hosts opt in from their own services.nix.
{
imports = [ inputs.maestro.homeManagerModules.default ];
}
-36
View File
@@ -1,36 +0,0 @@
{
config,
lib,
...
}:
let
cfg = config.custom.mpv;
in
{
options.custom.mpv = {
hwdec = lib.mkOption {
type = lib.types.str;
default = "auto-safe";
example = "nvdec-copy";
description = ''
Value for mpv's `hwdec`. `auto-safe` only picks a decoder mpv considers
reliable for the running driver and falls back to software otherwise,
so it is correct on Intel (arrakis) and on hosts with no usable GPU
decoder (yolo). NVIDIA hosts override it.
'';
};
};
config = {
programs.mpv = {
enable = true;
config = {
hwdec = cfg.hwdec;
vo = lib.mkDefault "gpu-next";
save-position-on-quit = true;
keep-open = "yes";
};
};
};
}
@@ -1,198 +0,0 @@
{
config,
lib,
pkgs,
...
}:
# nix-autodeploy: one webhook endpoint that turns "a repo I own pushed to main"
# into "that flake input is bumped, committed, and (optionally) applied".
#
# Replaces per-app deploy webhooks that rebuilt from a checkout on the box. The
# apps are flake inputs now, so deploying one is a lock bump plus a generation
# switch — the same operation for every app, hence one service instead of N.
let
cfg = config.services.nixAutodeploy;
# A user unit inherits almost no PATH, and the deploy shells out to git (with
# the gitea credential helper), nix and nh.
profilePath = lib.concatStringsSep ":" [
"%h/.local/bin"
"%h/.nix-profile/bin"
"/etc/profiles/per-user/%u/bin"
"/run/current-system/sw/bin"
];
deploy = pkgs.writeShellApplication {
name = "nix-autodeploy-deploy";
runtimeInputs = [
pkgs.git
pkgs.nix
pkgs.nh
pkgs.curl
pkgs.jq
pkgs.util-linux
];
text = ''
# usage: nix-autodeploy-deploy <flake-input> <apply|notify>
input=$1
mode=$2
flake=${lib.escapeShellArg cfg.flake}
topic=${lib.escapeShellArg cfg.ntfy.topic}
ntfy_url=${lib.escapeShellArg cfg.ntfy.url}
notify() {
[ -n "''${NTFY_TOKEN:-}" ] || return 0
curl -fsS -X POST "$ntfy_url/$topic" \
-H "Authorization: Bearer $NTFY_TOKEN" \
-H "Title: $1" -d "$2" > /dev/null || true
}
# Serialize: two pushes landing together would otherwise race on the same
# working tree and the same flake.lock.
exec 9> "''${XDG_RUNTIME_DIR:-/tmp}/nix-autodeploy.lock"
flock 9
cd "$flake"
branch=$(git rev-parse --abbrev-ref HEAD)
if [ "$branch" != main ]; then
notify "autodeploy skipped ($input)" "checkout is on $branch, not main"
exit 0
fi
if ! git diff --quiet || ! git diff --cached --quiet; then
notify "autodeploy skipped ($input)" "working tree is dirty"
exit 0
fi
git fetch --quiet origin main
git merge --ff-only --quiet origin/main
nix flake update "$input"
if git diff --quiet -- flake.lock; then
echo "autodeploy: $input already at the pushed revision"
exit 0
fi
rev=$(nix flake metadata --json |
jq -r --arg i "$input" '.locks.nodes[$i].locked.rev[0:7]')
git commit --quiet -m "chore(flake): bump $input to $rev" -- flake.lock
git push --quiet origin main
if [ "$mode" != apply ]; then
notify "$input bumped to $rev" "Not applied run 'nh home switch' when convenient."
exit 0
fi
if nh home switch "$flake"; then
notify "$input deployed" "Bumped to $rev and switched."
else
notify "$input FAILED to apply" "Lock is at $rev on main; the switch failed. See journalctl --user -u run-*."
exit 1
fi
'';
};
listener = pkgs.writers.writePython3Bin "nix-autodeploy" {
# Only line length: http.server's do_GET/do_POST spelling is already
# excused inline.
flakeIgnore = [ "E501" ];
} (builtins.readFile ./listener.py);
repoFile = (pkgs.formats.json { }).generate "nix-autodeploy-repos.json" (
lib.mapAttrs (_: r: { inherit (r) input apply; }) cfg.repos
);
in
{
options.services.nixAutodeploy = {
enable = lib.mkEnableOption "the forge-webhook listener that bumps and applies flake inputs";
port = lib.mkOption {
type = lib.types.port;
default = 7375;
description = "Port the listener binds on 0.0.0.0. There is no auth beyond the webhook HMAC, so only expose it through the reverse proxy.";
};
flake = lib.mkOption {
type = lib.types.str;
example = "/home/naps62/tea/nixos-config";
description = "Absolute path to the nixos-config checkout whose flake.lock gets bumped. Must be on main and clean, or the deploy skips.";
};
environmentFile = lib.mkOption {
type = lib.types.str;
example = "%h/.config/nix-autodeploy/env";
description = ''
File holding `NIX_AUTODEPLOY_SECRET` (the webhook HMAC secret, shared
with every repo below) and `NTFY_TOKEN`. Not in the store these are
secrets.
'';
};
ntfy = {
url = lib.mkOption {
type = lib.types.str;
default = "https://ntfy.home.naps.pt";
description = "Base URL of the ntfy server deploy results are posted to.";
};
topic = lib.mkOption {
type = lib.types.str;
default = "nix-autodeploy";
description = "ntfy topic for deploy results.";
};
};
repos = lib.mkOption {
default = { };
description = "Forge repositories to listen for, keyed by `<owner>/<repo>` exactly as the webhook payload spells it.";
example = lib.literalExpression ''
{ "yolo/rev" = { input = "rev"; }; }
'';
type = lib.types.attrsOf (
lib.types.submodule {
options = {
input = lib.mkOption {
type = lib.types.str;
description = "Name of the flake input in this repo's flake.nix that tracks that repository.";
};
apply = lib.mkOption {
type = lib.types.bool;
default = true;
description = ''
Whether to run `nh home switch` after the bump. Set false for an
app whose restart disrupts a live session the lock is still
bumped and pushed, and the ntfy message says it is waiting.
'';
};
};
}
);
};
};
config = lib.mkIf cfg.enable {
systemd.user.services.nix-autodeploy = {
Unit = {
Description = "nix-autodeploy forge webhooks bump and apply flake inputs";
After = [ "network.target" ];
# Same restart-budget trap every other always-on unit here avoids: at
# RestartSec=2 a fast-crashing listener would park in `failed`.
StartLimitIntervalSec = 0;
};
Service = {
Type = "simple";
ExecStart = lib.getExe listener;
EnvironmentFile = cfg.environmentFile;
Environment = [
"PATH=${profilePath}"
"NIX_AUTODEPLOY_PORT=${toString cfg.port}"
"NIX_AUTODEPLOY_DEPLOY_BIN=${lib.getExe deploy}"
"NIX_AUTODEPLOY_ENV_FILE=${cfg.environmentFile}"
"NIX_AUTODEPLOY_REPOS_FILE=${repoFile}"
];
Restart = "always";
RestartSec = 2;
};
Install.WantedBy = [ "default.target" ];
};
};
}
@@ -1,104 +0,0 @@
"""Forge webhook listener that turns a push into a flake-input bump.
Verifies the HMAC a Gitea (or GitHub) webhook signs the body with, then hands
the actual work to a transient systemd unit. Nothing is done in-process: the
deploy runs `nh home switch`, which restarts every unit home-manager owns —
including this listener — so the job has to outlive it.
"""
import hashlib
import hmac
import json
import os
import subprocess
import sys
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
SECRET = os.environ.get("NIX_AUTODEPLOY_SECRET", "").encode()
PORT = int(os.environ.get("NIX_AUTODEPLOY_PORT", "7375"))
DEPLOY = os.environ["NIX_AUTODEPLOY_DEPLOY_BIN"]
# {"<owner>/<repo>": {"input": "rev", "apply": true}, ...}. Passed as a file,
# not a variable: systemd's Environment= strips the quotes out of inline JSON.
with open(os.environ["NIX_AUTODEPLOY_REPOS_FILE"]) as fh:
REPOS = json.load(fh)
ENV_FILE = os.environ.get("NIX_AUTODEPLOY_ENV_FILE", "")
MAX_BODY = 1 << 20
if not SECRET:
sys.exit("NIX_AUTODEPLOY_SECRET is not set")
def signature_ok(body: bytes, headers) -> bool:
expected = hmac.new(SECRET, body, hashlib.sha256).hexdigest()
# Gitea sends the bare hex digest; GitHub prefixes it with "sha256=".
for name in ("X-Gitea-Signature", "X-Hub-Signature-256"):
got = headers.get(name)
if got and hmac.compare_digest(got.removeprefix("sha256="), expected):
return True
return False
def spawn(repo: str, entry: dict) -> None:
cmd = [
"systemd-run",
"--user",
"--collect",
f"--description=nix-autodeploy: {repo}",
# PATH is not inherited by a transient unit, and the deploy shells out
# to git, nix and the gitea credential helper.
f"--setenv=PATH={os.environ['PATH']}",
]
if ENV_FILE:
cmd.append(f"--property=EnvironmentFile={ENV_FILE}")
cmd += [DEPLOY, entry["input"], "apply" if entry.get("apply", True) else "notify"]
subprocess.run(cmd, check=True)
class Handler(BaseHTTPRequestHandler):
def reply(self, code: int, text: str) -> None:
payload = text.encode()
self.send_response(code)
self.send_header("Content-Type", "text/plain")
self.send_header("Content-Length", str(len(payload)))
self.end_headers()
self.wfile.write(payload)
def do_GET(self) -> None: # noqa: N802 - BaseHTTPRequestHandler's spelling
if self.path == "/health":
self.reply(200, "ok\n")
else:
self.reply(404, "no\n")
def do_POST(self) -> None: # noqa: N802
length = int(self.headers.get("Content-Length", "0"))
if length > MAX_BODY:
return self.reply(413, "body too large\n")
body = self.rfile.read(length)
if not signature_ok(body, self.headers):
return self.reply(401, "bad signature\n")
try:
event = json.loads(body)
except json.JSONDecodeError:
return self.reply(400, "bad json\n")
ref = event.get("ref")
repo = (event.get("repository") or {}).get("full_name")
if ref != "refs/heads/main":
return self.reply(200, f"ignored ref {ref}\n")
entry = REPOS.get(repo)
if entry is None:
return self.reply(200, f"ignored repo {repo}\n")
spawn(repo, entry)
self.reply(202, f"deploying {entry['input']}\n")
def log_message(self, fmt: str, *args) -> None:
# Journal already timestamps; the default format prepends its own.
sys.stderr.write(f"{self.address_string()} {fmt % args}\n")
if __name__ == "__main__":
ThreadingHTTPServer(("0.0.0.0", PORT), Handler).serve_forever()
+1
View File
@@ -0,0 +1 @@
Screenshots: stored in ~/downloads/screenshots, with date time in the filename
@@ -0,0 +1,100 @@
---
description: Merge worktree changes and clean up safely
---
# Merge Worktree and Cleanup
Merge changes from the current worktree into a target branch and clean up the worktree.
**Usage:** `/merge [target-branch]`
**Arguments:**
- `$1`: Target branch to merge into (default: auto-detect main/master)
## Process
### 1. Pre-flight Checks
```bash
CURRENT_BRANCH=$(git branch --show-current)
WORKTREE_DIR=$(git rev-parse --git-common-dir)
if [[ ! "$WORKTREE_DIR" == *".git/worktrees"* ]]; then
echo "❌ Error: You're not in a worktree"
exit 1
fi
if ! git diff-index --quiet HEAD --; then
echo "❌ Error: You have uncommitted changes"
git status --short
exit 1
fi
```
### 2. Determine Target Branch
```bash
TARGET_BRANCH="${1:-}"
if [ -z "$TARGET_BRANCH" ]; then
if git show-ref --verify --quiet refs/heads/main; then
TARGET_BRANCH="main"
elif git show-ref --verify --quiet refs/heads/master; then
TARGET_BRANCH="master"
else
echo "❌ Error: Cannot auto-detect main branch"
exit 1
fi
fi
```
### 3. Update Target Branch
```bash
WORKTREE_PATH=$(pwd)
COMMON_DIR=$(git rev-parse --git-common-dir)
REPO_ROOT=$(echo "$COMMON_DIR" | sed 's/\.git.*//' | sed 's/\/$//')
if [ "$COMMON_DIR" = ".git" ]; then
REPO_ROOT=$(git rev-parse --show-toplevel)
fi
cd "$REPO_ROOT"
git checkout "$TARGET_BRANCH"
git pull origin "$TARGET_BRANCH"
```
### 4. Merge
```bash
git merge --no-ff "$CURRENT_BRANCH" -m "Merge branch '$CURRENT_BRANCH'"
```
If you prefer a squash merge, do this instead:
```bash
git merge --squash "$CURRENT_BRANCH"
git commit -m "feat: <summary>"
```
### 5. Push and Clean Up
```bash
git push origin "$TARGET_BRANCH"
git worktree remove "$WORKTREE_PATH" --force || rm -rf "$WORKTREE_PATH"
git branch -d "$CURRENT_BRANCH" || git branch -D "$CURRENT_BRANCH"
```
### 6. Optional: Delete Remote Branch
```bash
git push origin --delete "$CURRENT_BRANCH"
```
## Language-specific checks
### Rust
- `cargo check`
- `cargo clippy` (clean up new warnings)
### TypeScript
- `tsc`
@@ -0,0 +1,9 @@
---
description: Rebase/merge upstream changes into the current branch
---
This command should only work while in a secondary branch (not main or master).
Commit any unstaged changes
Fetch updates from the origin repo
If there are new commits on the parent branch (typically origin/main), then merge them back into the current branch
Solve any conflicts, and analyze the incoming changes to see if additional changes are required to the branch's code (e.g.: if something was renamed in the meantime in main, our new code may need to be adjusted)
+6 -5
View File
@@ -1,13 +1,12 @@
_:
{
# Global rules (opencode/AGENTS.md) and the shared commands (merge, update)
# come from the agent-skills home-manager module — opencode has no @file
# imports, so the module concatenates the shared fragments into one file.
# opencode also auto-loads skills from ~/.claude/skills and ~/.agents/skills.
xdg.configFile = {
# Global config file
"opencode/opencode.json".source = ./opencode.json;
# Global rules (equivalent to ~/.claude/CLAUDE.md)
"opencode/AGENTS.md".source = ./AGENTS.md;
# Custom agents
"opencode/agents/oracle.md".source = ./agents/oracle.md;
"opencode/agents/explorer.md".source = ./agents/explorer.md;
@@ -17,8 +16,10 @@ _:
"opencode/agents/designer-bold.md".source = ./agents/designer-bold.md;
"opencode/agents/analyze-branch.md".source = ./agents/analyze-branch.md;
# Global commands (opencode-specific ones only)
# Global commands
"opencode/commands/work.md".source = ./commands/work.md;
"opencode/commands/merge.md".source = ./commands/merge.md;
"opencode/commands/update.md".source = ./commands/update.md;
"opencode/commands/smart-debug.md".source = ./commands/smart-debug.md;
"opencode/commands/tdd-cycle.md".source = ./commands/tdd-cycle.md;
"opencode/commands/security-scan.md".source = ./commands/security-scan.md;
-12
View File
@@ -1,12 +0,0 @@
{ pkgs, ... }:
let
pi = pkgs.callPackage ../../../pkgs/pi/package.nix { };
in
{
# ~/.pi/agent/AGENTS.md comes from the agent-skills home-manager module
# (pi has no @file imports, so the module concatenates the shared fragments).
# Pi reads skills straight from ~/.agents/skills, which the module links.
# Settings stay unmanaged: pi writes ~/.pi/agent/settings.json itself from
# /settings, /trust and `pi config`.
home.packages = [ pi ];
}
-7
View File
@@ -1,7 +0,0 @@
{ inputs, ... }:
# rev: the always-on code review server. Module only — no `enable` here. It
# discovers every repo under its roots and serves them without auth, so only a
# box that is already a trusted single-user machine should run it.
{
imports = [ inputs.rev.homeManagerModules.default ];
}
+16
View File
@@ -0,0 +1,16 @@
{
config,
pkgs,
lib,
...
}:
let
# T3 Code: open-source desktop control plane for coding agents (x86_64 only).
# Shares the custom.aiApps.deviceScaleFactor knob (defined in ../claude).
t3-code = pkgs.callPackage ../../../pkgs/t3-code/package.nix {
inherit (config.custom.aiApps) deviceScaleFactor;
};
in
{
home.packages = lib.optional pkgs.stdenv.hostPlatform.isx86_64 t3-code;
}
-1
View File
@@ -30,7 +30,6 @@ set -ag terminal-overrides ",xterm*:Tc:smcup@:rmcup@"
# Forward Shift+Enter and other modified keys through to applications
set -s extended-keys on
set -s extended-keys-format csi-u
set -as terminal-features "tmux-256color:extkeys"
# -------------------------------------------------------------------
-20
View File
@@ -1,20 +0,0 @@
{ pkgs, ... }:
{
imports = [ ./desktop/spicetify.nix ];
home.packages = with pkgs; [
obsidian
obs-studio
kooha
vokoscreen-ng
gimp
font-manager
xournalpp
remmina
slack
signal-desktop
zoom-us
yaak
bun
];
}
+1 -12
View File
@@ -10,18 +10,7 @@
enableCompletion = true;
autosuggestion.enable = true;
syntaxHighlighting.enable = true;
# A full compinit security-checks and rebuilds the dump on every shell
# start. Do that at most once a day and use the cached dump (-C) in
# between. Missing dump globs to nothing, so it takes the full path.
completionInit = ''
autoload -U compinit
_zcompdump_fresh() {
setopt local_options extendedglob
[[ -n ''${ZDOTDIR:-$HOME}/.zcompdump(#qNmh-24) ]]
}
if _zcompdump_fresh; then compinit -C; else compinit -u; fi
unfunction _zcompdump_fresh
'';
completionInit = "autoload -U compinit && compinit -u";
shellAliases = {
c = "cargo";
+9 -21
View File
@@ -5,10 +5,6 @@
imports = [
../common/programs/default.nix
../common/programs/desktop
../common/programs/workstation-apps.nix
../common/programs/editors.nix
../common/programs/dev-workstation.nix
../common/programs/ai-gui.nix
../common/programs/zen-browser.nix
../common/programs/hyprland
../common/programs/kitty
@@ -24,31 +20,15 @@
home.sessionVariables = {
LIBVA_DRIVER_NAME = "nvidia";
# Was GDM_BACKEND, which nothing reads (SDDM is the DM here).
GBM_BACKEND = "nvidia-drm";
GDM_BACKEND = "nvidia-drm";
__GLX_VENDOR_LIBRARY_NAME = "nvidia";
# Firefox/Zen decode video in a separate RDD process whose sandbox denies
# /dev/nvidia*, so nvidia-vaapi-driver never initialises there and playback
# silently drops to software. Costs one sandbox layer; no narrower switch
# exists. Check with `nvidia-smi --query-gpu=utilization.decoder`.
MOZ_DISABLE_RDD_SANDBOX = "1";
};
# Plain `nvdec` keeps frames in GPU memory and breaks some filters.
custom.mpv.hwdec = "nvdec-copy";
# The 4060 would render Cycles far faster, but nothing caches this build —
# flipping it on compiles Blender, OpenUSD, OpenSubdiv and OpenImageDenoise.
custom.blender.cuda = false;
custom.hyprland = {
yaziSize = "2400 1800";
cursorSize = 42;
# The screen actually shared in every call; skips the xdph picker entirely.
shareOutput = "HDMI-A-1";
verticalOutputs = [ "DP-2" ];
panelScale = 1.6;
};
custom.gaming = {
@@ -67,6 +47,7 @@
-- session the moment Hyprland starts so a cold boot lands on hyprlock, not
-- an open desktop. A brief flash before it paints is possible.
hl.exec_cmd("hyprlock")
hl.exec_cmd("$HOME/.local/bin/hyprpaper-rotate")
end)
-- ethui-dev
@@ -129,5 +110,12 @@
hl.config({ render = { direct_scanout = false } })
'';
programs.noctalia.settings = {
shell.ui_scale = 1.25;
notification.monitors = [ "DP-1" ];
# NOTE: v5 has no per-monitor wallpaper directories; only directory_light /
# directory_dark. Re-add via Settings UI if upstream gains support.
};
programs.kitty.settings.font_size = 16;
}
+141
View File
@@ -0,0 +1,141 @@
default_profile = "default"
[acp]
allow_agent_install = false
allowed_agents = []
auto_stop_idle_secs = 0
compaction_reminder = false
compaction_reminder_percent = 75
default_agent = "claude"
max_concurrent_workers = 5
node_path = ""
offer_structured_in_new_session = false
rate_limit_auto_resume = false
replay_events = 0
restrict_agents = false
show_tool_durations = true
silent_orphan_grace_secs = 120
[auth]
persist_sessions = true
[diff]
context_lines = 3
split_view = false
[hooks]
[host_hooks]
[logging]
default_level = "info"
file_path = "debug.log"
keep_count = 5
max_size_mib = 50
output = "file"
rotation = "size"
show_spans = false
[logging.targets]
[sandbox]
auto_cleanup = true
container_runtime = "docker"
default_image = "ghcr.io/agent-of-empires/aoe-sandbox:latest"
default_terminal_mode = "host"
enabled_by_default = false
environment = [
"TERM",
"COLORTERM",
"FORCE_COLOR",
"NO_COLOR",
]
extra_volumes = []
mount_ssh = false
selinux_relabel = false
volume_ignores = []
volume_ignores_strategy = "anonymous"
[session]
agent_status_hooks = true
auto_resume_on_restart = true
auto_stop_idle_secs = 0
click_action = "live_send"
confirm_before_quit = false
confirm_delete = false
conversation_summary = false
default_attach_mode = "tmux"
delete_to_trash = false
favorites_first = true
inherit_host_environment = false
live_send_exit_chord = "C-q"
live_send_leader = "C-b"
live_send_on_view_switch = false
merge_hooks_into_selected_agent = true
mouse_capture = true
opencode_preassign_session_id = false
prevent_sleep_idle_grace_minutes = 15
prevent_sleep_when_active = false
restart_wake_message = "wake up: pick up what you were doing"
row_tag = "none"
show_session_colors = true
show_tips = false
smart_rename = true
smart_rename_agent = ""
snooze_duration_minutes = 30
strict_hotkeys = false
tie_workdir_to_name = true
trash_retention_days = 30
unread_indicator = true
yolo_mode_default = true
[session.agent_detect_as]
kimiclaude = "claude"
synclaude = "claude"
[session.custom_agents]
kimiclaude = "kimiclaude"
synclaude = "synclaude"
[skills]
auto_propagate = false
[sound]
enabled = false
[status_hooks]
enabled = false
[telemetry]
enabled = false
[theme]
color_mode = "truecolor"
idle_decay_minutes = 0
name = "zinc"
[tmux]
clipboard = "auto"
mouse = "auto"
status_bar = "auto"
vt_live = true
[updates]
auto_update_plugins = false
update_check_mode = "notify"
[web]
notifications_enabled = true
notify_on_error = true
notify_on_idle = false
notify_on_waiting = true
notify_on_wake_fire = true
[worktree]
auto_cleanup = true
bare_repo_path_template = "./{branch}"
delete_branch_on_cleanup = false
enabled = true
init_submodules = true
path_template = "../{repo-name}-worktrees/{branch}"
workspace_path_template = "../{branch}-workspace-{session-id}"
+247
View File
@@ -0,0 +1,247 @@
{
"permissions": {
"allow": [
"Bash",
"Edit",
"Write",
"NotebookEdit",
"WebFetch",
"WebSearch"
],
"deny": [
"Read(~/.ssh/**)",
"Read(*.pem)",
"Read(*.key)",
"mcp__dokploy__application-one",
"mcp__dokploy__mounts-listByServiceId",
"mcp__dokploy__mounts-one",
"mcp__dokploy__compose-one",
"mcp__dokploy__schedule-create",
"mcp__dokploy__schedule-update",
"mcp__dokploy__schedule-runManually"
],
"defaultMode": "auto"
},
"hooks": {
"PostToolUse": [
{
"matcher": "Write|Edit|MultiEdit",
"hooks": [
{
"type": "command",
"command": "~/.claude/hooks/comment-lint.py"
}
]
},
{
"matcher": "AskUserQuestion",
"hooks": [
{
"type": "command",
"command": "sh -c 'unset IFS; set -f; umask 077; [ -n \"$AOE_INSTANCE_ID\" ] || exit 0; case \"$AOE_INSTANCE_ID\" in *[!0-9a-zA-Z_-]*) exit 0 ;; esac; B=/tmp/aoe-hooks-1000; mkdir -p \"$B\" 2>/dev/null || exit 0; LS=$(LC_ALL=C ls -ldn \"$B\" 2>/dev/null) || exit 0; set -- $LS; M=\"$1\"; case \"$M\" in drwx------|drwx------.|drwx------+|drwx------@) ;; *) exit 0 ;; esac; ME=$(id -u 2>/dev/null) || exit 0; [ \"$3\" = \"$ME\" ] || exit 0; D=\"$B/$AOE_INSTANCE_ID\"; mkdir -p \"$D\" 2>/dev/null; LS=$(LC_ALL=C ls -ldn \"$D\" 2>/dev/null) || exit 0; set -- $LS; M=\"$1\"; case \"$M\" in drwx------|drwx------.|drwx------+|drwx------@) ;; *) exit 0 ;; esac; [ \"$3\" = \"$ME\" ] || exit 0; printf running > \"$D/status\" 2>/dev/null; exit 0 # aoe-hooks'"
}
]
}
],
"Notification": [
{
"hooks": [
{
"type": "command",
"command": "~/.claude/hooks/tmux-attention.sh"
}
]
},
{
"matcher": "permission_prompt|elicitation_dialog|agent_needs_input",
"hooks": [
{
"type": "command",
"command": "sh -c 'unset IFS; set -f; umask 077; [ -n \"$AOE_INSTANCE_ID\" ] || exit 0; case \"$AOE_INSTANCE_ID\" in *[!0-9a-zA-Z_-]*) exit 0 ;; esac; B=/tmp/aoe-hooks-1000; mkdir -p \"$B\" 2>/dev/null || exit 0; LS=$(LC_ALL=C ls -ldn \"$B\" 2>/dev/null) || exit 0; set -- $LS; M=\"$1\"; case \"$M\" in drwx------|drwx------.|drwx------+|drwx------@) ;; *) exit 0 ;; esac; ME=$(id -u 2>/dev/null) || exit 0; [ \"$3\" = \"$ME\" ] || exit 0; D=\"$B/$AOE_INSTANCE_ID\"; mkdir -p \"$D\" 2>/dev/null; LS=$(LC_ALL=C ls -ldn \"$D\" 2>/dev/null) || exit 0; set -- $LS; M=\"$1\"; case \"$M\" in drwx------|drwx------.|drwx------+|drwx------@) ;; *) exit 0 ;; esac; [ \"$3\" = \"$ME\" ] || exit 0; printf waiting > \"$D/status\" 2>/dev/null; exit 0 # aoe-hooks'"
}
]
},
{
"matcher": "idle_prompt|agent_completed",
"hooks": [
{
"type": "command",
"command": "sh -c 'unset IFS; set -f; umask 077; [ -n \"$AOE_INSTANCE_ID\" ] || exit 0; case \"$AOE_INSTANCE_ID\" in *[!0-9a-zA-Z_-]*) exit 0 ;; esac; B=/tmp/aoe-hooks-1000; mkdir -p \"$B\" 2>/dev/null || exit 0; LS=$(LC_ALL=C ls -ldn \"$B\" 2>/dev/null) || exit 0; set -- $LS; M=\"$1\"; case \"$M\" in drwx------|drwx------.|drwx------+|drwx------@) ;; *) exit 0 ;; esac; ME=$(id -u 2>/dev/null) || exit 0; [ \"$3\" = \"$ME\" ] || exit 0; D=\"$B/$AOE_INSTANCE_ID\"; mkdir -p \"$D\" 2>/dev/null; LS=$(LC_ALL=C ls -ldn \"$D\" 2>/dev/null) || exit 0; set -- $LS; M=\"$1\"; case \"$M\" in drwx------|drwx------.|drwx------+|drwx------@) ;; *) exit 0 ;; esac; [ \"$3\" = \"$ME\" ] || exit 0; printf idle > \"$D/status\" 2>/dev/null; exit 0 # aoe-hooks'"
}
]
}
],
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": "~/.claude/hooks/comms-lint.py"
}
]
},
{
"matcher": "",
"hooks": [
{
"type": "command",
"command": "~/.claude/hooks/tmux-reset.sh"
}
]
},
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": "rtk hook claude"
}
]
},
{
"hooks": [
{
"type": "command",
"command": "sh -c 'unset IFS; set -f; umask 077; [ -n \"$AOE_INSTANCE_ID\" ] || exit 0; case \"$AOE_INSTANCE_ID\" in *[!0-9a-zA-Z_-]*) exit 0 ;; esac; B=/tmp/aoe-hooks-1000; mkdir -p \"$B\" 2>/dev/null || exit 0; LS=$(LC_ALL=C ls -ldn \"$B\" 2>/dev/null) || exit 0; set -- $LS; M=\"$1\"; case \"$M\" in drwx------|drwx------.|drwx------+|drwx------@) ;; *) exit 0 ;; esac; ME=$(id -u 2>/dev/null) || exit 0; [ \"$3\" = \"$ME\" ] || exit 0; D=\"$B/$AOE_INSTANCE_ID\"; mkdir -p \"$D\" 2>/dev/null; LS=$(LC_ALL=C ls -ldn \"$D\" 2>/dev/null) || exit 0; set -- $LS; M=\"$1\"; case \"$M\" in drwx------|drwx------.|drwx------+|drwx------@) ;; *) exit 0 ;; esac; [ \"$3\" = \"$ME\" ] || exit 0; IN=$(cat 2>/dev/null); S=running; case \"$IN\" in *\\\"tool_name\\\":\\\"AskUserQuestion\\\"*) S=waiting ;; esac; printf %s \"$S\" > \"$D/status\" 2>/dev/null; exit 0 # aoe-hooks'"
}
]
}
],
"SessionStart": [
{
"matcher": "startup|resume|clear",
"hooks": [
{
"type": "command",
"command": "~/.claude/hooks/git-autoupdate.sh"
}
]
},
{
"hooks": [
{
"type": "command",
"command": "python3 ~/.claude/scripts/aoe-register-remote.py"
}
]
},
{
"hooks": [
{
"type": "command",
"command": "/home/naps62/tea/rev/scripts/rev-hook-session-start.sh"
}
]
},
{
"hooks": [
{
"type": "command",
"command": "sh -c '[ -n \"$AOE_INSTANCE_ID\" ] || exit 0; command -v aoe >/dev/null 2>&1 || exit 0; aoe __extract-session-id 2>/dev/null; exit 0 # aoe-hooks'"
}
]
}
],
"Stop": [
{
"hooks": [
{
"type": "command",
"command": "/home/naps62/tea/rev/scripts/rev-hook-stop.sh"
}
]
},
{
"hooks": [
{
"type": "command",
"command": "sh -c 'unset IFS; set -f; umask 077; [ -n \"$AOE_INSTANCE_ID\" ] || exit 0; case \"$AOE_INSTANCE_ID\" in *[!0-9a-zA-Z_-]*) exit 0 ;; esac; B=/tmp/aoe-hooks-1000; mkdir -p \"$B\" 2>/dev/null || exit 0; LS=$(LC_ALL=C ls -ldn \"$B\" 2>/dev/null) || exit 0; set -- $LS; M=\"$1\"; case \"$M\" in drwx------|drwx------.|drwx------+|drwx------@) ;; *) exit 0 ;; esac; ME=$(id -u 2>/dev/null) || exit 0; [ \"$3\" = \"$ME\" ] || exit 0; D=\"$B/$AOE_INSTANCE_ID\"; mkdir -p \"$D\" 2>/dev/null; LS=$(LC_ALL=C ls -ldn \"$D\" 2>/dev/null) || exit 0; set -- $LS; M=\"$1\"; case \"$M\" in drwx------|drwx------.|drwx------+|drwx------@) ;; *) exit 0 ;; esac; [ \"$3\" = \"$ME\" ] || exit 0; printf idle > \"$D/status\" 2>/dev/null; exit 0 # aoe-hooks'"
}
]
}
],
"UserPromptSubmit": [
{
"hooks": [
{
"type": "command",
"command": "sh -c '[ -n \"$AOE_INSTANCE_ID\" ] || exit 0; command -v aoe >/dev/null 2>&1 || exit 0; aoe __extract-session-id 2>/dev/null; exit 0 # aoe-hooks'"
},
{
"type": "command",
"command": "sh -c 'unset IFS; set -f; umask 077; [ -n \"$AOE_INSTANCE_ID\" ] || exit 0; case \"$AOE_INSTANCE_ID\" in *[!0-9a-zA-Z_-]*) exit 0 ;; esac; B=/tmp/aoe-hooks-1000; mkdir -p \"$B\" 2>/dev/null || exit 0; LS=$(LC_ALL=C ls -ldn \"$B\" 2>/dev/null) || exit 0; set -- $LS; M=\"$1\"; case \"$M\" in drwx------|drwx------.|drwx------+|drwx------@) ;; *) exit 0 ;; esac; ME=$(id -u 2>/dev/null) || exit 0; [ \"$3\" = \"$ME\" ] || exit 0; D=\"$B/$AOE_INSTANCE_ID\"; mkdir -p \"$D\" 2>/dev/null; LS=$(LC_ALL=C ls -ldn \"$D\" 2>/dev/null) || exit 0; set -- $LS; M=\"$1\"; case \"$M\" in drwx------|drwx------.|drwx------+|drwx------@) ;; *) exit 0 ;; esac; [ \"$3\" = \"$ME\" ] || exit 0; printf running > \"$D/status\" 2>/dev/null; exit 0 # aoe-hooks'"
}
]
}
],
"StopFailure": [
{
"hooks": [
{
"type": "command",
"command": "sh -c 'unset IFS; set -f; umask 077; [ -n \"$AOE_INSTANCE_ID\" ] || exit 0; case \"$AOE_INSTANCE_ID\" in *[!0-9a-zA-Z_-]*) exit 0 ;; esac; B=/tmp/aoe-hooks-1000; mkdir -p \"$B\" 2>/dev/null || exit 0; LS=$(LC_ALL=C ls -ldn \"$B\" 2>/dev/null) || exit 0; set -- $LS; M=\"$1\"; case \"$M\" in drwx------|drwx------.|drwx------+|drwx------@) ;; *) exit 0 ;; esac; ME=$(id -u 2>/dev/null) || exit 0; [ \"$3\" = \"$ME\" ] || exit 0; D=\"$B/$AOE_INSTANCE_ID\"; mkdir -p \"$D\" 2>/dev/null; LS=$(LC_ALL=C ls -ldn \"$D\" 2>/dev/null) || exit 0; set -- $LS; M=\"$1\"; case \"$M\" in drwx------|drwx------.|drwx------+|drwx------@) ;; *) exit 0 ;; esac; [ \"$3\" = \"$ME\" ] || exit 0; printf idle > \"$D/status\" 2>/dev/null; exit 0 # aoe-hooks'"
}
]
}
],
"ElicitationResult": [
{
"hooks": [
{
"type": "command",
"command": "sh -c 'unset IFS; set -f; umask 077; [ -n \"$AOE_INSTANCE_ID\" ] || exit 0; case \"$AOE_INSTANCE_ID\" in *[!0-9a-zA-Z_-]*) exit 0 ;; esac; B=/tmp/aoe-hooks-1000; mkdir -p \"$B\" 2>/dev/null || exit 0; LS=$(LC_ALL=C ls -ldn \"$B\" 2>/dev/null) || exit 0; set -- $LS; M=\"$1\"; case \"$M\" in drwx------|drwx------.|drwx------+|drwx------@) ;; *) exit 0 ;; esac; ME=$(id -u 2>/dev/null) || exit 0; [ \"$3\" = \"$ME\" ] || exit 0; D=\"$B/$AOE_INSTANCE_ID\"; mkdir -p \"$D\" 2>/dev/null; LS=$(LC_ALL=C ls -ldn \"$D\" 2>/dev/null) || exit 0; set -- $LS; M=\"$1\"; case \"$M\" in drwx------|drwx------.|drwx------+|drwx------@) ;; *) exit 0 ;; esac; [ \"$3\" = \"$ME\" ] || exit 0; printf running > \"$D/status\" 2>/dev/null; exit 0 # aoe-hooks'"
}
]
}
]
},
"worktree": {
"baseRef": "fresh"
},
"enabledPlugins": {
"building@subvisual": true,
"rust-analyzer-lsp@claude-plugins-official": false,
"caveman@caveman": true
},
"extraKnownMarketplaces": {
"subvisual": {
"source": {
"source": "github",
"repo": "subvisual/harness"
}
},
"superpowers-marketplace": {
"source": {
"source": "github",
"repo": "obra/superpowers-marketplace"
}
},
"caveman": {
"source": {
"source": "github",
"repo": "JuliusBrussee/caveman"
}
}
},
"skipDangerousModePermissionPrompt": true,
"theme": "dark",
"editorMode": "vim",
"preferredNotifChannel": "auto",
"autoCompactEnabled": true,
"remoteControlAtStartup": false,
"inputNeededNotifEnabled": true,
"mcpServers": {
"playwright": {
"args": [
"-y",
"@playwright/mcp@latest"
],
"command": "npx",
"type": "stdio"
}
},
"sandbox": {
"enabled": false
}
}
+31 -24
View File
@@ -4,12 +4,14 @@
...
}:
let
# ./opencode.json holds only yolo's overrides; everything else is inherited
# so agents, commands and skills keep coming from common. Attrsets merge
# key-by-key, lists are replaced whole.
mergedOpencodeConfig = (pkgs.formats.json { }).generate "opencode.json" (
lib.recursiveUpdate (lib.importJSON ../common/programs/opencode/opencode.json) (
lib.importJSON ./opencode.json
claudeSettings = "home/yolo/claude-settings.json";
# ./claude-settings.json holds only what yolo overrides; everything else is
# inherited so common changes reach this host. Attrsets merge key-by-key,
# lists are replaced whole (permissions.allow is yolo's, not a union).
mergedClaudeSettings = (pkgs.formats.json { }).generate "claude-settings.json" (
lib.recursiveUpdate (lib.importJSON ../common/programs/claude/settings.json) (
lib.importJSON ./claude-settings.json
)
);
in
@@ -21,20 +23,12 @@ in
../common/programs/kitty
../common/programs/gpg.nix
../common/programs/aoe
../common/programs/maestro
../common/programs/rev
../common/programs/nix-autodeploy
../common/features/xdg.nix
./monitors.nix
./services.nix
./ssh.nix
];
# Host-local: bash goes from "ask" to "allow" so unattended opencode sessions
# stop stalling on every git and grep.
# It also drops the prompt on branches under review, which is the tradeoff.
xdg.configFile."opencode/opencode.json".source = lib.mkForce mergedOpencodeConfig;
custom.hyprland.cursorSize = 32;
# Amber, and a different silhouette to Nordzy — this desktop is only ever seen
@@ -46,18 +40,31 @@ in
light = "Bibata-Modern-Amber";
};
# Headless browser driver the agent tooling shells out to. Was a global npm
# install on the ubuntu box.
home.packages = [ pkgs.agent-browser ];
home = {
# Headless browser driver the agent tooling shells out to. Was a global npm
# install on the ubuntu box.
packages = [ pkgs.agent-browser ];
# This is the one box that runs the agent-skills units; each starts a session,
# so a second machine enabling them would run the same job twice.
programs.agentSkills = {
machine = "yolo";
prDaemon.enable = true;
hourlog.enable = true;
weekReview.enable = true;
# Both default to ~/projects/nixos-config in common/programs; this clone
# lives under ~/tea. nh.flake sets NH_FLAKE, so without it `nh home switch`
# with no argument resolves to a path that does not exist.
mutableFilesRepoPath = "/home/naps62/tea/nixos-config";
mutableFiles = {
# Host-local, not shared: this sets yolo_mode_default = true, which starts
# aoe sessions with permission checks skipped. Only correct on this box.
".config/agent-of-empires/config.toml".source = ./aoe-config.toml;
# Likewise host-local: carries skipDangerousModePermissionPrompt and the
# rev hook paths, neither of which belong on a workstation.
".claude/settings.json" = {
source = lib.mkForce mergedClaudeSettings;
upstreamPath = claudeSettings;
};
};
};
programs.agentSkills.machine = "yolo";
programs.nh.flake = "/home/naps62/tea/nixos-config";
# Idle lock and dpms-off blank the virtual output: Sunshine then captures a
-5
View File
@@ -1,5 +0,0 @@
{
"permission": {
"bash": "allow"
}
}
+112 -69
View File
@@ -3,11 +3,12 @@
inputs,
...
}:
# The user services this box exists to run.
# The user services this box exists to run, ported from hand-written units in
# ~/.config/systemd/user on the Ubuntu machine.
#
# maestro and rev come from their own flakes, so nix owns the build as well as
# the unit. aoe-web is still the odd one out: its unit is defined here and the
# binary comes from the flake input.
# NOT self-contained: every ExecStart under ~/.bun or ~/.local/bin is an
# imperatively-installed binary, and the WorkingDirectories are clones of
# separate repos. Nix owns the unit definitions here, nothing more.
let
# A user unit gets almost no PATH by default; these are the profile dirs the
# original units got for free from the system PATH on Ubuntu.
@@ -18,91 +19,133 @@ let
aoe = inputs.agent-of-empires.packages.${pkgs.system}.aoe-with-web;
in
{
# sem is here as well as on rev's unit: the shell uses it directly too.
home.packages = [
sem
# ACP adapter aoe's structured (web) sessions spawn as `claude-agent-acp`.
pkgs.claude-agent-acp
pkgs.bun
];
# maestro's and rev's units come from their flake modules, not from the
# hand-written set below. 8081, not 8080: aoe-web already has that port on
# this host. There is no auth layer, so 0.0.0.0 is only safe behind the LAN
# perimeter.
services.maestro = {
enable = true;
web.enable = true;
settings = {
daemon.bind_addr = "127.0.0.1:7070";
web = {
bind_addr = "0.0.0.0:8081";
daemon_url = "http://127.0.0.1:7070";
};
};
};
# Everything under ~, three levels deep — the worktrees live at
# ~/<area>/<repo>/worktrees/<name>. sem gives entity-level diffs; without it
# rev falls back to line diffs.
services.rev = {
enable = true;
roots = [ "%h" ];
depth = 3;
semBin = "${sem}/bin/sem";
};
# One endpoint for every repo this config pins. rev and agent-skills apply
# straight away; maestro only bumps the lock and notifies, because switching
# restarts the daemon that owns every interactive shell on this box.
services.nixAutodeploy = {
enable = true;
flake = "/home/naps62/tea/nixos-config";
environmentFile = "%h/.config/nix-autodeploy/env";
repos = {
"yolo/rev".input = "rev";
"yolo/agent-skills".input = "agent-skills";
"naps62/maestro" = {
input = "maestro";
apply = false;
};
};
};
systemd.user.services = {
aoe-web = {
rev = {
Unit = {
Description = "aoe serve Agent of Empires web dashboard";
Description = "rev always-on local code review server";
After = [ "network.target" ];
# MUST stay 0: at RestartSec=2 a fast-crashing aoe burns the default
# MUST stay 0: at RestartSec=2 a fast-crashing rev burns the default
# 5-starts-per-10s budget, and systemd parks the unit in `failed` until
# a manual `systemctl --user reset-failed`.
StartLimitIntervalSec = 0;
};
Service = {
Type = "simple";
WorkingDirectory = "%h";
# The fork removed dashboard auth entirely, so there is no --auth flag
# any more — the reverse proxy is the only access gate. --allowed-host
# is what makes the rebinding gate accept a hostname under a wildcard
# bind (an IP literal needs no flag).
ExecStart = "${aoe}/bin/aoe serve --host 0.0.0.0 --port 8080 --behind-proxy --allowed-host aoe.n62.casa";
# TMUX_TMPDIR keeps the daemon on the same tmux server the shell and
# TUI use, instead of a second one under /tmp (same bug pr-daemon had).
WorkingDirectory = "%h/tea/rev";
# nodejs_26, not pkgs.nodejs: rev's package.json sets engines >=26 and
# the nixpkgs default is 24.
ExecStart = "${pkgs.nodejs_26}/bin/node server/index.ts";
Environment = [
"NODE_ENV=production"
"REV_ROOTS=%h"
"REV_DEPTH=3"
"REV_SEM_BIN=${sem}/bin/sem"
"PATH=${toolPath}"
"TMUX_TMPDIR=%t"
];
Restart = "always";
RestartSec = 2;
# If this unit boots before any shell, the shared tmux server lands in
# its cgroup; the default control-group kill would take every session
# down on restart.
KillMode = "process";
};
Install.WantedBy = [ "default.target" ];
};
rev-deploy = {
Unit = {
Description = "rev-deploy Gitea webhook listener that deploys rev on push to main";
After = [ "network.target" ];
# Same restart-budget trap as `rev` above.
StartLimitIntervalSec = 0;
};
Service = {
Type = "simple";
WorkingDirectory = "%h/tea/rev";
ExecStart = "${pkgs.bun}/bin/bun scripts/deploy-webhook.ts";
EnvironmentFile = "%h/.config/rev/deploy.env";
Environment = [ "PATH=${toolPath}" ];
Restart = "always";
RestartSec = 2;
};
Install.WantedBy = [ "default.target" ];
};
aoe-web = {
Unit = {
Description = "aoe serve Agent of Empires web dashboard";
After = [ "network.target" ];
# Same restart-budget trap as `rev` above.
StartLimitIntervalSec = 0;
};
Service = {
Type = "simple";
WorkingDirectory = "%h";
# aoe refuses `--auth none` on a non-loopback bind unless --behind-proxy
# is set; --allowed-host is what makes the rebinding gate accept a
# hostname under a wildcard bind (an IP literal needs no flag).
ExecStart = "${aoe}/bin/aoe serve --host 0.0.0.0 --port 8080 --auth none --behind-proxy --allowed-host aoe.n62.casa";
Environment = [ "PATH=${toolPath}" ];
Restart = "always";
RestartSec = 2;
};
Install.WantedBy = [ "default.target" ];
};
hourlog = {
Unit = {
Description = "Start the Friday hour log in a tmux session";
Documentation = [ "https://git.naps.pt/yolo/agent-skills" ];
ConditionPathIsDirectory = "%h/tea/agent-skills";
};
Service = {
Type = "oneshot";
ExecStart = "%h/tea/agent-skills/bin/hourlog-session.sh";
Environment = [ "PATH=${toolPath}" ];
# This unit may be what starts the tmux server; the default cgroup kill
# would take it back down as soon as ExecStart returns.
KillMode = "process";
};
};
week-review = {
Unit = {
Description = "Start the weekly agent-skills review in a tmux session";
Documentation = [ "https://git.naps.pt/yolo/agent-skills" ];
ConditionPathIsDirectory = "%h/tea/agent-skills";
};
Service = {
Type = "oneshot";
ExecStart = "%h/tea/agent-skills/bin/week-review-session.sh";
Environment = [ "PATH=${toolPath}" ];
KillMode = "process";
};
};
};
systemd.user.timers = {
hourlog = {
Unit.Description = "Friday hour log, 18:00 Europe/Lisbon";
Timer = {
# Zone suffix pinned because the machine clock is UTC; keeps it at 18:00
# wall time across DST.
OnCalendar = "Fri 18:00 Europe/Lisbon";
Persistent = true;
AccuracySec = "1min";
};
Install.WantedBy = [ "timers.target" ];
};
week-review = {
Unit.Description = "Weekly agent-skills review, Fridays 17:00 Europe/Lisbon";
Timer = {
OnCalendar = "Fri 17:00 Europe/Lisbon";
Persistent = true;
AccuracySec = "1min";
};
Install.WantedBy = [ "timers.target" ];
};
};
}
# pr-daemon, hourlog and week-review are not here: their units ship with the
# scripts they run, in the agent-skills module. This host opts in with
# programs.agentSkills.*.enable in home/yolo/default.nix.
-6
View File
@@ -35,12 +35,6 @@
share = "media";
mountPoint = "/mnt/media";
}
{
# yolo VM's /home/naps62 (share defined in hosts/yolo/default.nix).
server = "10.7.10.2";
share = "home";
mountPoint = "/home/naps62/yolo";
}
];
boot.kernelParams = [
+5 -2
View File
@@ -1,4 +1,7 @@
{ pkgs, ... }:
let
sddm-noctalia-theme = pkgs.callPackage ../../../../pkgs/sddm-noctalia-theme/package.nix { };
in
{
services.xserver = {
enable = true;
@@ -12,7 +15,7 @@
services.displayManager.sddm = {
enable = true;
wayland.enable = true;
theme = "sddm-astronaut-theme";
theme = "sddm-noctalia";
package = pkgs.kdePackages.sddm;
extraPackages = with pkgs.kdePackages; [
qtmultimedia
@@ -23,6 +26,6 @@
};
environment.systemPackages = with pkgs; [
sddm-astronaut
sddm-noctalia-theme
];
}
@@ -10,13 +10,6 @@
portalPackage = inputs.hyprland.packages.${pkgs.stdenv.hostPlatform.system}.xdg-desktop-portal-hyprland;
};
# swayosd (the volume/brightness OSD, configured in home) writes
# /sys/class/backlight/*/brightness directly. The package ships a udev rule
# that chgrps those files to `video`; without both the rule and the group,
# brightness keys pop the OSD but change nothing.
services.udev.packages = [ pkgs.swayosd ];
users.users.naps62.extraGroups = [ "video" ];
# GTK/GNOME services still useful on Wayland
programs.dconf.enable = true;
-10
View File
@@ -26,10 +26,6 @@
networking.hostName = "konishi";
# Set here rather than in hardware-configuration.nix so it survives a
# nixos-generate-config regeneration; the list merges with that file's.
fileSystems."/".options = [ "noatime" ];
# NAS media share — reachable only over the wg-home VPN. Lazy automount, so it
# never blocks boot and (re)mounts on first access once the VPN is up.
custom.smbMounts = [
@@ -38,12 +34,6 @@
share = "media";
mountPoint = "/mnt/media";
}
{
# yolo VM's /home/naps62 (share defined in hosts/yolo/default.nix).
server = "10.7.10.2";
share = "home";
mountPoint = "/home/naps62/yolo";
}
];
# Arm Wake-on-LAN (magic packet) on the Intel igc NIC and re-apply it on every
-24
View File
@@ -58,30 +58,6 @@
# virtio drivers; the agent itself is a separate service.
qemuGuest.enable = true;
# Exposes /home/naps62 to arrakis/konishi (custom.smbMounts on both, mounted
# at ~/yolo). Guest auth — the home dir holds SSH keys and credentials, so
# the only guard is `hosts allow` limiting clients to the wireguard subnet
# (desktops connect from wg addresses, 10.10.*).
# Clients address this VM as 10.7.10.2; keep that DHCP lease reserved.
samba = {
enable = true;
openFirewall = true;
settings = {
global = {
"map to guest" = "bad user";
"hosts allow" = "10.10.0.0/16 127.0.0.1";
"hosts deny" = "ALL";
};
home = {
path = "/home/naps62";
"read only" = "no";
"guest ok" = "yes";
"force user" = "naps62";
"force group" = "users";
};
};
};
# No physical seat: Sunshine is a user service and cannot capture until a
# graphical session exists, so a cold boot must reach one unattended. No
# hyprlock on start (unlike konishi) — nobody could type the password in.
File diff suppressed because it is too large Load Diff
+8
View File
@@ -0,0 +1,8 @@
{
"name": "claude-agent-acp-env",
"version": "0.70.0",
"private": true,
"dependencies": {
"@agentclientprotocol/claude-agent-acp": "0.70.0"
}
}
+51
View File
@@ -0,0 +1,51 @@
{
lib,
buildNpmPackage,
makeWrapper,
nodejs_26,
}:
# The ACP adapter aoe spawns for its structured view (`aoe acp doctor` calls it
# "claude"). Upstream only publishes it to npm, and `npm install -g` cannot work
# here: npm's prefix is the read-only nodejs derivation in the store.
#
# The sources here are a one-dependency wrapper project, not upstream's repo.
# To bump: set the version in package.json, run
# npm install --package-lock-only --ignore-scripts
# nix run nixpkgs#prefetch-npm-deps -- package-lock.json
# and paste the hash into npmDepsHash.
buildNpmPackage {
pname = "claude-agent-acp";
version = "0.70.0";
src = lib.cleanSource ./.;
npmDepsHash = "sha256-+q0Va1VvyImKqyWyOBYVmSFDC8TpKViikdYhE1VwGAE=";
nodejs = nodejs_26;
dontNpmBuild = true;
npmFlags = [ "--ignore-scripts" ];
nativeBuildInputs = [ makeWrapper ];
installPhase = ''
runHook preInstall
mkdir -p $out/lib/claude-agent-acp
cp -r node_modules $out/lib/claude-agent-acp/
# The adapter's shebang is `env node`; aoe spawns it from a systemd unit
# whose PATH need not carry one.
makeWrapper ${nodejs_26}/bin/node $out/bin/claude-agent-acp \
--add-flags $out/lib/claude-agent-acp/node_modules/@agentclientprotocol/claude-agent-acp/dist/index.js
runHook postInstall
'';
meta = {
description = "Agent Client Protocol adapter for Claude Code, spawned by aoe's structured view";
homepage = "https://www.npmjs.com/package/@agentclientprotocol/claude-agent-acp";
license = lib.licenses.asl20;
mainProgram = "claude-agent-acp";
};
}
-72
View File
@@ -1,72 +0,0 @@
{
lib,
stdenvNoCC,
fetchurl,
autoPatchelfHook,
makeWrapper,
stdenv,
}:
let
version = "0.84.2";
# Prebuilt Bun executables from the release page. Bump version + hashes from
# the SHA256SUMS asset:
# https://github.com/earendil-works/pi/releases
sources = {
x86_64-linux = {
asset = "pi-linux-x64.tar.gz";
hash = "sha256-kG++eH/SJcSsYk/n69Wx1Vpg4PXH71F5XSMVZPnuHBM=";
};
aarch64-linux = {
asset = "pi-linux-arm64.tar.gz";
hash = "sha256-0VNy2p5LTF/vn9Fb7XbX9fFyDdOf583g7GLltlrWPvE=";
};
};
source =
sources.${stdenvNoCC.hostPlatform.system}
or (throw "pi: unsupported system ${stdenvNoCC.hostPlatform.system}");
in
stdenvNoCC.mkDerivation {
pname = "pi";
inherit version;
src = fetchurl {
url = "https://github.com/earendil-works/pi/releases/download/v${version}/${source.asset}";
inherit (source) hash;
};
nativeBuildInputs = [
autoPatchelfHook
makeWrapper
];
# libgcc, for the bundled native clipboard module.
buildInputs = [ (lib.getLib stdenv.cc.cc) ];
installPhase = ''
runHook preInstall
# The binary resolves its themes, bundled node_modules and wasm relative to
# itself, so the whole tree ships together and only `pi` gets a wrapper.
mkdir -p $out/share/pi
cp -r . $out/share/pi/
chmod +x $out/share/pi/pi
# Nix owns updates here; `pi update --self` would write into the store.
makeWrapper $out/share/pi/pi $out/bin/pi \
--set-default PI_SKIP_VERSION_CHECK 1
runHook postInstall
'';
meta = {
description = "Pi coding agent CLI";
homepage = "https://pi.dev";
changelog = "https://github.com/earendil-works/pi/releases/tag/v${version}";
license = lib.licenses.mit;
sourceProvenance = [ lib.sourceTypes.binaryNativeCode ];
platforms = lib.attrNames sources;
mainProgram = "pi";
};
}
+33
View File
@@ -0,0 +1,33 @@
{ pkgs, ... }:
pkgs.stdenv.mkDerivation {
pname = "sddm-noctalia-theme";
version = "1.0";
src = pkgs.fetchFromGitHub {
owner = "mahaveergurjar";
repo = "sddm";
rev = "noctalia";
sha256 = "sha256-e/gYI6znHXxlDCOVh4p265x3kO0nQUU897hCY1yEz88=";
};
installPhase = ''
mkdir -p $out/share/sddm/themes/sddm-noctalia
cp -r * $out/share/sddm/themes/sddm-noctalia/
# Patch for Qt6 SDDM greeter compatibility
# SDDM reads QtVersion from metadata.desktop to find the greeter binary;
# default is 5 (sddm-greeter), we need 6 (sddm-greeter-qt6)
substituteInPlace $out/share/sddm/themes/sddm-noctalia/metadata.desktop \
--replace-fail "Theme-Id=noctalia" "QtVersion=6
Theme-Id=noctalia"
'';
meta = with pkgs.lib; {
description = "Noctalia-themed SDDM login theme";
homepage = "https://github.com/mahaveergurjar/sddm/tree/noctalia";
license = licenses.gpl3;
platforms = platforms.linux;
};
}