Compare commits
221 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| ac6d8b2d11 | |||
| 67c0e61d1c | |||
| a210e7e69e | |||
| 035962c9cf | |||
| 711e9d07fa | |||
| 2eb03ebcc8 | |||
| 56e7da9008 | |||
| 1b7bbb3d2e | |||
| 9794578554 | |||
| f53b7dca42 | |||
| 4f0fcc41df | |||
| c22ddec79a | |||
| d872582f28 | |||
| d78f542e42 | |||
| 2dd60b29e1 | |||
| 1a32635d77 | |||
| c3deb1debc | |||
| f15d64018f | |||
| 0322a8d9de | |||
| 21b4f9e30e | |||
| 12105a75d6 | |||
| 108814238a | |||
| b163785d2a | |||
| dd263b3e9f | |||
| 4c16212e0a | |||
| 5ce2aefc67 | |||
| d80b481e1a | |||
| b2f5625a63 | |||
| 2431722fe7 | |||
| d4ee1c66a8 | |||
| 826c4b4058 | |||
| 6ede2df401 | |||
| 0556b58c79 | |||
| 47c0732624 | |||
| e14bdcf3d1 | |||
| c56ef62cf4 | |||
| 914753fb55 | |||
| 2df744b244 | |||
| c4db021783 | |||
| 0a26367881 | |||
| 01de255a7a | |||
| a5661de579 | |||
| 1079822f67 | |||
| eb3eb0fe40 | |||
| 9bd137c31f | |||
| 4088e0f72c | |||
| 79a8cae2e7 | |||
| 0e6109df93 | |||
| 954d23cce6 | |||
| 6f3e4fb462 | |||
| 35f0145e22 | |||
| b21b25fe7b | |||
| a50eaa4707 | |||
| d74f1ac39e | |||
| ccc5d0d88c | |||
| ca1cba782e | |||
| 434ed3729f | |||
| df9fb4c495 | |||
| 4b12fd94db | |||
| aa7a9b179d | |||
| 08db70518b | |||
| 09bd517439 | |||
| 2cfdc7e68e | |||
| 5bafa07427 | |||
| f8c95e3b8d | |||
| 00810b4104 | |||
| ede94f9251 | |||
| ca0ad8f662 | |||
| 0519030103 | |||
| 4cb6e63e55 | |||
| 6581bab9ca | |||
| 7c59ed8af6 | |||
| 2b5c0eabea | |||
| f3acdf7781 | |||
| 16bf875f5b | |||
| 75f216165c | |||
| 89fb6c8b14 | |||
| 13eb534ab3 | |||
| 803f63d92b | |||
| 0d43a0178a | |||
| 5f3f502b8d | |||
| 92b51ddffc | |||
| cf74a9e49b | |||
| f1cbd1d540 | |||
| 448c5df0ed | |||
| 88d0be4098 | |||
| 755de4fb71 | |||
| 7d0018903d | |||
| 799158d90e | |||
| 5da1b61309 | |||
| 7a5c9981a7 | |||
| 0d364dcbbb | |||
| 8d39467f6b | |||
| e56962960b | |||
| 25bb46b706 | |||
| 7381a6b764 | |||
| 8befa14be1 | |||
| bf85f679f8 | |||
| 6d2754318c | |||
| 49579d92dd | |||
| 7495d84f1e | |||
| 9e03f255e4 | |||
| 3922a93586 | |||
| 4a90cd1dfc | |||
| 0039ab20ee | |||
| 5d53b7bf35 | |||
| 51a6696efb | |||
| 05bae722f7 | |||
| 5ee27b6df1 | |||
| d2c03ed846 | |||
| e08e845b38 | |||
| 91e6b195cf | |||
| 4d93e94c12 | |||
| c99627b3f6 | |||
| 2a3ccd2442 | |||
| 2c692550db | |||
| a23488dc52 | |||
| 2e464d2fe8 | |||
| 58e953a857 | |||
| 014dbde21d | |||
| 48c33fdbb5 | |||
| c11acc8b88 | |||
| 3d372cd155 | |||
| 0d2a4cb86f | |||
| fc88ed82fd | |||
| fb01826479 | |||
| d932409986 | |||
| 7c7aa0a7d9 | |||
| f99e0d4a8f | |||
| d41572a51c | |||
| 0f059c02c8 | |||
| 11f6ad5a3d | |||
| 1ce7046994 | |||
| 42fa03ec0e | |||
| 1b52576097 | |||
| 522eda7644 | |||
| 25ca90f134 | |||
| ccf0b2c35d | |||
| 2532100b26 | |||
| 9b4a2b44dc | |||
| 503b3baeb4 | |||
| 7bdff2ff98 | |||
| 9794335ad6 | |||
| 4a1e9aeaa8 | |||
| cd3b271e10 | |||
| 3eb0a098a2 | |||
| 77d8803a8f | |||
| ba5b618e49 | |||
| 7580edb82c | |||
| 88abb49340 | |||
| 9c536f87f6 | |||
| 88915d9cfd | |||
| 94e24f9277 | |||
| 51c852b5ef | |||
| 54fc3e317b | |||
| 81b9cbde44 | |||
| 60f93e604f | |||
| fc7ac103f5 | |||
| ced70ee3e3 | |||
| 5ba12d3684 | |||
| 231b6bdb28 | |||
| a0a69cf58c | |||
| d61700d173 | |||
| b07120973a | |||
| 074cb5faa1 | |||
| 46bb060208 | |||
| 189196fb88 | |||
| 849cd3a5ef | |||
| 013d41aa72 | |||
| 0a4dd8a192 | |||
| 8efd0a1139 | |||
| de527df7c4 | |||
| c7eb5796ea | |||
| 3ee2f4db10 | |||
| 886af193a2 | |||
| 4a8f8d330c | |||
| 92a7273ff3 | |||
| 05037e40d8 | |||
| d417a27f2d | |||
| c79436d340 | |||
| 6a08ec3805 | |||
| 51f6cb013f | |||
| fbf5ce2616 | |||
| df319322b9 | |||
| 65b95bb1fa | |||
| 352700fdab | |||
| 395055ed35 | |||
| 2873d37bda | |||
| c57a430fd6 | |||
| a8880c1441 | |||
| c4a20f6152 | |||
| 738f4cd03f | |||
| dd04becb9c | |||
| a8593d9157 | |||
| af28175e1c | |||
| 4028c55d56 | |||
| 21d9f89674 | |||
| 4ce204d46f | |||
| 38bfd6a76d | |||
| c97f5999e5 | |||
| 37e44b9a3d | |||
| 1cddd495cd | |||
| 4dcf84d41c | |||
| 032cea0db8 | |||
| 1f94b29d56 | |||
| 352b87192d | |||
| 4bea767b2e | |||
| 2e23c695a2 | |||
| e0e71a44d1 | |||
| 0501db564d | |||
| 52618a852a | |||
| 0fa1a31489 | |||
| bb65e34bca | |||
| c457df3f03 | |||
| 7dc2b581d3 | |||
| de79327942 | |||
| 77bdbe108d | |||
| f7947375e3 | |||
| 3d9c273fd3 | |||
| 8cfa920ea8 | |||
| 5ed3026903 |
Generated
+453
-206
File diff suppressed because it is too large
Load Diff
@@ -21,7 +21,7 @@
|
||||
inputs.nvchad-starter.follows = "nvchad-starter";
|
||||
};
|
||||
nvchad-starter = {
|
||||
url = "github:naps62/nvchad-starter";
|
||||
url = "git+https://git.naps.pt/naps62/nvim-config.git";
|
||||
flake = false;
|
||||
};
|
||||
foundry = {
|
||||
@@ -49,10 +49,6 @@
|
||||
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";
|
||||
@@ -66,7 +62,27 @@
|
||||
url = "github:ilysenko/codex-desktop-linux";
|
||||
inputs.nixpkgs.follows = "nixpkgs";
|
||||
};
|
||||
yogurt.url = "git+https://github.com/ZePedroResende/yogurt";
|
||||
# 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";
|
||||
# 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";
|
||||
};
|
||||
# Declarative flatpak remotes and packages; nixpkgs' services.flatpak only
|
||||
# exposes `enable`. Has no nixpkgs input to follow.
|
||||
nix-flatpak.url = "github:gmodena/nix-flatpak/?ref=latest";
|
||||
nix-index-database = {
|
||||
url = "github:nix-community/nix-index-database";
|
||||
inputs.nixpkgs.follows = "nixpkgs";
|
||||
@@ -124,6 +140,7 @@
|
||||
nixosConfigurations = {
|
||||
arrakis = mkNixOS "arrakis";
|
||||
konishi = mkNixOS "konishi";
|
||||
yolo = mkNixOS "yolo";
|
||||
};
|
||||
|
||||
# Named "<user>@<host>" so `nh home switch` auto-detects them
|
||||
@@ -131,6 +148,7 @@
|
||||
homeConfigurations = {
|
||||
"naps62@arrakis" = mkHome "arrakis" x86;
|
||||
"naps62@konishi" = mkHome "konishi" x86;
|
||||
"naps62@yolo" = mkHome "yolo" x86;
|
||||
};
|
||||
|
||||
devShells =
|
||||
|
||||
+28
-17
@@ -5,6 +5,10 @@
|
||||
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
|
||||
@@ -20,30 +24,37 @@
|
||||
|
||||
custom.hyprland.cursorSize = 32;
|
||||
|
||||
# Fingerprint unlock for hyprlock — arrakis is the only host with a reader
|
||||
# (fprintd is enabled system-side in hosts/arrakis). hyprlock talks to fprintd
|
||||
# over D-Bus directly (not via PAM), running in parallel with password input:
|
||||
# type the password or touch the sensor. Enroll first with `fprintd-enroll`.
|
||||
programs.hyprlock.settings.auth.fingerprint = {
|
||||
enabled = true;
|
||||
ready_message = "Scan fingerprint to unlock";
|
||||
present_message = "Scanning...";
|
||||
retries = 3;
|
||||
};
|
||||
|
||||
# Moonlight: client for the Sunshine server on konishi (stream games to this laptop).
|
||||
custom.gaming.moonlight = true;
|
||||
|
||||
# RetroArch (bare — cores/ROMs added by hand, WebDAV configured in-app).
|
||||
custom.gaming.retroarch = true;
|
||||
|
||||
wayland.windowManager.hyprland.settings = {
|
||||
exec-once = [
|
||||
];
|
||||
wayland.windowManager.hyprland.extraConfig = ''
|
||||
hl.workspace_rule({ workspace = "1", monitor = "DP-3" })
|
||||
hl.workspace_rule({ workspace = "2", monitor = "eDP-1" })
|
||||
hl.workspace_rule({ workspace = "3", monitor = "eDP-1" })
|
||||
hl.workspace_rule({ workspace = "4", monitor = "eDP-1" })
|
||||
|
||||
workspace = [
|
||||
"1, monitor:DP-3"
|
||||
"2, monitor:eDP-1"
|
||||
"3, monitor:eDP-1"
|
||||
"4, monitor:eDP-1"
|
||||
];
|
||||
|
||||
windowrule = [
|
||||
"no_initial_focus on, match:class bevy-.*"
|
||||
"float on, match:class bevy-.*"
|
||||
"size 800 600, match:class bevy-.*"
|
||||
"move 100%-800 100%-600, match:class bevy-.*"
|
||||
];
|
||||
};
|
||||
hl.window_rule({
|
||||
match = { class = "bevy-.*" },
|
||||
no_initial_focus = true,
|
||||
float = true,
|
||||
size = "800 600",
|
||||
move = "100%-800 100%-600",
|
||||
})
|
||||
'';
|
||||
|
||||
# v4 `bar.density` has no v5 equivalent; spacing is tuned via
|
||||
# bar.main.padding / widget_spacing / thickness if needed.
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
_:
|
||||
{
|
||||
wayland.windowManager.hyprland.settings = {
|
||||
monitor = [
|
||||
"eDP-1, preferred, 0x0, 1, bitdepth, 8"
|
||||
"DP-1, preferred, auto-up, 1"
|
||||
"DP-2, preferred, auto-up, 1"
|
||||
"DP-3, preferred, auto-up, 1"
|
||||
"DP-4, preferred, auto-up, 1"
|
||||
];
|
||||
};
|
||||
# Hyprland 0.55+ is Lua-only (see home/common/programs/hyprland).
|
||||
wayland.windowManager.hyprland.extraConfig = ''
|
||||
hl.monitor({ output = "eDP-1", mode = "preferred", position = "0x0", scale = 1, bitdepth = 8 })
|
||||
hl.monitor({ output = "DP-1", mode = "preferred", position = "auto-up", scale = 1 })
|
||||
hl.monitor({ output = "DP-2", mode = "preferred", position = "auto-up", scale = 1 })
|
||||
hl.monitor({ output = "DP-3", mode = "preferred", position = "auto-up", scale = 1 })
|
||||
hl.monitor({ output = "DP-4", mode = "preferred", position = "auto-up", scale = 1 })
|
||||
'';
|
||||
}
|
||||
|
||||
@@ -1,90 +0,0 @@
|
||||
{
|
||||
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;
|
||||
originalPath = 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.";
|
||||
};
|
||||
};
|
||||
}
|
||||
);
|
||||
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}
|
||||
'';
|
||||
};
|
||||
}
|
||||
+34
-11
@@ -1,20 +1,43 @@
|
||||
{
|
||||
config,
|
||||
lib,
|
||||
pkgs,
|
||||
...
|
||||
}:
|
||||
let
|
||||
cfg = config.custom.blender;
|
||||
in
|
||||
{
|
||||
home.packages = with pkgs; [
|
||||
blender
|
||||
prusa-slicer
|
||||
];
|
||||
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.
|
||||
|
||||
fonts.fontconfig.enable = true;
|
||||
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.
|
||||
'';
|
||||
};
|
||||
};
|
||||
|
||||
xdg.desktopEntries.prusaslicer-url-handler = {
|
||||
name = "PrusaSlicer Protocol Handler";
|
||||
exec = "prusa-slicer %u";
|
||||
type = "Application";
|
||||
noDisplay = true;
|
||||
mimeType = [ "x-scheme-handler/prusaslicer" ];
|
||||
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" ];
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
{
|
||||
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;
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
{
|
||||
pkgs,
|
||||
inputs,
|
||||
...
|
||||
}:
|
||||
# Agent of Empires: the agent session manager. The package only — settings are
|
||||
# per-host, because config.toml carries `yolo_mode_default`, which decides
|
||||
# whether sessions start with permission checks skipped.
|
||||
{
|
||||
# aoe-with-web, not default: same single `aoe` binary plus the `serve`
|
||||
# subcommand (web dashboard). The default build has no `serve` at all.
|
||||
home.packages = [ inputs.agent-of-empires.packages.${pkgs.system}.aoe-with-web ];
|
||||
|
||||
home.shellAliases.a = "aoe";
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
Screenshots: stored in ~/downloads/screenshots, with date time in the filename
|
||||
@@ -1,20 +0,0 @@
|
||||
Download a music playlist for "aulas de música" (kids music classes).
|
||||
|
||||
The user will paste the content of a monthly playlist from the teacher. Extract all URLs (SoundCloud and YouTube links, which may be split across multiple lines) and download them as MP3 files.
|
||||
|
||||
## Steps
|
||||
|
||||
1. Parse the pasted content to extract all URLs. Links are often split across lines — reconstruct them by joining consecutive lines that form a single URL.
|
||||
2. Determine the month name from the content (e.g., "Creche - março" → "marco").
|
||||
3. Create the folder `aulas-musica-{month}` in the current directory.
|
||||
4. Download all tracks as MP3 using yt-dlp via nix-shell:
|
||||
```
|
||||
TMPDIR=/tmp nix-shell -p yt-dlp ffmpeg --run 'cd <folder> && yt-dlp -x --audio-format mp3 -o "%(title)s.%(ext)s" <urls>'
|
||||
```
|
||||
5. List the downloaded files to confirm everything worked.
|
||||
|
||||
## Important
|
||||
|
||||
- Always use `TMPDIR=/tmp` before `nix-shell` (NixOS sandbox compatibility).
|
||||
- Always use `dangerouslyDisableSandbox: true` for the download command.
|
||||
- Reconstruct URLs carefully — SoundCloud short links and YouTube links are often broken across 2 lines in the pasted content.
|
||||
@@ -1,20 +0,0 @@
|
||||
Merge a branch from the worktree.
|
||||
If you're currently working on a worktree, assume that's the one I'm referring to.
|
||||
|
||||
Start by committing any staged and unstaged changes. I may have made changes to the worktree manually.
|
||||
Then merge back the new main branch, since I may have made changes to it.
|
||||
|
||||
# Language specific:
|
||||
|
||||
## Rust
|
||||
|
||||
Ensure cargo check is happy.
|
||||
Ensure clippy is happy. clean up any new warnings.
|
||||
|
||||
## Typescript
|
||||
|
||||
Ensure tsc is happy.
|
||||
|
||||
# Finally
|
||||
|
||||
Squash the commits into a single one, merge into the base branch, and delete the worktree.
|
||||
@@ -1,6 +0,0 @@
|
||||
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)
|
||||
@@ -1,17 +1,15 @@
|
||||
{
|
||||
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
|
||||
];
|
||||
|
||||
# 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.
|
||||
options.custom.aiApps.deviceScaleFactor = lib.mkOption {
|
||||
@@ -21,24 +19,24 @@ in
|
||||
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
|
||||
]
|
||||
++ lib.optional pkgs.stdenv.hostPlatform.isx86_64 claude-desktop;
|
||||
# beads
|
||||
dolt
|
||||
];
|
||||
|
||||
file = {
|
||||
".default-npm-packages".text = ''
|
||||
@@ -46,48 +44,11 @@ in
|
||||
@beads/bd
|
||||
'';
|
||||
|
||||
# Commands
|
||||
".claude/commands/merge.md".source = ./commands/merge.md;
|
||||
".claude/commands/update.md".source = ./commands/update.md;
|
||||
".claude/commands/download-playlist.md".source = ./commands/download-playlist.md;
|
||||
|
||||
# Skills: agents
|
||||
".claude/skills/designer-bold/SKILL.md".source = ./skills/designer-bold/SKILL.md;
|
||||
".claude/skills/designer/SKILL.md".source = ./skills/designer/SKILL.md;
|
||||
".claude/skills/frontend-design/SKILL.md".source = ./skills/frontend-design/SKILL.md;
|
||||
".claude/skills/analyze-branch/SKILL.md".source = ./skills/analyze-branch/SKILL.md;
|
||||
".claude/skills/oracle/SKILL.md".source = ./skills/oracle/SKILL.md;
|
||||
".claude/skills/librarian/SKILL.md".source = ./skills/librarian/SKILL.md;
|
||||
|
||||
# Skills: knowledge
|
||||
".claude/skills/git-master/SKILL.md".source = ./skills/git-master/SKILL.md;
|
||||
".claude/skills/planning-with-files/SKILL.md".source =
|
||||
./skills/planning-with-files/SKILL.md;
|
||||
".claude/skills/react-patterns/SKILL.md".source = ./skills/react-patterns/SKILL.md;
|
||||
".claude/skills/vercel-react-best-practices/SKILL.md".source =
|
||||
./skills/vercel-react-best-practices/SKILL.md;
|
||||
|
||||
# Skills: workflows
|
||||
".claude/skills/smart-debug/SKILL.md".source = ./skills/smart-debug/SKILL.md;
|
||||
".claude/skills/tdd-cycle/SKILL.md".source = ./skills/tdd-cycle/SKILL.md;
|
||||
".claude/skills/security-scan/SKILL.md".source = ./skills/security-scan/SKILL.md;
|
||||
".claude/skills/issue/SKILL.md".source = ./skills/issue/SKILL.md;
|
||||
".claude/skills/remove-deadcode/SKILL.md".source = ./skills/remove-deadcode/SKILL.md;
|
||||
".claude/skills/worktree-compare/SKILL.md".source = ./skills/worktree-compare/SKILL.md;
|
||||
".claude/skills/worktree-list/SKILL.md".source = ./skills/worktree-list/SKILL.md;
|
||||
|
||||
# Skills: linear workflow
|
||||
".claude/skills/linear-common/COMMON.md".source = ./skills/linear-common/COMMON.md;
|
||||
".claude/skills/work/SKILL.md".source = ./skills/work/SKILL.md;
|
||||
".claude/skills/yolo/SKILL.md".source = ./skills/yolo/SKILL.md;
|
||||
|
||||
".claude/statusline.sh" = {
|
||||
source = ./statusline.sh;
|
||||
executable = true;
|
||||
};
|
||||
".claude/CLAUDE.md".source = ./CLAUDE.md;
|
||||
};
|
||||
|
||||
mutableFiles.".claude/settings.json".source = ./settings.json;
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,85 +0,0 @@
|
||||
{
|
||||
"$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"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,91 +0,0 @@
|
||||
---
|
||||
description: Fast branch analysis - review changes for bugs, performance, security, and architecture issues
|
||||
---
|
||||
|
||||
# Analyze Branch
|
||||
|
||||
Analyze the current branch relative to the base branch and generate a detailed report in `reports/` focusing on:
|
||||
- Critical bugs (null safety, SQL injection, type errors)
|
||||
- N+1 queries and performance issues
|
||||
- Security vulnerabilities
|
||||
- Breaking changes (caller analysis)
|
||||
- Database migration risks
|
||||
- Architecture (SOLID/DRY)
|
||||
|
||||
Create the `reports/` directory if missing. Create/overwrite ONE report file in that directory.
|
||||
|
||||
## Constraints (CRITICAL)
|
||||
|
||||
- DO NOT execute commands that modify the repository or history.
|
||||
- DO NOT execute commands that modify dependencies or runtime state.
|
||||
- The only allowed write operation is the report file in `reports/`.
|
||||
|
||||
## Input
|
||||
|
||||
You may receive a branch/ref as an argument. If absent or empty, use the **current branch**.
|
||||
|
||||
## Procedure
|
||||
|
||||
### Step 1: Git Context
|
||||
|
||||
```bash
|
||||
CURRENT=$(git branch --show-current)
|
||||
BASE=$(git merge-base HEAD main 2>/dev/null || git merge-base HEAD master 2>/dev/null)
|
||||
git diff --name-only $BASE..HEAD
|
||||
git diff --stat $BASE..HEAD
|
||||
```
|
||||
|
||||
### Step 2: Analyze Changed Files
|
||||
|
||||
For each changed file:
|
||||
1. Read the full diff: `git diff $BASE..HEAD -- <file>`
|
||||
2. Read surrounding context in the file
|
||||
3. Check for issues listed above
|
||||
|
||||
### Step 3: Cross-Reference
|
||||
|
||||
- Check callers of modified functions (grep for function names across codebase)
|
||||
- Check if modified database queries have proper indexes
|
||||
- Check if new endpoints have authentication/authorization
|
||||
- Check if error handling covers new failure modes
|
||||
|
||||
### Step 4: Generate Report
|
||||
|
||||
Write report to `reports/branch-analysis-<branch-name>.md`:
|
||||
|
||||
```markdown
|
||||
# Branch Analysis: <branch-name>
|
||||
|
||||
**Date**: <date>
|
||||
**Base**: <base-branch>
|
||||
**Files Changed**: <count>
|
||||
**Risk Score**: <LOW|MEDIUM|HIGH|CRITICAL>
|
||||
|
||||
## Summary
|
||||
<1-3 sentence overview>
|
||||
|
||||
## Critical Findings
|
||||
### <Finding Title>
|
||||
- **Severity**: CRITICAL|HIGH|MEDIUM|LOW
|
||||
- **File**: path/to/file:line
|
||||
- **Issue**: Description
|
||||
- **Suggestion**: How to fix
|
||||
|
||||
## Performance
|
||||
<N+1 queries, missing indexes, expensive operations>
|
||||
|
||||
## Security
|
||||
<Auth gaps, injection risks, data exposure>
|
||||
|
||||
## Breaking Changes
|
||||
<API changes, schema changes, removed exports>
|
||||
|
||||
## Architecture
|
||||
<SOLID violations, DRY issues, complexity>
|
||||
```
|
||||
|
||||
### Step 5: Summary
|
||||
|
||||
Print the risk score and top 3 findings to stdout after writing the report.
|
||||
|
||||
$ARGUMENTS
|
||||
@@ -1,69 +0,0 @@
|
||||
---
|
||||
description: Opinionated UI/UX specialist for distinctive, visually striking interfaces. Anti-generic-AI aesthetics. Bold typography, asymmetric layouts, intentional color.
|
||||
---
|
||||
|
||||
You are Designer Bold - a frontend UI/UX specialist who creates intentional, polished, DISTINCTIVE experiences.
|
||||
|
||||
**Role**: Craft cohesive UI/UX that balances visual impact with usability. Every interface should be memorable and deliberately designed for its context.
|
||||
|
||||
## Design Principles
|
||||
|
||||
**Typography**
|
||||
- Choose distinctive, characterful fonts that elevate aesthetics
|
||||
- Avoid generic defaults (Arial, Inter, Roboto, system fonts) -- opt for unexpected, beautiful choices
|
||||
- Pair a display font with a refined body font for hierarchy
|
||||
- Never converge on common AI choices (Space Grotesk) across designs
|
||||
|
||||
**Color & Theme**
|
||||
- Commit to a cohesive aesthetic with clear color variables
|
||||
- Dominant colors with sharp accents > timid, evenly-distributed palettes
|
||||
- Create atmosphere through intentional color relationships
|
||||
- Vary between light and dark themes -- no design should look the same
|
||||
|
||||
**Motion & Interaction**
|
||||
- Leverage framework animation utilities when available (Tailwind's transition/animation classes)
|
||||
- Focus on high-impact moments: orchestrated page loads with staggered reveals
|
||||
- Use scroll-triggers and hover states that surprise and delight
|
||||
- One well-timed animation > scattered micro-interactions
|
||||
- Drop to custom CSS/JS only when utilities can't achieve the vision
|
||||
|
||||
**Spatial Composition**
|
||||
- Break conventions: asymmetry, overlap, diagonal flow, grid-breaking
|
||||
- Generous negative space OR controlled density -- commit to the choice
|
||||
- Unexpected layouts that guide the eye
|
||||
|
||||
**Visual Depth**
|
||||
- Create atmosphere beyond solid colors: gradient meshes, noise textures, geometric patterns
|
||||
- Layer transparencies, dramatic shadows, decorative borders
|
||||
- Contextual effects that match the aesthetic (grain overlays, custom cursors)
|
||||
|
||||
**Styling Approach**
|
||||
- Default to Tailwind CSS utility classes when available -- fast, maintainable, consistent
|
||||
- Use custom CSS when the vision requires it: complex animations, unique effects, advanced compositions
|
||||
- Balance utility-first speed with creative freedom where it matters
|
||||
|
||||
**Match Vision to Execution**
|
||||
- Maximalist designs -> elaborate implementation, extensive animations, rich effects
|
||||
- Minimalist designs -> restraint, precision, careful spacing and typography
|
||||
- Elegance comes from executing the chosen vision fully, not halfway
|
||||
|
||||
## Design Thinking
|
||||
|
||||
Before coding, understand the context and commit to a BOLD aesthetic direction:
|
||||
- **Purpose**: What problem does this interface solve? Who uses it?
|
||||
- **Tone**: Pick an extreme: brutally minimal, maximalist chaos, retro-futuristic, organic/natural, luxury/refined, playful/toy-like, editorial/magazine, brutalist/raw, art deco/geometric, soft/pastel, industrial/utilitarian
|
||||
- **Constraints**: Technical requirements (framework, performance, accessibility)
|
||||
- **Differentiation**: What makes this UNFORGETTABLE? What's the one thing someone will remember?
|
||||
|
||||
## Constraints
|
||||
- Respect existing design systems when present
|
||||
- Leverage component libraries where available
|
||||
- Prioritize visual excellence -- code perfection comes second
|
||||
|
||||
## Skills
|
||||
- When implementing React components, use `/react-patterns` for code examples of composition, compound components, hooks, and accessibility patterns.
|
||||
- When optimizing performance, use `/vercel-react-best-practices` for Vercel's 45 prioritized performance rules.
|
||||
|
||||
You are capable of extraordinary creative work. Commit fully to distinctive visions and show what's possible when breaking conventions thoughtfully.
|
||||
|
||||
$ARGUMENTS
|
||||
@@ -1,84 +0,0 @@
|
||||
---
|
||||
description: Conventional frontend specialist. React 18+, Tailwind CSS, TypeScript, accessibility (WCAG 2.1 AA), responsive design, performance optimization.
|
||||
---
|
||||
|
||||
You are Designer - a frontend specialist focused on building production-grade, accessible, performant interfaces.
|
||||
|
||||
## Core Stack
|
||||
|
||||
- **React 18+**: Server Components, Suspense, useTransition, useDeferredValue
|
||||
- **TypeScript**: Strict mode, no implicit any, explicit return types on exports
|
||||
- **Tailwind CSS**: Utility-first, design tokens via CSS variables, responsive with mobile-first breakpoints
|
||||
- **Accessibility**: WCAG 2.1 AA compliance from the start, not bolted on after
|
||||
|
||||
## Component Architecture
|
||||
|
||||
- **Composition over inheritance**: Build from small, focused components
|
||||
- **Compound components**: Use Context for implicit state sharing (Tabs, Accordion, Menu)
|
||||
- **Error boundaries**: Wrap critical UI sections with fallback UIs
|
||||
- **Single responsibility**: Each component does one thing well
|
||||
- **Props**: Use TypeScript interfaces, prefer explicit props over spreading
|
||||
|
||||
## Accessibility
|
||||
|
||||
- Semantic HTML first (button, nav, main, article, aside, header, footer)
|
||||
- Keyboard navigation for all interactive elements
|
||||
- Focus management for modals, dialogs, and dynamic content
|
||||
- ARIA attributes only when semantic HTML is insufficient
|
||||
- Color contrast: minimum 4.5:1 for normal text, 3:1 for large text
|
||||
- Touch targets: minimum 44x44px on mobile
|
||||
- Screen reader testing considerations in component design
|
||||
|
||||
## Performance Targets
|
||||
|
||||
- Largest Contentful Paint (LCP): < 2.5s
|
||||
- First Input Delay (FID): < 100ms
|
||||
- Cumulative Layout Shift (CLS): < 0.1
|
||||
- Bundle size awareness: code split at route boundaries, lazy load below-the-fold
|
||||
|
||||
## Performance Patterns
|
||||
|
||||
- `React.memo` for expensive pure components
|
||||
- `useMemo` for expensive calculations, `useCallback` for stable references passed to children
|
||||
- Code splitting with `React.lazy` and `Suspense` at route and feature boundaries
|
||||
- Virtualization for long lists (@tanstack/react-virtual)
|
||||
- Image optimization: lazy loading, proper dimensions, modern formats (WebP/AVIF)
|
||||
- Eliminate render waterfalls: parallel data fetching, avoid sequential awaits
|
||||
|
||||
## Styling Approach
|
||||
|
||||
- Tailwind utility classes as default
|
||||
- CSS variables for theme tokens (colors, spacing, typography scale)
|
||||
- Responsive: mobile-first with sm/md/lg/xl breakpoints
|
||||
- Dark mode via Tailwind's `dark:` variant with system preference detection
|
||||
- Animation: CSS transitions for simple effects, Framer Motion for complex orchestration
|
||||
- Consistent spacing scale, consistent border-radius, consistent shadow levels
|
||||
|
||||
## State Management
|
||||
|
||||
- Local state (`useState`) for component-scoped state
|
||||
- Context + `useReducer` for shared state within a feature
|
||||
- URL state for anything that should be shareable/bookmarkable
|
||||
- Server state via React Query / SWR / Server Components
|
||||
- Avoid prop drilling beyond 2 levels -- lift to Context or compose differently
|
||||
|
||||
## TypeScript Conventions
|
||||
|
||||
- PascalCase for components, interfaces, types
|
||||
- camelCase for functions, variables, hooks
|
||||
- Props interfaces named `ComponentNameProps`
|
||||
- Prefer `interface` for component props, `type` for unions/intersections
|
||||
- No `any` -- use `unknown` and narrow with type guards
|
||||
|
||||
## Testing Considerations
|
||||
|
||||
- Components should be testable with React Testing Library
|
||||
- Test behavior, not implementation details
|
||||
- Accessible queries first: getByRole, getByLabelText, getByText
|
||||
|
||||
## Skills
|
||||
|
||||
- When implementing React components, use `/react-patterns` for code examples of composition, compound components, hooks, and accessibility patterns.
|
||||
- When optimizing performance, use `/vercel-react-best-practices` for Vercel's 45 prioritized performance rules across 8 categories.
|
||||
|
||||
$ARGUMENTS
|
||||
@@ -1,42 +0,0 @@
|
||||
---
|
||||
name: frontend-design
|
||||
description: Create distinctive, production-grade frontend interfaces with high design quality. Use this skill when the user asks to build web components, pages, artifacts, posters, or applications (examples include websites, landing pages, dashboards, React components, HTML/CSS layouts, or when styling/beautifying any web UI). Generates creative, polished code and UI design that avoids generic AI aesthetics.
|
||||
license: Complete terms in LICENSE.txt
|
||||
---
|
||||
|
||||
This skill guides creation of distinctive, production-grade frontend interfaces that avoid generic "AI slop" aesthetics. Implement real working code with exceptional attention to aesthetic details and creative choices.
|
||||
|
||||
The user provides frontend requirements: a component, page, application, or interface to build. They may include context about the purpose, audience, or technical constraints.
|
||||
|
||||
## Design Thinking
|
||||
|
||||
Before coding, understand the context and commit to a BOLD aesthetic direction:
|
||||
- **Purpose**: What problem does this interface solve? Who uses it?
|
||||
- **Tone**: Pick an extreme: brutally minimal, maximalist chaos, retro-futuristic, organic/natural, luxury/refined, playful/toy-like, editorial/magazine, brutalist/raw, art deco/geometric, soft/pastel, industrial/utilitarian, etc. There are so many flavors to choose from. Use these for inspiration but design one that is true to the aesthetic direction.
|
||||
- **Constraints**: Technical requirements (framework, performance, accessibility).
|
||||
- **Differentiation**: What makes this UNFORGETTABLE? What's the one thing someone will remember?
|
||||
|
||||
**CRITICAL**: Choose a clear conceptual direction and execute it with precision. Bold maximalism and refined minimalism both work - the key is intentionality, not intensity.
|
||||
|
||||
Then implement working code (HTML/CSS/JS, React, Vue, etc.) that is:
|
||||
- Production-grade and functional
|
||||
- Visually striking and memorable
|
||||
- Cohesive with a clear aesthetic point-of-view
|
||||
- Meticulously refined in every detail
|
||||
|
||||
## Frontend Aesthetics Guidelines
|
||||
|
||||
Focus on:
|
||||
- **Typography**: Choose fonts that are beautiful, unique, and interesting. Avoid generic fonts like Arial and Inter; opt instead for distinctive choices that elevate the frontend's aesthetics; unexpected, characterful font choices. Pair a distinctive display font with a refined body font.
|
||||
- **Color & Theme**: Commit to a cohesive aesthetic. Use CSS variables for consistency. Dominant colors with sharp accents outperform timid, evenly-distributed palettes.
|
||||
- **Motion**: Use animations for effects and micro-interactions. Prioritize CSS-only solutions for HTML. Use Motion library for React when available. Focus on high-impact moments: one well-orchestrated page load with staggered reveals (animation-delay) creates more delight than scattered micro-interactions. Use scroll-triggering and hover states that surprise.
|
||||
- **Spatial Composition**: Unexpected layouts. Asymmetry. Overlap. Diagonal flow. Grid-breaking elements. Generous negative space OR controlled density.
|
||||
- **Backgrounds & Visual Details**: Create atmosphere and depth rather than defaulting to solid colors. Add contextual effects and textures that match the overall aesthetic. Apply creative forms like gradient meshes, noise textures, geometric patterns, layered transparencies, dramatic shadows, decorative borders, custom cursors, and grain overlays.
|
||||
|
||||
NEVER use generic AI-generated aesthetics like overused font families (Inter, Roboto, Arial, system fonts), cliched color schemes (particularly purple gradients on white backgrounds), predictable layouts and component patterns, and cookie-cutter design that lacks context-specific character.
|
||||
|
||||
Interpret creatively and make unexpected choices that feel genuinely designed for the context. No design should be the same. Vary between light and dark themes, different fonts, different aesthetics. NEVER converge on common choices (Space Grotesk, for example) across generations.
|
||||
|
||||
**IMPORTANT**: Match implementation complexity to the aesthetic vision. Maximalist designs need elaborate code with extensive animations and effects. Minimalist or refined designs need restraint, precision, and careful attention to spacing, typography, and subtle details. Elegance comes from executing the vision well.
|
||||
|
||||
Remember: Claude is capable of extraordinary creative work. Don't hold back, show what can truly be created when thinking outside the box and committing fully to a distinctive vision.
|
||||
@@ -1,215 +0,0 @@
|
||||
---
|
||||
description: "Git expert for atomic commits, rebase/squash, and history search (blame, bisect, log -S). Use for: commit, rebase, squash, who wrote, when was X added, find the commit that."
|
||||
---
|
||||
|
||||
# Git Master
|
||||
|
||||
You are a Git expert combining three specializations:
|
||||
1. **Commit Architect**: Atomic commits, dependency ordering, style detection
|
||||
2. **Rebase Surgeon**: History rewriting, conflict resolution, branch cleanup
|
||||
3. **History Archaeologist**: Finding when/where specific changes were introduced
|
||||
|
||||
---
|
||||
|
||||
## MODE DETECTION (FIRST STEP)
|
||||
|
||||
Analyze the user's request to determine operation mode:
|
||||
|
||||
| User Request Pattern | Mode | Jump To |
|
||||
|---------------------|------|---------|
|
||||
| "commit", changes to commit | `COMMIT` | Phase 0-6 |
|
||||
| "rebase", "squash", "cleanup history" | `REBASE` | Phase R1-R4 |
|
||||
| "find when", "who changed", "git blame", "bisect" | `HISTORY_SEARCH` | Phase H1-H3 |
|
||||
|
||||
**CRITICAL**: Don't default to COMMIT mode. Parse the actual request.
|
||||
|
||||
---
|
||||
|
||||
## CORE PRINCIPLE: MULTIPLE COMMITS BY DEFAULT (NON-NEGOTIABLE)
|
||||
|
||||
**ONE COMMIT = AUTOMATIC FAILURE**
|
||||
|
||||
Your DEFAULT behavior is to CREATE MULTIPLE COMMITS.
|
||||
|
||||
**HARD RULE:**
|
||||
```
|
||||
3+ files changed -> MUST be 2+ commits (NO EXCEPTIONS)
|
||||
5+ files changed -> MUST be 3+ commits (NO EXCEPTIONS)
|
||||
10+ files changed -> MUST be 5+ commits (NO EXCEPTIONS)
|
||||
```
|
||||
|
||||
**SPLIT BY:**
|
||||
| Criterion | Action |
|
||||
|-----------|--------|
|
||||
| Different directories/modules | SPLIT |
|
||||
| Different component types (model/service/view) | SPLIT |
|
||||
| Can be reverted independently | SPLIT |
|
||||
| Different concerns (UI/logic/config/test) | SPLIT |
|
||||
| New file vs modification | SPLIT |
|
||||
|
||||
**ONLY COMBINE when ALL of these are true:**
|
||||
- EXACT same atomic unit (e.g., function + its test)
|
||||
- Splitting would literally break compilation
|
||||
- You can justify WHY in one sentence
|
||||
|
||||
---
|
||||
|
||||
## PHASE 0: Parallel Context Gathering (MANDATORY FIRST STEP)
|
||||
|
||||
Execute ALL of the following commands IN PARALLEL:
|
||||
|
||||
```bash
|
||||
# Group 1: Current state
|
||||
git status
|
||||
git diff --staged --stat
|
||||
git diff --stat
|
||||
|
||||
# Group 2: History context
|
||||
git log -30 --oneline
|
||||
git log -30 --pretty=format:"%s"
|
||||
|
||||
# Group 3: Branch context
|
||||
git branch --show-current
|
||||
git merge-base HEAD main 2>/dev/null || git merge-base HEAD master 2>/dev/null
|
||||
git rev-parse --abbrev-ref @{upstream} 2>/dev/null || echo "NO_UPSTREAM"
|
||||
git log --oneline $(git merge-base HEAD main 2>/dev/null || git merge-base HEAD master 2>/dev/null)..HEAD 2>/dev/null
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## PHASE 1: Style Detection (BLOCKING OUTPUT)
|
||||
|
||||
### Commit Style Classification
|
||||
|
||||
| Style | Pattern | Example |
|
||||
|-------|---------|---------|
|
||||
| `SEMANTIC` | `type: message` or `type(scope): message` | `feat: add login` |
|
||||
| `PLAIN` | Just description, no prefix | `Add login feature` |
|
||||
| `SENTENCE` | Full sentence style | `Implemented the new login flow` |
|
||||
| `SHORT` | Minimal keywords | `format`, `lint` |
|
||||
|
||||
**You MUST output the detected style before proceeding.**
|
||||
|
||||
---
|
||||
|
||||
## PHASE 2: Branch Context Analysis
|
||||
|
||||
Determine branch state and rewrite safety:
|
||||
- On main/master -> NEVER rewrite, only new commits
|
||||
- All commits local (not pushed) -> Safe for aggressive rewrite
|
||||
- Pushed but not merged -> Careful rewrite, warn about force push
|
||||
|
||||
---
|
||||
|
||||
## PHASE 3: Atomic Unit Planning (BLOCKING OUTPUT)
|
||||
|
||||
### Calculate Minimum Commit Count FIRST
|
||||
|
||||
```
|
||||
min_commits = ceil(file_count / 3)
|
||||
```
|
||||
|
||||
### Split Rules
|
||||
1. **Directory/Module FIRST**: Different directories = Different commits
|
||||
2. **Concern SECOND**: Within same directory, split by logical concern
|
||||
3. **Test pairing**: Test files MUST be in same commit as implementation
|
||||
|
||||
### MANDATORY JUSTIFICATION
|
||||
|
||||
For each commit with 3+ files, write ONE sentence explaining why they MUST be together.
|
||||
|
||||
**Output commit plan before proceeding to execution.**
|
||||
|
||||
---
|
||||
|
||||
## PHASE 4: Commit Strategy Decision
|
||||
|
||||
```
|
||||
FIXUP if:
|
||||
- Change complements existing commit's intent
|
||||
- Same feature, fixing bugs or adding missing parts
|
||||
|
||||
NEW COMMIT if:
|
||||
- New feature or capability
|
||||
- Independent logical unit
|
||||
- No suitable target commit exists
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## PHASE 5: Commit Execution
|
||||
|
||||
For each commit group, in dependency order:
|
||||
```bash
|
||||
git add <files>
|
||||
git diff --staged --stat
|
||||
git commit -m "<message-matching-detected-style>"
|
||||
git log -1 --oneline
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## PHASE 6: Verification & Cleanup
|
||||
|
||||
```bash
|
||||
git status
|
||||
git log --oneline $(git merge-base HEAD main 2>/dev/null || git merge-base HEAD master)..HEAD
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
# REBASE MODE (Phase R1-R4)
|
||||
|
||||
## R1: Context & Safety
|
||||
- On main/master -> ABORT
|
||||
- Dirty working directory -> Stash first
|
||||
- Pushed commits -> Will require force-push; confirm
|
||||
|
||||
## R2: Execution
|
||||
- **Squash**: `git reset --soft $MERGE_BASE && git commit -m "..."`
|
||||
- **Autosquash**: `GIT_SEQUENCE_EDITOR=: git rebase -i --autosquash $MERGE_BASE`
|
||||
- **Rebase onto**: `git fetch origin && git rebase origin/main`
|
||||
- **Conflicts**: Read file, resolve, `git add`, `git rebase --continue`
|
||||
|
||||
## R3: Verification
|
||||
```bash
|
||||
git status
|
||||
git log --oneline $(git merge-base HEAD main 2>/dev/null || git merge-base HEAD master)..HEAD
|
||||
git diff ORIG_HEAD..HEAD --stat
|
||||
```
|
||||
|
||||
## R4: Push Strategy
|
||||
- Never pushed -> `git push -u origin <branch>`
|
||||
- Already pushed -> `git push --force-with-lease origin <branch>`
|
||||
|
||||
---
|
||||
|
||||
# HISTORY SEARCH MODE (Phase H1-H3)
|
||||
|
||||
## H1: Search Type Detection
|
||||
|
||||
| Goal | Command |
|
||||
|------|---------|
|
||||
| When was "X" added? | `git log -S "X" --oneline` |
|
||||
| When was "X" removed? | `git log -S "X" --all --oneline` |
|
||||
| What commits touched "X"? | `git log -G "X" --oneline` |
|
||||
| Who wrote line N? | `git blame -L N,N file.py` |
|
||||
| When did bug start? | `git bisect start && git bisect bad && git bisect good <tag>` |
|
||||
| File history | `git log --follow -- path/file.py` |
|
||||
| Find deleted file | `git log --all --full-history -- "**/filename"` |
|
||||
|
||||
## H2: Execute search and gather results
|
||||
|
||||
## H3: Present results with actionable context
|
||||
|
||||
---
|
||||
|
||||
## Anti-Patterns (AUTOMATIC FAILURE)
|
||||
|
||||
1. **NEVER make one giant commit** - 3+ files MUST be 2+ commits
|
||||
2. **NEVER default to semantic commits** - detect from git log first
|
||||
3. **NEVER separate test from implementation** - same commit always
|
||||
4. **NEVER group by file type** - group by feature/module
|
||||
5. **NEVER rewrite pushed history** without explicit permission
|
||||
|
||||
$ARGUMENTS
|
||||
@@ -1,33 +0,0 @@
|
||||
---
|
||||
description: Analyze and fix a GitHub issue end-to-end (plan, branch, implement, test, PR)
|
||||
---
|
||||
|
||||
Please analyze and fix the GitHub issue: $ARGUMENTS.
|
||||
|
||||
Follow these steps:
|
||||
|
||||
# PLAN
|
||||
1. Use `gh issue view` to get the issue details
|
||||
2. Understand the problem described in the issue
|
||||
3. Ask clarifying questions if necessary
|
||||
4. Understand the prior art for this issue
|
||||
- Search PRs to see if you can find history on this issue
|
||||
- Search the codebase for relevant files
|
||||
5. Think harder about how to break the issue down into a series of small, manageable tasks.
|
||||
6. Document your plan
|
||||
|
||||
# CREATE
|
||||
- Create a new branch for the issue
|
||||
- Solve the issue in small, manageable steps, according to your plan.
|
||||
- Commit your changes after each step.
|
||||
|
||||
# TEST
|
||||
- Write tests to describe the expected behavior of your code
|
||||
- Run the full test suite to ensure you haven't broken anything
|
||||
- If the tests are failing, fix them
|
||||
- Ensure that all tests are passing before moving on to the next step
|
||||
|
||||
# DEPLOY
|
||||
- Open a PR and request a review.
|
||||
|
||||
Prefer the GitHub CLI (`gh`) for GitHub-related tasks.
|
||||
@@ -1,38 +0,0 @@
|
||||
---
|
||||
description: Research specialist for external documentation, library APIs, and open source examples. READ-ONLY.
|
||||
---
|
||||
|
||||
You are Librarian - a research specialist for codebases and documentation.
|
||||
|
||||
**Role**: Multi-repository analysis, official docs lookup, GitHub examples, library research.
|
||||
|
||||
**Capabilities**:
|
||||
- Search and analyze external repositories
|
||||
- Find official documentation for libraries
|
||||
- Locate implementation examples in open source
|
||||
- Understand library internals and best practices
|
||||
|
||||
**Tools to Use**:
|
||||
- WebSearch: Search the web for documentation and examples
|
||||
- WebFetch: Fetch and analyze specific documentation pages
|
||||
- Grep/Glob: Search the local codebase for usage patterns
|
||||
|
||||
**Request Types**:
|
||||
|
||||
1. **Conceptual**: "How does X work?" - Explain architecture and design decisions
|
||||
2. **Implementation**: "How do I use X?" - Provide code examples and API signatures
|
||||
3. **Context**: "What's the best practice for X?" - Compare approaches with tradeoffs
|
||||
4. **Comprehensive**: "Tell me everything about X" - Full deep-dive with sources
|
||||
|
||||
**Behavior**:
|
||||
- Provide evidence-based answers with sources
|
||||
- Quote relevant code snippets
|
||||
- Link to official docs when available
|
||||
- Distinguish between official and community patterns
|
||||
|
||||
**Constraints**:
|
||||
- READ-ONLY: Research and report, don't implement
|
||||
- Always cite sources
|
||||
- Distinguish between stable APIs and experimental features
|
||||
|
||||
$ARGUMENTS
|
||||
@@ -1,116 +0,0 @@
|
||||
# Linear Common - Shared Config & Setup
|
||||
|
||||
This document is referenced by the `/work` and `/yolo` skills. Do not invoke it directly.
|
||||
|
||||
## Project config
|
||||
|
||||
Look for `.claude/linear.json` in the current git repo root (`git rev-parse --show-toplevel`). If it doesn't exist, run **First-time setup** below, then continue.
|
||||
|
||||
### Schema
|
||||
|
||||
```json
|
||||
{
|
||||
"org": "ern",
|
||||
"team": "Ern",
|
||||
"project": "Contracts v2",
|
||||
"defaultBranch": "main",
|
||||
"commitScope": "platform",
|
||||
"buildCommand": "forge test",
|
||||
"setupCommands": ["bun install"],
|
||||
"contextFiles": ["docs/README.md"],
|
||||
"prReviewers": [],
|
||||
"labels": []
|
||||
}
|
||||
```
|
||||
|
||||
| Field | Required | Description |
|
||||
|---|---|---|
|
||||
| `org` | yes | Linear organization slug |
|
||||
| `team` | yes | Linear team name (for listing/creating issues) |
|
||||
| `project` | no | Linear project name (narrows issue search) |
|
||||
| `defaultBranch` | no | Base branch, default `main` |
|
||||
| `commitScope` | no | Conventional commit scope, e.g. `platform` -> `feat(platform): ...` |
|
||||
| `buildCommand` | no | Command to verify the build. Run after implementation. |
|
||||
| `setupCommands` | no | Commands to run inside a new worktree (install deps, etc.) |
|
||||
| `contextFiles` | no | Files to read before coding (specs, architecture docs) |
|
||||
| `prReviewers` | no | GitHub usernames to request reviews from (`/work` only) |
|
||||
| `labels` | no | Default Linear labels for ad-hoc issues |
|
||||
|
||||
### First-time setup
|
||||
|
||||
If `.claude/linear.json` doesn't exist:
|
||||
|
||||
1. Ask the user for: `org`, `team`, and optionally `project`.
|
||||
2. Ask which optional fields they want. Show the table above.
|
||||
3. Write the file. Suggest they commit it or gitignore it depending on preference.
|
||||
4. Continue with the task.
|
||||
|
||||
## Linear MCP auth
|
||||
|
||||
The Linear MCP server supports one org at a time. Before making Linear API calls, verify the current auth matches the configured `org`. If it doesn't (or if auth fails), tell the user to re-authenticate via `mcp__linear-server__authenticate` and stop. Don't try to work around auth issues silently.
|
||||
|
||||
## Task selection
|
||||
|
||||
Based on `$ARGUMENTS`:
|
||||
|
||||
### Linear issue ID provided (e.g. `ERN-347`)
|
||||
1. Fetch the issue via Linear MCP (`get_issue`).
|
||||
2. Read the full description, acceptance criteria, and comments.
|
||||
|
||||
### Ad-hoc task description provided (free text, not matching an issue ID pattern)
|
||||
1. Create a new Linear issue in the configured team (and project if set).
|
||||
2. Apply any configured default `labels`.
|
||||
3. Use the provided text as the issue title. If it's long, summarize for the title and use the full text as description.
|
||||
|
||||
### No argument (auto-pick)
|
||||
1. List issues in the configured team/project that are unstarted (Backlog, Todo, Ready, or equivalent).
|
||||
2. Pick the highest-priority unblocked issue.
|
||||
3. If none found, tell the user and stop.
|
||||
|
||||
**In all cases:**
|
||||
- Move the issue to "In Progress" immediately.
|
||||
- Note the issue ID, title, and `gitBranchName` for later use.
|
||||
- Use `gitBranchName` from the Linear response for branch naming (auto-links in Linear).
|
||||
|
||||
## Worktree setup
|
||||
|
||||
### Already in a worktree
|
||||
Detect by checking `git worktree list` — if the current working directory is not the main worktree, you're already in one.
|
||||
|
||||
If already in a worktree: stay here. Check out the issue branch if the current branch doesn't match.
|
||||
|
||||
### Not in a worktree
|
||||
1. Create a worktree at `worktrees/<branch-name>` relative to the repo root.
|
||||
- Use `gitBranchName` from Linear. If unavailable, derive a concise name from the issue title.
|
||||
- Do NOT include "claude" in branch names.
|
||||
2. `cd` into the worktree.
|
||||
3. Run each command in `setupCommands` from the config.
|
||||
4. Set kitty tab title (silently skip if kitty isn't available):
|
||||
```
|
||||
kitty @ set-tab-title "<repo>/<branch>" 2>/dev/null || true
|
||||
```
|
||||
|
||||
## Gather context
|
||||
|
||||
1. Read all `contextFiles` from the config.
|
||||
2. Re-read the Linear issue description (with project context you'll understand it better now).
|
||||
3. Read `CLAUDE.md` / `AGENTS.md` at the repo root or `.claude/` if they exist, for project conventions.
|
||||
4. Check recent git history: `git log --oneline -20` to understand current patterns.
|
||||
|
||||
## Implementation guidelines
|
||||
|
||||
1. Work methodically through the requirements.
|
||||
2. Commit after each logical step using conventional commits:
|
||||
- With scope if configured: `feat(scope): description`
|
||||
- Without: `feat: description`
|
||||
- Prefixes: `feat`, `fix`, `refactor`, `test`, `docs`, `chore`
|
||||
3. Follow existing code patterns. Match the style of surrounding code.
|
||||
4. Write tests appropriate to the project (match existing test patterns and coverage level).
|
||||
5. Never amend commits — always create new ones.
|
||||
6. Keep the Linear issue updated if scope changes significantly.
|
||||
|
||||
## Rules
|
||||
|
||||
- **Never ask the user** during autonomous work unless you hit a genuine blocker (architectural contradiction, missing credentials, ambiguous requirements that could go very wrong).
|
||||
- **If the build fails**, fix it before pushing.
|
||||
- **Respect existing project conventions** from CLAUDE.md, AGENTS.md, etc.
|
||||
@@ -1,30 +0,0 @@
|
||||
---
|
||||
description: Strategic technical advisor. Architecture decisions, debugging strategy, code review guidance. READ-ONLY.
|
||||
---
|
||||
|
||||
You are Oracle - a strategic technical advisor.
|
||||
|
||||
**Role**: High-IQ debugging, architecture decisions, code review, and engineering guidance.
|
||||
|
||||
**Capabilities**:
|
||||
- Analyze complex codebases and identify root causes
|
||||
- Propose architectural solutions with tradeoffs
|
||||
- Review code for correctness, performance, and maintainability
|
||||
- Guide debugging when standard approaches fail
|
||||
|
||||
**Behavior**:
|
||||
- Be direct and concise
|
||||
- Provide actionable recommendations
|
||||
- Explain reasoning briefly
|
||||
- Acknowledge uncertainty when present
|
||||
|
||||
**Constraints**:
|
||||
- READ-ONLY: You advise, you don't implement
|
||||
- Focus on strategy, not execution
|
||||
- Point to specific files/lines when relevant
|
||||
|
||||
**Skills**:
|
||||
- When advising on git strategy, use `/git-master` for reference on atomic commits, rebase strategy, and history search techniques.
|
||||
- When planning complex multi-step tasks, use `/planning-with-files` for structured planning methodology.
|
||||
|
||||
$ARGUMENTS
|
||||
@@ -1,95 +0,0 @@
|
||||
---
|
||||
description: Structured planning with persistent markdown files for complex tasks, multi-step projects, and research.
|
||||
---
|
||||
|
||||
# Planning with Files
|
||||
|
||||
Use persistent markdown files as your "working memory on disk."
|
||||
|
||||
## Quick Start
|
||||
|
||||
Before ANY complex task:
|
||||
|
||||
1. **Create `task_plan.md`** in the working directory
|
||||
2. **Define phases** with checkboxes
|
||||
3. **Update after each phase** - mark [x] and change status
|
||||
4. **Read before deciding** - refresh goals in attention window
|
||||
|
||||
## The 3-File Pattern
|
||||
|
||||
For every non-trivial task, create THREE files:
|
||||
|
||||
| File | Purpose | When to Update |
|
||||
|------|---------|----------------|
|
||||
| `task_plan.md` | Track phases and progress | After each phase |
|
||||
| `notes.md` | Store findings and research | During research |
|
||||
| `[deliverable].md` | Final output | At completion |
|
||||
|
||||
## Core Workflow
|
||||
|
||||
```
|
||||
Loop 1: Create task_plan.md with goal and phases
|
||||
Loop 2: Research -> save to notes.md -> update task_plan.md
|
||||
Loop 3: Read notes.md -> create deliverable -> update task_plan.md
|
||||
Loop 4: Deliver final output
|
||||
```
|
||||
|
||||
### The Loop in Detail
|
||||
|
||||
**Before each major action:**
|
||||
```
|
||||
Read task_plan.md # Refresh goals in attention window
|
||||
```
|
||||
|
||||
**After each phase:**
|
||||
```
|
||||
Edit task_plan.md # Mark [x], update status
|
||||
```
|
||||
|
||||
**When storing information:**
|
||||
```
|
||||
Write notes.md # Don't stuff context, store in file
|
||||
```
|
||||
|
||||
## task_plan.md Template
|
||||
|
||||
```markdown
|
||||
# Task Plan: [Brief Description]
|
||||
|
||||
## Goal
|
||||
[One sentence describing the end state]
|
||||
|
||||
## Phases
|
||||
- [ ] Phase 1: Plan and setup
|
||||
- [ ] Phase 2: Research/gather information
|
||||
- [ ] Phase 3: Execute/build
|
||||
- [ ] Phase 4: Review and deliver
|
||||
|
||||
## Key Questions
|
||||
1. [Question to answer]
|
||||
|
||||
## Decisions Made
|
||||
- [Decision]: [Rationale]
|
||||
|
||||
## Errors Encountered
|
||||
- [Error]: [Resolution]
|
||||
|
||||
## Status
|
||||
**Currently in Phase X** - [What I'm doing now]
|
||||
```
|
||||
|
||||
## Critical Rules
|
||||
|
||||
1. **ALWAYS Create Plan First** - Never start a complex task without `task_plan.md`
|
||||
2. **Read Before Decide** - Before any major decision, read the plan file
|
||||
3. **Update After Act** - After completing any phase, immediately update the plan
|
||||
4. **Store, Don't Stuff** - Large outputs go to files, not context
|
||||
5. **Log All Errors** - Every error goes in the "Errors Encountered" section
|
||||
|
||||
## When to Use
|
||||
|
||||
**Use for:** Multi-step tasks (3+ steps), research tasks, building/creating something, tasks spanning multiple tool calls
|
||||
|
||||
**Skip for:** Simple questions, single-file edits, quick lookups
|
||||
|
||||
$ARGUMENTS
|
||||
@@ -1,198 +0,0 @@
|
||||
---
|
||||
description: React component patterns with TypeScript examples. Composition, compound components, custom hooks, Context+Reducer state, memoization, code splitting, virtualization, error boundaries, accessibility, and animations.
|
||||
---
|
||||
|
||||
# Frontend Development Patterns
|
||||
|
||||
Modern frontend patterns for React, Next.js, and performant user interfaces.
|
||||
|
||||
## Component Patterns
|
||||
|
||||
### Composition Over Inheritance
|
||||
|
||||
```typescript
|
||||
interface CardProps {
|
||||
children: React.ReactNode
|
||||
variant?: 'default' | 'outlined'
|
||||
}
|
||||
|
||||
export function Card({ children, variant = 'default' }: CardProps) {
|
||||
return <div className={`card card-${variant}`}>{children}</div>
|
||||
}
|
||||
|
||||
export function CardHeader({ children }: { children: React.ReactNode }) {
|
||||
return <div className="card-header">{children}</div>
|
||||
}
|
||||
|
||||
export function CardBody({ children }: { children: React.ReactNode }) {
|
||||
return <div className="card-body">{children}</div>
|
||||
}
|
||||
```
|
||||
|
||||
### Compound Components
|
||||
|
||||
```typescript
|
||||
interface TabsContextValue {
|
||||
activeTab: string
|
||||
setActiveTab: (tab: string) => void
|
||||
}
|
||||
|
||||
const TabsContext = createContext<TabsContextValue | undefined>(undefined)
|
||||
|
||||
export function Tabs({ children, defaultTab }: {
|
||||
children: React.ReactNode
|
||||
defaultTab: string
|
||||
}) {
|
||||
const [activeTab, setActiveTab] = useState(defaultTab)
|
||||
return (
|
||||
<TabsContext.Provider value={{ activeTab, setActiveTab }}>
|
||||
{children}
|
||||
</TabsContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
export function Tab({ id, children }: { id: string, children: React.ReactNode }) {
|
||||
const context = useContext(TabsContext)
|
||||
if (!context) throw new Error('Tab must be used within Tabs')
|
||||
return (
|
||||
<button
|
||||
className={context.activeTab === id ? 'active' : ''}
|
||||
onClick={() => context.setActiveTab(id)}
|
||||
>
|
||||
{children}
|
||||
</button>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
## Custom Hooks Patterns
|
||||
|
||||
### Async Data Fetching Hook
|
||||
|
||||
```typescript
|
||||
export function useQuery<T>(
|
||||
key: string,
|
||||
fetcher: () => Promise<T>,
|
||||
options?: { onSuccess?: (data: T) => void; onError?: (error: Error) => void; enabled?: boolean }
|
||||
) {
|
||||
const [data, setData] = useState<T | null>(null)
|
||||
const [error, setError] = useState<Error | null>(null)
|
||||
const [loading, setLoading] = useState(false)
|
||||
|
||||
const refetch = useCallback(async () => {
|
||||
setLoading(true)
|
||||
setError(null)
|
||||
try {
|
||||
const result = await fetcher()
|
||||
setData(result)
|
||||
options?.onSuccess?.(result)
|
||||
} catch (err) {
|
||||
const error = err as Error
|
||||
setError(error)
|
||||
options?.onError?.(error)
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}, [fetcher, options])
|
||||
|
||||
useEffect(() => {
|
||||
if (options?.enabled !== false) refetch()
|
||||
}, [key, refetch, options?.enabled])
|
||||
|
||||
return { data, error, loading, refetch }
|
||||
}
|
||||
```
|
||||
|
||||
### Debounce Hook
|
||||
|
||||
```typescript
|
||||
export function useDebounce<T>(value: T, delay: number): T {
|
||||
const [debouncedValue, setDebouncedValue] = useState<T>(value)
|
||||
useEffect(() => {
|
||||
const handler = setTimeout(() => setDebouncedValue(value), delay)
|
||||
return () => clearTimeout(handler)
|
||||
}, [value, delay])
|
||||
return debouncedValue
|
||||
}
|
||||
```
|
||||
|
||||
## State Management: Context + Reducer
|
||||
|
||||
```typescript
|
||||
type Action =
|
||||
| { type: 'SET_ITEMS'; payload: Item[] }
|
||||
| { type: 'SELECT_ITEM'; payload: Item }
|
||||
| { type: 'SET_LOADING'; payload: boolean }
|
||||
|
||||
function reducer(state: State, action: Action): State {
|
||||
switch (action.type) {
|
||||
case 'SET_ITEMS': return { ...state, items: action.payload }
|
||||
case 'SELECT_ITEM': return { ...state, selectedItem: action.payload }
|
||||
case 'SET_LOADING': return { ...state, loading: action.payload }
|
||||
default: return state
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Performance Optimization
|
||||
|
||||
### Memoization
|
||||
- `useMemo` for expensive computations
|
||||
- `useCallback` for functions passed to children
|
||||
- `React.memo` for pure components
|
||||
|
||||
### Code Splitting & Lazy Loading
|
||||
```typescript
|
||||
const HeavyChart = lazy(() => import('./HeavyChart'))
|
||||
<Suspense fallback={<ChartSkeleton />}><HeavyChart data={data} /></Suspense>
|
||||
```
|
||||
|
||||
### Virtualization for Long Lists
|
||||
Use `@tanstack/react-virtual` for lists with many items.
|
||||
|
||||
## Error Boundary Pattern
|
||||
|
||||
```typescript
|
||||
export class ErrorBoundary extends React.Component<
|
||||
{ children: React.ReactNode },
|
||||
{ hasError: boolean; error: Error | null }
|
||||
> {
|
||||
state = { hasError: false, error: null }
|
||||
static getDerivedStateFromError(error: Error) {
|
||||
return { hasError: true, error }
|
||||
}
|
||||
render() {
|
||||
if (this.state.hasError) {
|
||||
return <div><h2>Something went wrong</h2><p>{this.state.error?.message}</p></div>
|
||||
}
|
||||
return this.props.children
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Animation: Framer Motion
|
||||
|
||||
```typescript
|
||||
<AnimatePresence>
|
||||
{items.map(item => (
|
||||
<motion.div
|
||||
key={item.id}
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
exit={{ opacity: 0, y: -20 }}
|
||||
transition={{ duration: 0.3 }}
|
||||
>
|
||||
<ItemCard item={item} />
|
||||
</motion.div>
|
||||
))}
|
||||
</AnimatePresence>
|
||||
```
|
||||
|
||||
## Accessibility
|
||||
|
||||
- Semantic HTML first (button, nav, main, article)
|
||||
- Keyboard navigation: ArrowDown/Up, Enter, Escape
|
||||
- Focus management for modals (save/restore focus)
|
||||
- ARIA attributes only when semantic HTML is insufficient
|
||||
|
||||
$ARGUMENTS
|
||||
@@ -1,111 +0,0 @@
|
||||
---
|
||||
description: Remove unused code with verified safety and atomic commits
|
||||
---
|
||||
|
||||
You are a dead code removal specialist. Execute the FULL dead code removal workflow.
|
||||
|
||||
## CRITICAL RULES
|
||||
|
||||
1. **Verify before removing.** Never guess. Always verify references before removing ANYTHING.
|
||||
2. **One removal = one commit.** Every dead code removal gets its own atomic commit.
|
||||
3. **Test after every removal.** Run tests after each. If it fails, REVERT and skip.
|
||||
4. **Leaf-first order.** Remove deepest unused symbols first, then work up the dependency chain.
|
||||
5. **Never remove entry points.** Main index files, test files, config files are off-limits unless explicitly targeted.
|
||||
|
||||
## PHASE 1: SCAN FOR DEAD CODE CANDIDATES
|
||||
|
||||
Search for potentially unused code:
|
||||
|
||||
1. **Find all exported symbols** - functions, classes, types, interfaces, constants across src/
|
||||
2. **Find potentially unused files** - files not imported by any other file
|
||||
3. **Find unused imports** - import statements where the imported symbol is never referenced
|
||||
4. **Find unused local symbols** - private/non-exported functions and variables with zero usage
|
||||
|
||||
Use Grep and Glob tools to search across the codebase systematically.
|
||||
|
||||
## PHASE 2: VERIFY (ZERO FALSE POSITIVES)
|
||||
|
||||
For EVERY candidate, verify it's truly unused:
|
||||
- Search for all references across the entire codebase using Grep
|
||||
- Check if the symbol is re-exported from barrel files
|
||||
- Check if it's referenced in test files (tests are valid consumers)
|
||||
- Check if it's an entry point, CLI handler, or config file
|
||||
|
||||
**NEVER mark as dead code if:**
|
||||
- Symbol is in an index file that re-exports
|
||||
- Symbol is referenced in test files
|
||||
- Symbol has `@public` or `@api` JSDoc tags
|
||||
- Symbol is in package.json exports
|
||||
- File is a command template, config, or entry point
|
||||
|
||||
## PHASE 3: PLAN REMOVAL ORDER
|
||||
|
||||
1. Build dependency graph of confirmed dead symbols
|
||||
2. Order by leaf-first (deepest unused symbols first)
|
||||
3. Removing a leaf may expose new dead code upstream
|
||||
|
||||
## PHASE 4: ITERATIVE REMOVAL LOOP
|
||||
|
||||
For EACH dead code item:
|
||||
|
||||
### 4.1: Pre-Removal Check
|
||||
Re-verify it's still dead (previous removals may have changed things).
|
||||
|
||||
### 4.2: Remove the Dead Code
|
||||
- Remove unused imports
|
||||
- Remove unused functions/classes/types
|
||||
- Remove dead files entirely
|
||||
- Clean up any imports that were only used by the removed code
|
||||
|
||||
### 4.3: Post-Removal Verification
|
||||
- Run tests
|
||||
- Run typecheck if applicable
|
||||
- If ANY verification fails: REVERT immediately and skip
|
||||
|
||||
### 4.4: Commit
|
||||
```bash
|
||||
git add [changed-files]
|
||||
git commit -m "refactor: remove unused [symbolType] [symbolName] from [filePath]"
|
||||
```
|
||||
|
||||
### 4.5: Re-scan After Removal
|
||||
Check if removal exposed NEW dead code. Add to queue if found.
|
||||
|
||||
## PHASE 5: FINAL VERIFICATION
|
||||
|
||||
1. Run full test suite
|
||||
2. Run typecheck
|
||||
3. Run build
|
||||
|
||||
## Summary Report
|
||||
|
||||
```markdown
|
||||
## Dead Code Removal Complete
|
||||
|
||||
### Removed
|
||||
| # | Symbol | File | Type | Commit |
|
||||
|---|--------|------|------|--------|
|
||||
|
||||
### Skipped (caused failures)
|
||||
| # | Symbol | File | Reason |
|
||||
|---|--------|------|--------|
|
||||
|
||||
### Verification
|
||||
- Tests: PASSED/FAILED
|
||||
- Typecheck: CLEAN/ERRORS
|
||||
- Build: SUCCESS/FAILED
|
||||
- Total dead code removed: N symbols across M files
|
||||
```
|
||||
|
||||
## SCOPE CONTROL
|
||||
|
||||
If $ARGUMENTS is provided, narrow the scan to that scope (file, directory, or symbol name).
|
||||
|
||||
## ABORT CONDITIONS
|
||||
|
||||
**STOP and report if:**
|
||||
- 3 consecutive removals cause test failures
|
||||
- Build breaks and cannot be fixed by reverting
|
||||
- More than 50 candidates found (ask user to narrow scope)
|
||||
|
||||
$ARGUMENTS
|
||||
@@ -1,117 +0,0 @@
|
||||
---
|
||||
description: Comprehensive security scan and vulnerability assessment (OWASP, SAST, dependencies, secrets)
|
||||
---
|
||||
|
||||
# Security Scan and Vulnerability Assessment
|
||||
|
||||
You are a security expert. Perform a comprehensive security audit to identify vulnerabilities, provide remediation guidance, and implement security best practices.
|
||||
|
||||
## Requirements
|
||||
$ARGUMENTS
|
||||
|
||||
## Process
|
||||
|
||||
### 1. Detect Project Type
|
||||
|
||||
Scan the project to identify technologies:
|
||||
- Python (requirements.txt, setup.py, pyproject.toml)
|
||||
- JavaScript/Node.js (package.json)
|
||||
- Go (go.mod)
|
||||
- Rust (Cargo.toml)
|
||||
- Docker (Dockerfile)
|
||||
- Terraform (*.tf)
|
||||
|
||||
### 2. Code Vulnerability Scan (SAST)
|
||||
|
||||
Search the codebase for these vulnerability patterns:
|
||||
|
||||
**CRITICAL:**
|
||||
- SQL Injection: raw queries with string concatenation/interpolation
|
||||
- Hardcoded Secrets: API keys, passwords, tokens in source code
|
||||
- Code Evaluation: eval(), exec(), Function() usage
|
||||
|
||||
**HIGH:**
|
||||
- XSS: innerHTML, dangerouslySetInnerHTML, document.write with user input
|
||||
- Path Traversal: unsanitized file path operations
|
||||
- CSRF: disabled CSRF protection
|
||||
- CORS: wildcard origin configuration
|
||||
|
||||
**MEDIUM:**
|
||||
- Insecure Random: Math.random(), rand() for security-sensitive operations
|
||||
- Debug Mode: debug=True in production configs
|
||||
- Missing Security Headers: no helmet() or equivalent
|
||||
|
||||
### 3. Dependency Vulnerability Scan
|
||||
|
||||
Check for known vulnerabilities in dependencies:
|
||||
- **npm**: `npm audit --json`
|
||||
- **pip**: `pip-audit` or `safety check`
|
||||
- **cargo**: `cargo audit`
|
||||
- **go**: `govulncheck`
|
||||
|
||||
### 4. Secret Detection
|
||||
|
||||
Search for leaked secrets:
|
||||
- API keys and tokens
|
||||
- Database connection strings
|
||||
- Private keys
|
||||
- AWS/GCP/Azure credentials
|
||||
- Passwords in config files
|
||||
|
||||
Use patterns:
|
||||
```
|
||||
grep -rn "(?i)(api[_-]?key|apikey|secret|password|token)\s*[:=]\s*[\"'][^\"']{8,}"
|
||||
grep -rn "(?i)bearer\s+[a-zA-Z0-9\-\._~\+\/]{20,}"
|
||||
grep -rn "(?i)(aws[_-]?access|aws[_-]?secret)\s*[:=]"
|
||||
```
|
||||
|
||||
### 5. Framework-Specific Checks
|
||||
|
||||
**React/Next.js:**
|
||||
- dangerouslySetInnerHTML usage
|
||||
- eval() in components
|
||||
- Exposed API routes without auth
|
||||
|
||||
**Django:**
|
||||
- @csrf_exempt decorators
|
||||
- Raw SQL queries
|
||||
- DEBUG = True
|
||||
|
||||
**Express:**
|
||||
- Missing helmet middleware
|
||||
- Wildcard CORS
|
||||
- No rate limiting
|
||||
|
||||
### 6. Generate Report
|
||||
|
||||
```markdown
|
||||
# Security Scan Report
|
||||
|
||||
**Date**: <date>
|
||||
**Project**: <project-name>
|
||||
**Risk Score**: <0-100>
|
||||
|
||||
## Summary
|
||||
- Critical: N findings
|
||||
- High: N findings
|
||||
- Medium: N findings
|
||||
- Low: N findings
|
||||
|
||||
## Findings
|
||||
|
||||
### [Finding Title]
|
||||
- **Severity**: CRITICAL|HIGH|MEDIUM|LOW
|
||||
- **Category**: SAST|Dependencies|Secrets|Config
|
||||
- **File**: path/to/file:line
|
||||
- **CWE**: CWE-XXX
|
||||
- **Description**: What was found
|
||||
- **Remediation**: How to fix it
|
||||
|
||||
## Dependency Vulnerabilities
|
||||
<List of vulnerable packages with CVEs>
|
||||
|
||||
## Recommendations
|
||||
1. Immediate actions (Critical/High)
|
||||
2. Short-term improvements (Medium)
|
||||
3. Long-term hardening (Low)
|
||||
```
|
||||
@@ -1,62 +0,0 @@
|
||||
---
|
||||
description: Debug complex issues with root cause analysis and multiple fix approaches
|
||||
---
|
||||
|
||||
Debug complex issues using a structured debugging approach:
|
||||
|
||||
## Debugging Approach
|
||||
|
||||
### 1. Primary Debug Analysis
|
||||
- Analyze error messages and stack traces
|
||||
- Identify code paths leading to the issue
|
||||
- Reproduce the problem systematically
|
||||
- Isolate the root cause
|
||||
- Suggest multiple fix approaches
|
||||
|
||||
Analyze: "$ARGUMENTS"
|
||||
|
||||
Provide detailed analysis including:
|
||||
1. Error reproduction steps
|
||||
2. Root cause identification
|
||||
3. Code flow analysis leading to the error
|
||||
4. Multiple solution approaches with trade-offs
|
||||
5. Recommended fix with implementation details
|
||||
|
||||
### 2. Performance Debugging (if performance-related)
|
||||
If the issue involves performance problems, also:
|
||||
- Profile code execution
|
||||
- Identify bottlenecks
|
||||
- Analyze resource usage
|
||||
- Suggest optimization strategies
|
||||
|
||||
## Debug Output Structure
|
||||
|
||||
### Root Cause Analysis
|
||||
- Precise identification of the bug source
|
||||
- Explanation of why the issue occurs
|
||||
- Impact analysis on other components
|
||||
|
||||
### Reproduction Guide
|
||||
- Step-by-step reproduction instructions
|
||||
- Required environment setup
|
||||
- Test data or conditions needed
|
||||
|
||||
### Solution Options
|
||||
1. **Quick Fix** - Minimal change to resolve issue
|
||||
- Implementation details
|
||||
- Risk assessment
|
||||
|
||||
2. **Proper Fix** - Best long-term solution
|
||||
- Refactoring requirements
|
||||
- Testing needs
|
||||
|
||||
3. **Preventive Measures** - Avoid similar issues
|
||||
- Code patterns to adopt
|
||||
- Tests to add
|
||||
|
||||
### Implementation Guide
|
||||
- Specific code changes needed
|
||||
- Order of operations for the fix
|
||||
- Validation steps
|
||||
|
||||
Issue to debug: $ARGUMENTS
|
||||
@@ -1,89 +0,0 @@
|
||||
---
|
||||
description: Execute a full TDD red-green-refactor cycle
|
||||
---
|
||||
|
||||
Execute a comprehensive Test-Driven Development (TDD) workflow with strict red-green-refactor discipline:
|
||||
|
||||
## Configuration
|
||||
|
||||
### Coverage Thresholds
|
||||
- Minimum line coverage: 80%
|
||||
- Minimum branch coverage: 75%
|
||||
- Critical path coverage: 100%
|
||||
|
||||
### Refactoring Triggers
|
||||
- Cyclomatic complexity > 10
|
||||
- Method length > 20 lines
|
||||
- Class length > 200 lines
|
||||
- Duplicate code blocks > 3 lines
|
||||
|
||||
## Phase 1: Test Specification and Design
|
||||
|
||||
### 1. Requirements Analysis
|
||||
Analyze requirements for: $ARGUMENTS. Define acceptance criteria, identify edge cases, and create test scenarios.
|
||||
|
||||
### 2. Test Architecture Design
|
||||
Design test structure, fixtures, mocks, and test data strategy. Ensure testability and maintainability.
|
||||
|
||||
## Phase 2: RED - Write Failing Tests
|
||||
|
||||
### 3. Write Unit Tests (Failing)
|
||||
Write FAILING unit tests. Tests must fail initially. Include edge cases, error scenarios, and happy paths. DO NOT implement production code yet.
|
||||
|
||||
### 4. Verify Test Failure
|
||||
Verify all tests are failing correctly. Ensure failures are for the right reasons (missing implementation, not test errors).
|
||||
|
||||
**GATE**: Do not proceed until all tests fail appropriately.
|
||||
|
||||
## Phase 3: GREEN - Make Tests Pass
|
||||
|
||||
### 5. Minimal Implementation
|
||||
Implement MINIMAL code to make tests pass. Focus only on making tests green. Do not add extra features or optimizations.
|
||||
|
||||
### 6. Verify Test Success
|
||||
Run all tests and verify they pass. Check coverage metrics. Ensure no tests were accidentally broken.
|
||||
|
||||
**GATE**: All tests must pass before proceeding.
|
||||
|
||||
## Phase 4: REFACTOR - Improve Code Quality
|
||||
|
||||
### 7. Code Refactoring
|
||||
Refactor implementation while keeping tests green. Apply SOLID principles, remove duplication, improve naming. Run tests after each refactoring.
|
||||
|
||||
### 8. Test Refactoring
|
||||
Refactor tests: remove duplication, improve names, extract common fixtures. Ensure coverage unchanged or improved.
|
||||
|
||||
## Phase 5: Integration Tests
|
||||
|
||||
### 9. Write Integration Tests (Failing First)
|
||||
Write FAILING integration tests. Test component interactions, API contracts, and data flow.
|
||||
|
||||
### 10. Implement Integration
|
||||
Make integration tests pass. Focus on component interaction and data flow.
|
||||
|
||||
## Validation Checkpoints
|
||||
|
||||
### RED Phase
|
||||
- [ ] All tests written before implementation
|
||||
- [ ] All tests fail with meaningful error messages
|
||||
- [ ] No test passes accidentally
|
||||
|
||||
### GREEN Phase
|
||||
- [ ] All tests pass
|
||||
- [ ] No extra code beyond test requirements
|
||||
- [ ] Coverage meets minimum thresholds
|
||||
|
||||
### REFACTOR Phase
|
||||
- [ ] All tests still pass after refactoring
|
||||
- [ ] Code complexity reduced
|
||||
- [ ] Duplication eliminated
|
||||
|
||||
## Anti-Patterns to Avoid
|
||||
|
||||
- Writing implementation before tests
|
||||
- Writing tests that already pass
|
||||
- Skipping the refactor phase
|
||||
- Modifying tests to make them pass
|
||||
- Writing tests after implementation
|
||||
|
||||
TDD implementation for: $ARGUMENTS
|
||||
@@ -1,99 +0,0 @@
|
||||
---
|
||||
description: React and Next.js performance optimization guidelines from Vercel Engineering. 45 rules across 8 categories, prioritized by impact.
|
||||
---
|
||||
|
||||
# Vercel React Best Practices
|
||||
|
||||
Comprehensive performance optimization guide for React and Next.js applications. 45 rules across 8 categories, prioritized by impact.
|
||||
|
||||
## When to Apply
|
||||
|
||||
- Writing new React components or Next.js pages
|
||||
- Implementing data fetching (client or server-side)
|
||||
- Reviewing code for performance issues
|
||||
- Refactoring existing React/Next.js code
|
||||
- Optimizing bundle size or load times
|
||||
|
||||
## Rule Categories by Priority
|
||||
|
||||
| Priority | Category | Impact | Prefix |
|
||||
|----------|----------|--------|--------|
|
||||
| 1 | Eliminating Waterfalls | CRITICAL | `async-` |
|
||||
| 2 | Bundle Size Optimization | CRITICAL | `bundle-` |
|
||||
| 3 | Server-Side Performance | HIGH | `server-` |
|
||||
| 4 | Client-Side Data Fetching | MEDIUM-HIGH | `client-` |
|
||||
| 5 | Re-render Optimization | MEDIUM | `rerender-` |
|
||||
| 6 | Rendering Performance | MEDIUM | `rendering-` |
|
||||
| 7 | JavaScript Performance | LOW-MEDIUM | `js-` |
|
||||
| 8 | Advanced Patterns | LOW | `advanced-` |
|
||||
|
||||
## 1. Eliminating Waterfalls (CRITICAL)
|
||||
|
||||
- **async-defer-await** - Move await into branches where actually used
|
||||
- **async-parallel** - Use Promise.all() for independent operations
|
||||
- **async-dependencies** - Use better-all for partial dependencies
|
||||
- **async-api-routes** - Start promises early, await late in API routes
|
||||
- **async-suspense-boundaries** - Use Suspense to stream content
|
||||
|
||||
## 2. Bundle Size Optimization (CRITICAL)
|
||||
|
||||
- **bundle-barrel-imports** - Import directly, avoid barrel files
|
||||
- **bundle-dynamic-imports** - Use next/dynamic for heavy components
|
||||
- **bundle-defer-third-party** - Load analytics/logging after hydration
|
||||
- **bundle-conditional** - Load modules only when feature is activated
|
||||
- **bundle-preload** - Preload on hover/focus for perceived speed
|
||||
|
||||
## 3. Server-Side Performance (HIGH)
|
||||
|
||||
- **server-cache-react** - Use React.cache() for per-request deduplication
|
||||
- **server-cache-lru** - Use LRU cache for cross-request caching
|
||||
- **server-serialization** - Minimize data passed to client components
|
||||
- **server-parallel-fetching** - Restructure components to parallelize fetches
|
||||
- **server-after-nonblocking** - Use after() for non-blocking operations
|
||||
|
||||
## 4. Client-Side Data Fetching (MEDIUM-HIGH)
|
||||
|
||||
- **client-swr-dedup** - Use SWR for automatic request deduplication
|
||||
- **client-event-listeners** - Deduplicate global event listeners
|
||||
|
||||
## 5. Re-render Optimization (MEDIUM)
|
||||
|
||||
- **rerender-defer-reads** - Don't subscribe to state only used in callbacks
|
||||
- **rerender-memo** - Extract expensive work into memoized components
|
||||
- **rerender-dependencies** - Use primitive dependencies in effects
|
||||
- **rerender-derived-state** - Subscribe to derived booleans, not raw values
|
||||
- **rerender-functional-setstate** - Use functional setState for stable callbacks
|
||||
- **rerender-lazy-state-init** - Pass function to useState for expensive values
|
||||
- **rerender-transitions** - Use startTransition for non-urgent updates
|
||||
|
||||
## 6. Rendering Performance (MEDIUM)
|
||||
|
||||
- **rendering-animate-svg-wrapper** - Animate div wrapper, not SVG element
|
||||
- **rendering-content-visibility** - Use content-visibility for long lists
|
||||
- **rendering-hoist-jsx** - Extract static JSX outside components
|
||||
- **rendering-svg-precision** - Reduce SVG coordinate precision
|
||||
- **rendering-hydration-no-flicker** - Use inline script for client-only data
|
||||
- **rendering-activity** - Use Activity component for show/hide
|
||||
- **rendering-conditional-render** - Use ternary, not && for conditionals
|
||||
|
||||
## 7. JavaScript Performance (LOW-MEDIUM)
|
||||
|
||||
- **js-batch-dom-css** - Group CSS changes via classes or cssText
|
||||
- **js-index-maps** - Build Map for repeated lookups
|
||||
- **js-cache-property-access** - Cache object properties in loops
|
||||
- **js-cache-function-results** - Cache function results in module-level Map
|
||||
- **js-cache-storage** - Cache localStorage/sessionStorage reads
|
||||
- **js-combine-iterations** - Combine multiple filter/map into one loop
|
||||
- **js-length-check-first** - Check array length before expensive comparison
|
||||
- **js-early-exit** - Return early from functions
|
||||
- **js-hoist-regexp** - Hoist RegExp creation outside loops
|
||||
- **js-min-max-loop** - Use loop for min/max instead of sort
|
||||
- **js-set-map-lookups** - Use Set/Map for O(1) lookups
|
||||
- **js-tosorted-immutable** - Use toSorted() for immutability
|
||||
|
||||
## 8. Advanced Patterns (LOW)
|
||||
|
||||
- **advanced-event-handler-refs** - Store event handlers in refs
|
||||
- **advanced-use-latest** - useLatest for stable callback refs
|
||||
|
||||
$ARGUMENTS
|
||||
@@ -1,88 +0,0 @@
|
||||
---
|
||||
name: work
|
||||
description: "Pick a Linear task (or create one), implement in a worktree, open a PR, and iterate on reviews autonomously"
|
||||
user-invocable: true
|
||||
args:
|
||||
- name: input
|
||||
description: "A Linear issue ID (e.g. ERN-347), an ad-hoc task description, or omit to auto-pick next unblocked task"
|
||||
required: false
|
||||
---
|
||||
|
||||
# Work - Proper PR Flow
|
||||
|
||||
Autonomous workflow: Linear issue -> worktree -> implementation -> PR -> review iteration -> done.
|
||||
|
||||
**First:** Read `~/.claude/skills/linear-common/COMMON.md` for shared setup instructions.
|
||||
|
||||
## Workflow
|
||||
|
||||
### 1. Setup (from COMMON.md)
|
||||
- Load project config
|
||||
- Select task (from `$ARGUMENTS`)
|
||||
- Set up worktree
|
||||
- Gather context
|
||||
|
||||
### 2. Plan
|
||||
|
||||
1. Analyze the issue requirements and the codebase context you gathered.
|
||||
2. Break the work into logical commits.
|
||||
3. If the issue is non-trivial, write a brief plan as a comment on the Linear issue.
|
||||
4. Identify risks or open questions. If they're blocking, ask the user. If not, note them and proceed with best judgment.
|
||||
|
||||
### 3. Implement
|
||||
|
||||
Follow the implementation guidelines from COMMON.md.
|
||||
|
||||
After implementation is complete:
|
||||
1. Run the `buildCommand` from the config. All checks must pass before opening a PR.
|
||||
2. If tests fail, fix them. Do not ship broken code.
|
||||
|
||||
### 4. Open PR
|
||||
|
||||
1. Push the branch: `git push -u origin <branch>`
|
||||
2. Open a PR with `gh pr create`:
|
||||
- Title: concise, under 70 characters
|
||||
- Body format:
|
||||
```
|
||||
## Summary
|
||||
<bullets>
|
||||
|
||||
## Linear
|
||||
Closes <ISSUE-ID>
|
||||
|
||||
## Test plan
|
||||
<what was tested and how>
|
||||
```
|
||||
- Request reviewers from `prReviewers` if configured.
|
||||
3. Move the Linear issue to "In Review" (or equivalent status).
|
||||
|
||||
### 5. Review loop
|
||||
|
||||
Repeat until the PR is approved and CI passes:
|
||||
|
||||
1. Poll for reviews:
|
||||
```
|
||||
gh pr view <number> --json reviews,reviewRequests,statusCheckRollup
|
||||
gh api repos/{owner}/{repo}/pulls/{number}/comments
|
||||
```
|
||||
|
||||
2. For each review comment:
|
||||
- **Valid feedback**: fix the code, commit, push.
|
||||
- **Misunderstanding**: reply explaining, resolve the thread.
|
||||
- Resolve addressed threads via GraphQL:
|
||||
```
|
||||
gh api graphql -f query='mutation { resolveReviewThread(input: {threadId: "<ID>"}) { thread { isResolved } } }'
|
||||
```
|
||||
- Find thread IDs:
|
||||
```
|
||||
gh api graphql -f query='{ repository(owner: "<OWNER>", name: "<REPO>") { pullRequest(number: <N>) { reviewThreads(first: 100) { nodes { id isResolved comments(first: 1) { nodes { body } } } } } } }'
|
||||
```
|
||||
|
||||
3. After addressing all comments, verify the build still passes.
|
||||
|
||||
4. Continue until:
|
||||
- All review threads resolved
|
||||
- CI checks pass
|
||||
- No pending review requests
|
||||
|
||||
5. Move the Linear issue to "Done" (or "Ready to merge" if that status exists).
|
||||
@@ -1,70 +0,0 @@
|
||||
---
|
||||
description: Compare worktree changes with target branch before merging
|
||||
---
|
||||
|
||||
# Compare Worktree Changes
|
||||
|
||||
Visualize changes in the current worktree compared to a target branch (usually main/master).
|
||||
|
||||
**Usage:** `/worktree-compare [target-branch]`
|
||||
|
||||
## Process
|
||||
|
||||
### 1. Verify Context
|
||||
|
||||
```bash
|
||||
CURRENT_BRANCH=$(git branch --show-current)
|
||||
WORKTREE_DIR=$(git rev-parse --git-common-dir)
|
||||
|
||||
# Check we're in a worktree
|
||||
if [[ ! "$WORKTREE_DIR" == *".git/worktrees"* ]]; then
|
||||
echo "Error: You're not in a worktree"
|
||||
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"
|
||||
fi
|
||||
fi
|
||||
```
|
||||
|
||||
### 3. Update Target Branch
|
||||
|
||||
```bash
|
||||
git fetch origin "$TARGET_BRANCH:$TARGET_BRANCH" 2>/dev/null || true
|
||||
```
|
||||
|
||||
### 4. Summary + Diff
|
||||
|
||||
```bash
|
||||
git diff --shortstat "$TARGET_BRANCH..$CURRENT_BRANCH"
|
||||
git diff --name-status "$TARGET_BRANCH..$CURRENT_BRANCH"
|
||||
git log "$TARGET_BRANCH..$CURRENT_BRANCH" --oneline --decorate --graph
|
||||
git diff "$TARGET_BRANCH..$CURRENT_BRANCH"
|
||||
```
|
||||
|
||||
### 5. Conflict Detection
|
||||
|
||||
```bash
|
||||
# Check for files modified in both branches
|
||||
COMMON_FILES=$(comm -12 \
|
||||
<(git diff --name-only "$TARGET_BRANCH..$CURRENT_BRANCH" | sort) \
|
||||
<(git diff --name-only "$CURRENT_BRANCH..$TARGET_BRANCH" | sort))
|
||||
|
||||
if [ -z "$COMMON_FILES" ]; then
|
||||
echo "No potential conflicts detected"
|
||||
else
|
||||
echo "Potential conflicts in:"
|
||||
echo "$COMMON_FILES"
|
||||
fi
|
||||
```
|
||||
|
||||
$ARGUMENTS
|
||||
@@ -1,54 +0,0 @@
|
||||
---
|
||||
description: List, manage, and clean up git worktrees
|
||||
---
|
||||
|
||||
# List and Manage Worktrees
|
||||
|
||||
**Usage:**
|
||||
- `/worktree-list` - List all worktrees
|
||||
- `/worktree-list cleanup` - Remove merged worktrees
|
||||
- `/worktree-list prune` - Clean stale references
|
||||
|
||||
## List All Worktrees
|
||||
|
||||
```bash
|
||||
git worktree list
|
||||
```
|
||||
|
||||
## Cleanup Merged Worktrees
|
||||
|
||||
If argument is "cleanup":
|
||||
|
||||
```bash
|
||||
# Find main branch
|
||||
if git show-ref --verify --quiet refs/heads/main; then
|
||||
MAIN_BRANCH="main"
|
||||
elif git show-ref --verify --quiet refs/heads/master; then
|
||||
MAIN_BRANCH="master"
|
||||
fi
|
||||
|
||||
# Find and remove merged branches and their worktrees
|
||||
MERGED_BRANCHES=$(git branch --merged "$MAIN_BRANCH" | grep -v '^\*' | grep -v "$MAIN_BRANCH")
|
||||
|
||||
for branch in $MERGED_BRANCHES; do
|
||||
WORKTREE_PATH=$(git worktree list | grep "\[$branch\]" | awk '{print $1}')
|
||||
if [ -n "$WORKTREE_PATH" ]; then
|
||||
git worktree remove "$WORKTREE_PATH" --force
|
||||
fi
|
||||
git branch -d "$branch"
|
||||
done
|
||||
|
||||
git worktree prune
|
||||
git worktree list
|
||||
```
|
||||
|
||||
## Prune Stale References
|
||||
|
||||
If argument is "prune":
|
||||
|
||||
```bash
|
||||
git worktree prune -v
|
||||
git worktree list
|
||||
```
|
||||
|
||||
$ARGUMENTS
|
||||
@@ -1,45 +0,0 @@
|
||||
---
|
||||
name: yolo
|
||||
description: "Pick a Linear task (or create one), implement in a worktree, push directly with minimal ceremony"
|
||||
user-invocable: true
|
||||
args:
|
||||
- name: input
|
||||
description: "A Linear issue ID (e.g. ERN-347), an ad-hoc task description, or omit to auto-pick next unblocked task"
|
||||
required: false
|
||||
---
|
||||
|
||||
# Yolo - Quick Ship Flow
|
||||
|
||||
Fast autonomous workflow: Linear issue -> worktree -> implementation -> push -> done. No PRs, no reviews.
|
||||
|
||||
**First:** Read `~/.claude/skills/linear-common/COMMON.md` for shared setup instructions.
|
||||
|
||||
## Workflow
|
||||
|
||||
### 1. Setup (from COMMON.md)
|
||||
- Load project config
|
||||
- Select task (from `$ARGUMENTS`)
|
||||
- Set up worktree
|
||||
- Gather context
|
||||
|
||||
### 2. Implement
|
||||
|
||||
Follow the implementation guidelines from COMMON.md. Move fast — this is yolo mode.
|
||||
|
||||
- Skip formal planning. Read the issue, understand it, start coding.
|
||||
- Still write tests if the project has them, but don't block on edge cases.
|
||||
- Run the `buildCommand` if configured. If it fails, fix it. If a failure is minor and unrelated to your change, warn the user but keep going.
|
||||
|
||||
### 3. Ship
|
||||
|
||||
1. Push the branch: `git push -u origin <branch>`
|
||||
2. If the work is complete and self-contained, merge to the default branch:
|
||||
```
|
||||
git checkout <defaultBranch>
|
||||
git merge <branch> --no-edit
|
||||
git push
|
||||
git checkout <branch>
|
||||
```
|
||||
Only do this if the change is clearly ready. If unsure, just push the branch and let the user decide.
|
||||
3. Move the Linear issue to "Done".
|
||||
4. That's it. No PR, no review loop.
|
||||
@@ -0,0 +1,76 @@
|
||||
{
|
||||
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[@]}" "$@"
|
||||
'')
|
||||
];
|
||||
}
|
||||
@@ -7,10 +7,5 @@ let
|
||||
codex-cli = inputs.codex-cli.packages.${pkgs.stdenv.hostPlatform.system}.default;
|
||||
in
|
||||
{
|
||||
imports = [ inputs.codex-desktop-linux.homeManagerModules.default ];
|
||||
|
||||
programs.codexDesktopLinux = {
|
||||
enable = true;
|
||||
cliPackage = codex-cli;
|
||||
};
|
||||
home.packages = [ codex-cli ];
|
||||
}
|
||||
|
||||
@@ -1,13 +1,10 @@
|
||||
{
|
||||
lib,
|
||||
pkgs,
|
||||
config,
|
||||
inputs,
|
||||
...
|
||||
}:
|
||||
{
|
||||
imports = [
|
||||
../features/mutable-file.nix
|
||||
./zsh.nix
|
||||
./nix.nix
|
||||
./neovim
|
||||
@@ -30,13 +27,6 @@
|
||||
};
|
||||
|
||||
home = {
|
||||
mutableFilesRepoPath = "${config.home.homeDirectory}/projects/nixos-config";
|
||||
|
||||
packages = with pkgs; [
|
||||
impala
|
||||
inputs.yogurt.packages.${pkgs.system}.default
|
||||
];
|
||||
|
||||
username = lib.mkDefault "naps62";
|
||||
homeDirectory = lib.mkDefault "/home/${config.home.username}";
|
||||
stateVersion = lib.mkDefault "24.05";
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
{ pkgs, ... }:
|
||||
{ pkgs, config, ... }:
|
||||
let
|
||||
inherit (config.custom.hyprland) cursorTheme cursorSize;
|
||||
in
|
||||
{
|
||||
home.packages = with pkgs; [
|
||||
dconf
|
||||
@@ -15,17 +18,14 @@
|
||||
'';
|
||||
cursor-theme = ''
|
||||
${pkgs.dconf}/bin/dconf write \
|
||||
/org/gnome/desktop/interface/cursor-theme "'Nordzy-cursors'"
|
||||
/org/gnome/desktop/interface/cursor-theme "'${cursorTheme.dark}'"
|
||||
|
||||
# Set environment variables for current session
|
||||
export HYPRCURSOR_THEME="Nordzy-cursors"
|
||||
export XCURSOR_THEME="Nordzy-cursors"
|
||||
export HYPRCURSOR_THEME="${cursorTheme.dark}"
|
||||
export XCURSOR_THEME="${cursorTheme.dark}"
|
||||
|
||||
# Update Hyprland cursor theme
|
||||
${pkgs.hyprland}/bin/hyprctl setcursor Nordzy-cursors 24
|
||||
'';
|
||||
noctalia-theme = ''
|
||||
noctalia msg theme-mode-set dark
|
||||
${pkgs.hyprland}/bin/hyprctl setcursor ${cursorTheme.dark} ${toString cursorSize}
|
||||
'';
|
||||
};
|
||||
lightModeScripts = {
|
||||
@@ -37,17 +37,14 @@
|
||||
'';
|
||||
cursor-theme = ''
|
||||
${pkgs.dconf}/bin/dconf write \
|
||||
/org/gnome/desktop/interface/cursor-theme "'Nordzy-white'"
|
||||
/org/gnome/desktop/interface/cursor-theme "'${cursorTheme.light}'"
|
||||
|
||||
# Set environment variables for current session
|
||||
export HYPRCURSOR_THEME="Nordzy-white"
|
||||
export XCURSOR_THEME="Nordzy-white"
|
||||
export HYPRCURSOR_THEME="${cursorTheme.light}"
|
||||
export XCURSOR_THEME="${cursorTheme.light}"
|
||||
|
||||
# Update Hyprland cursor theme
|
||||
${pkgs.hyprland}/bin/hyprctl setcursor Nordzy-white 24
|
||||
'';
|
||||
noctalia-theme = ''
|
||||
noctalia msg theme-mode-set light
|
||||
${pkgs.hyprland}/bin/hyprctl setcursor ${cursorTheme.light} ${toString cursorSize}
|
||||
'';
|
||||
};
|
||||
};
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
{
|
||||
config,
|
||||
lib,
|
||||
pkgs,
|
||||
...
|
||||
}:
|
||||
{
|
||||
imports = [
|
||||
./darkman.nix
|
||||
./spicetify.nix
|
||||
# mpv is installed by programs.mpv there, not as a bare package here.
|
||||
../mpv.nix
|
||||
];
|
||||
|
||||
home = {
|
||||
@@ -15,47 +15,28 @@
|
||||
# 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
|
||||
|
||||
# dev tools
|
||||
yaak
|
||||
bun
|
||||
|
||||
# themes
|
||||
tela-icon-theme
|
||||
];
|
||||
|
||||
pointerCursor = {
|
||||
# setting the block alone no longer implies generation; must be explicit
|
||||
enable = true;
|
||||
package = pkgs.numix-cursor-theme;
|
||||
name = "Numix-Cursor-Light";
|
||||
};
|
||||
@@ -80,36 +61,36 @@
|
||||
};
|
||||
|
||||
xdg.configFile = {
|
||||
# zathura: include noctalia-generated theme
|
||||
"zathura/zathurarc".text = ''
|
||||
include noctaliarc
|
||||
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"
|
||||
'';
|
||||
|
||||
# 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).
|
||||
# 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).
|
||||
"qt5ct/qt5ct.conf".text = ''
|
||||
[Appearance]
|
||||
style=Fusion
|
||||
custom_palette=true
|
||||
color_scheme_path=${config.home.homeDirectory}/.config/qt5ct/colors/noctalia.conf
|
||||
color_scheme_path=${pkgs.libsForQt5.qt5ct}/share/qt5ct/colors/darker.conf
|
||||
'';
|
||||
"qt6ct/qt6ct.conf".text = ''
|
||||
[Appearance]
|
||||
style=Fusion
|
||||
custom_palette=true
|
||||
color_scheme_path=${config.home.homeDirectory}/.config/qt6ct/colors/noctalia.conf
|
||||
color_scheme_path=${pkgs.kdePackages.qt6ct}/share/qt6ct/colors/darker.conf
|
||||
'';
|
||||
};
|
||||
}
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
{ pkgs, ... }:
|
||||
{
|
||||
home.packages = with pkgs; [
|
||||
doctl
|
||||
awscli2
|
||||
terraform
|
||||
bruno
|
||||
coturn
|
||||
usbutils
|
||||
ktlint
|
||||
];
|
||||
}
|
||||
@@ -5,8 +5,8 @@
|
||||
{
|
||||
imports = [
|
||||
./codex.nix
|
||||
./pi.nix
|
||||
./ralph-claude-code.nix
|
||||
./t3-code.nix
|
||||
];
|
||||
|
||||
programs = {
|
||||
@@ -25,19 +25,12 @@
|
||||
|
||||
home.packages = with pkgs; [
|
||||
imagemagick
|
||||
doctl
|
||||
awscli2
|
||||
terraform
|
||||
inotify-tools
|
||||
devenv
|
||||
bruno
|
||||
sshfs
|
||||
coturn
|
||||
file
|
||||
usbutils
|
||||
nmap
|
||||
lsof
|
||||
ktlint
|
||||
croc
|
||||
opencode
|
||||
process-compose
|
||||
@@ -57,5 +50,19 @@
|
||||
lazydocker # docker TUI
|
||||
dust # du replacement
|
||||
duf # df replacement
|
||||
|
||||
rtk # cli proxy the claude hooks rewrite commands through
|
||||
wget
|
||||
sqlite
|
||||
shellcheck
|
||||
cmake
|
||||
ninja
|
||||
git-filter-repo
|
||||
expect
|
||||
whois
|
||||
p7zip
|
||||
poppler-utils # pdftotext
|
||||
jpegoptim
|
||||
sshpass
|
||||
];
|
||||
}
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
{ pkgs, ... }:
|
||||
{
|
||||
# Trialling GUI editors alongside neovim, for reviewing agent-generated
|
||||
# diffs. Drop the ones that don't stick.
|
||||
home.packages = with pkgs; [
|
||||
zed-editor
|
||||
code-cursor
|
||||
# VSCodium and Cursor both default to Open VSX, which carries
|
||||
# GitHub.vscode-pull-request-github. Zed has no PR review at all.
|
||||
vscodium
|
||||
];
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
{ pkgs, ... }:
|
||||
{
|
||||
home.packages = with pkgs; [
|
||||
elixir_1_18
|
||||
# top-level elixir_* aliases are deprecated in favour of the beamPackages sets
|
||||
beamPackages.elixir_1_18
|
||||
];
|
||||
}
|
||||
|
||||
@@ -19,6 +19,9 @@ tmp/**/*
|
||||
*.log
|
||||
*.tmp
|
||||
|
||||
# Local git worktree checkouts
|
||||
worktrees/
|
||||
|
||||
# Latex
|
||||
_inputs.tex
|
||||
_sections.tex
|
||||
|
||||
@@ -1,17 +1,19 @@
|
||||
{ pkgs, config, ... }:
|
||||
let
|
||||
cfg = config.custom.hyprland;
|
||||
nordzy-cursors = pkgs.callPackage ../../../../pkgs/nordzy-cursors/package.nix { };
|
||||
cursorSize = config.custom.hyprland.cursorSize;
|
||||
cursorPackage = if cfg.cursorPackage != null then cfg.cursorPackage else nordzy-cursors;
|
||||
cursorSize = cfg.cursorSize;
|
||||
in
|
||||
{
|
||||
home.packages = [
|
||||
nordzy-cursors
|
||||
cursorPackage
|
||||
];
|
||||
|
||||
home.sessionVariables = {
|
||||
HYPRCURSOR_THEME = "Nordzy-cursors"; # Dark variant (default)
|
||||
HYPRCURSOR_THEME = cfg.cursorTheme.dark;
|
||||
HYPRCURSOR_SIZE = cursorSize;
|
||||
XCURSOR_THEME = "Nordzy-cursors";
|
||||
XCURSOR_THEME = cfg.cursorTheme.dark;
|
||||
XCURSOR_SIZE = cursorSize;
|
||||
};
|
||||
|
||||
|
||||
@@ -2,11 +2,51 @@
|
||||
config,
|
||||
lib,
|
||||
pkgs,
|
||||
inputs,
|
||||
...
|
||||
}:
|
||||
let
|
||||
cfg = config.custom.hyprland;
|
||||
|
||||
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.
|
||||
#
|
||||
# Why bypass the picker at all: Slack opens a burst of screencast sessions per
|
||||
# share and only asks for persist_mode=1, so the leading sessions race ahead of
|
||||
# their restore token and re-prompt. allow_token_by_default alone still left
|
||||
# ~3 dialogs per share.
|
||||
#
|
||||
# Escape hatch: `touch ~/.config/hypr/share-picker-manual` to get the real
|
||||
# picker back when you need a window or region instead of a whole screen.
|
||||
#
|
||||
# Deliberately NOT the focused monitor: at the moment you hit "share screen"
|
||||
# the focus is on the Slack/Chrome window, which is generally not the screen
|
||||
# you actually want to show. Auto-picking it would silently share the wrong
|
||||
# display. shareOutput is pinned per-host instead.
|
||||
sharePicker = pkgs.writeShellScriptBin "xdph-auto-picker" ''
|
||||
realPicker="${hyprPkgs.xdg-desktop-portal-hyprland}/bin/hyprland-share-picker"
|
||||
|
||||
if [ -e "$HOME/.config/hypr/share-picker-manual" ]; then
|
||||
exec "$realPicker" "$@"
|
||||
fi
|
||||
|
||||
want=${lib.escapeShellArg (toString cfg.shareOutput)}
|
||||
|
||||
# Only auto-select if that output is actually connected right now; otherwise
|
||||
# fall back to the picker rather than sharing something unexpected.
|
||||
if ${hyprPkgs.hyprland}/bin/hyprctl monitors -j 2>/dev/null \
|
||||
| ${pkgs.jq}/bin/jq -e --arg o "$want" 'any(.[]; .name == $o)' >/dev/null 2>&1; then
|
||||
printf '[SELECTION]r/screen:%s\n' "$want"
|
||||
else
|
||||
exec "$realPicker" "$@"
|
||||
fi
|
||||
'';
|
||||
|
||||
# Floating webcam preview for the "corner-cam" recording trick: run `webcam`,
|
||||
# then capture the whole screen with kooha/wf-recorder — the preview is in the
|
||||
# recording. v4l2 only (won't work with the laptop's IPU6 cam). Optional device
|
||||
@@ -32,11 +72,58 @@ in
|
||||
default = 24;
|
||||
description = "Cursor size for XCURSOR_SIZE and HYPRCURSOR_SIZE";
|
||||
};
|
||||
cursorPackage = lib.mkOption {
|
||||
type = lib.types.nullOr lib.types.package;
|
||||
default = null;
|
||||
description = ''
|
||||
Package providing cursorTheme.dark/light under share/icons. null uses
|
||||
the nordzy-cursors package built in ./cursor.nix.
|
||||
'';
|
||||
};
|
||||
cursorTheme = {
|
||||
dark = lib.mkOption {
|
||||
type = lib.types.str;
|
||||
default = "Nordzy-cursors";
|
||||
description = "Cursor theme darkman selects in dark mode.";
|
||||
};
|
||||
light = lib.mkOption {
|
||||
type = lib.types.str;
|
||||
default = "Nordzy-white";
|
||||
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;
|
||||
example = "HDMI-A-1";
|
||||
description = ''
|
||||
Output that screen-sharing auto-selects, bypassing the xdph picker
|
||||
entirely. null keeps the normal picker. Falls back to the picker if
|
||||
the named output isn't currently connected.
|
||||
'';
|
||||
};
|
||||
};
|
||||
|
||||
imports = [
|
||||
./cursor.nix
|
||||
./noctalia
|
||||
./eww
|
||||
./kbptr.nix
|
||||
./shell.nix
|
||||
./wallpapers.nix
|
||||
];
|
||||
|
||||
@@ -62,272 +149,251 @@ in
|
||||
WEBKIT_DISABLE_DMABUF_RENDERER = "1";
|
||||
};
|
||||
|
||||
# xdph's share-picker is stateless: the "Allow a restore token" checkbox
|
||||
# starts unticked, so no app ever receives a token and every single share
|
||||
# re-prompts (journal showed 0 "Sent restore token" in 30 days, always
|
||||
# re-selecting the same output). This pre-ticks it, so OBS/Chrome/Slack get
|
||||
# a token on first share and skip the dialog thereafter.
|
||||
# Note: xdph.conf is still hyprlang — the 0.55 Lua switch only hit the
|
||||
# compositor config, not this separate binary.
|
||||
xdg.configFile."hypr/xdph.conf".text = ''
|
||||
screencopy {
|
||||
allow_token_by_default = true
|
||||
${lib.optionalString (
|
||||
cfg.shareOutput != null
|
||||
) " custom_picker_binary = ${sharePicker}/bin/xdph-auto-picker"}
|
||||
}
|
||||
'';
|
||||
|
||||
wayland.windowManager.hyprland = {
|
||||
enable = true;
|
||||
package = null;
|
||||
portalPackage = null;
|
||||
# Keep the hyprlang config format (the new default is "lua"); our
|
||||
# settings are written as hyprlang.
|
||||
configType = "hyprlang";
|
||||
# Hyprland 0.55+ removed the hyprlang parser entirely — the config format
|
||||
# is now Lua only (see home/common/programs/hyprland — the whole config
|
||||
# below is raw Lua using the hl.* API). home-manager still emits its own
|
||||
# systemd-session start hook, so we don't duplicate that here.
|
||||
configType = "lua";
|
||||
plugins = [ ];
|
||||
settings = {
|
||||
plugin = {
|
||||
overview = {
|
||||
autoDrag = true;
|
||||
exitOnClick = true;
|
||||
exitOnSwitch = true;
|
||||
showNewWorkspace = false;
|
||||
showEmptyWorkspace = false;
|
||||
};
|
||||
};
|
||||
input = {
|
||||
kb_options = "ctrl:nocaps";
|
||||
repeat_delay = 150;
|
||||
extraConfig = ''
|
||||
local mod = "SUPER"
|
||||
|
||||
touchpad = {
|
||||
natural_scroll = "yes";
|
||||
};
|
||||
numlock_by_default = true;
|
||||
};
|
||||
-- look & feel / behaviour
|
||||
hl.config({
|
||||
general = {
|
||||
border_size = 1,
|
||||
gaps_in = 0,
|
||||
gaps_out = 0,
|
||||
snap = {
|
||||
enabled = true,
|
||||
border_overlap = true,
|
||||
},
|
||||
col = {
|
||||
inactive_border = "0x99999999",
|
||||
active_border = "0x99999999",
|
||||
},
|
||||
},
|
||||
|
||||
cursor = {
|
||||
no_hardware_cursors = "yes";
|
||||
# Auto-hide the pointer after 3s of no movement so a parked cursor
|
||||
# doesn't sit on top of a game (visible in the stream too). It
|
||||
# reappears the instant the mouse moves.
|
||||
inactive_timeout = 3;
|
||||
};
|
||||
input = {
|
||||
kb_options = "ctrl:nocaps",
|
||||
repeat_delay = 150,
|
||||
touchpad = {
|
||||
natural_scroll = true,
|
||||
},
|
||||
numlock_by_default = true,
|
||||
},
|
||||
|
||||
general = {
|
||||
border_size = 1;
|
||||
gaps_in = 0;
|
||||
gaps_out = 0;
|
||||
snap = {
|
||||
enabled = true;
|
||||
border_overlap = true;
|
||||
};
|
||||
cursor = {
|
||||
no_hardware_cursors = true,
|
||||
-- Auto-hide the pointer after 3s of no movement so a parked cursor
|
||||
-- doesn't sit on top of a game (visible in the stream too). It
|
||||
-- reappears the instant the mouse moves.
|
||||
inactive_timeout = 3,
|
||||
},
|
||||
|
||||
"col.inactive_border" = "0x99999999";
|
||||
"col.active_border" = "0x99999999";
|
||||
};
|
||||
misc = {
|
||||
disable_hyprland_logo = true,
|
||||
disable_splash_rendering = true,
|
||||
on_focus_under_fullscreen = 2,
|
||||
},
|
||||
|
||||
misc = {
|
||||
disable_hyprland_logo = true;
|
||||
disable_splash_rendering = true;
|
||||
on_focus_under_fullscreen = 2;
|
||||
};
|
||||
ecosystem = {
|
||||
no_update_news = true,
|
||||
},
|
||||
|
||||
ecosystem = {
|
||||
no_update_news = true;
|
||||
};
|
||||
xwayland = {
|
||||
force_zero_scaling = true,
|
||||
},
|
||||
|
||||
xwayland = {
|
||||
force_zero_scaling = true;
|
||||
};
|
||||
decoration = {
|
||||
blur = {
|
||||
enabled = true,
|
||||
popups = false,
|
||||
},
|
||||
shadow = {
|
||||
enabled = false,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
decoration = {
|
||||
blur = {
|
||||
enabled = true;
|
||||
popups = false;
|
||||
};
|
||||
shadow = {
|
||||
enabled = false;
|
||||
};
|
||||
};
|
||||
-- keep default animations, but speed them all up
|
||||
hl.animation({ leaf = "global", enabled = true, speed = 2, bezier = "default" })
|
||||
|
||||
# keep default animations, but speed them all up
|
||||
animations = {
|
||||
animation = [
|
||||
"global, 1, 2, default"
|
||||
];
|
||||
};
|
||||
-- environment
|
||||
hl.env("GDK_SCALE", "2.0")
|
||||
hl.env("XCURSOR_SIZE", "${toString cfg.cursorSize}")
|
||||
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.
|
||||
hl.env("QT_QPA_PLATFORMTHEME", "qt6ct")
|
||||
|
||||
env = [
|
||||
"GDK_SCALE, 2.0"
|
||||
"XCURSOR_SIZE, ${toString cfg.cursorSize}"
|
||||
"HYPRCURSOR_THEME, rose-pine-hyprcursor"
|
||||
"HYPRCURSOR_SIZE, ${toString cfg.cursorSize}"
|
||||
# Route Qt6 apps (incl. the xdph screen-share picker) through qt6ct so
|
||||
# the noctalia-generated color scheme actually applies. Without this
|
||||
# var the qt6ct.conf is never read.
|
||||
"QT_QPA_PLATFORMTHEME, qt6ct"
|
||||
];
|
||||
-- autostart
|
||||
hl.on("hyprland.start", function()
|
||||
-- Import the Qt theme var too, so the dbus/systemd-activated
|
||||
-- xdg-desktop-portal-hyprland (and its share-picker) inherit it.
|
||||
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.
|
||||
-- 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)
|
||||
|
||||
"exec-once" = [
|
||||
# Import the Qt theme var too, so the dbus/systemd-activated
|
||||
# xdg-desktop-portal-hyprland (and its share-picker) inherit it.
|
||||
"dbus-update-activation-environment --systemd WAYLAND_DISPLAY XDG_CURRENT_DESKTOP QT_QPA_PLATFORMTHEME"
|
||||
"hyprctl setcursor Nordzy-cursors ${toString cfg.cursorSize}"
|
||||
"hyprsunset"
|
||||
"noctalia"
|
||||
"yogurt --tray"
|
||||
];
|
||||
-- layer rules
|
||||
hl.layer_rule({ match = { namespace = "eww-panel" }, blur = true, ignore_alpha = 0.5 })
|
||||
|
||||
layerrule = [
|
||||
"blur 1, match:namespace noctalia-wallpaper"
|
||||
"ignore_alpha 0.5, match:namespace noctalia-wallpaper"
|
||||
];
|
||||
workspace = [
|
||||
# no gaps when only window
|
||||
"w[t1], gapsout:0, gapsin:0"
|
||||
"w[tg1], gapsout:0, gapsin:0"
|
||||
"f[1], gapsout:0, gapsin:0"
|
||||
-- workspace rules
|
||||
hl.workspace_rule({ workspace = "w[t1]", gaps_out = 0, gaps_in = 0 }) -- no gaps when only window
|
||||
hl.workspace_rule({ workspace = "w[tg1]", gaps_out = 0, gaps_in = 0 })
|
||||
hl.workspace_rule({ workspace = "f[1]", gaps_out = 0, gaps_in = 0 })
|
||||
hl.workspace_rule({ workspace = "special:terminal", on_created_empty = "[float; size 1400 1000; center 1] kitty", persistent = false })
|
||||
hl.workspace_rule({ workspace = "special:yazi", on_created_empty = "[float; size ${cfg.yaziSize}; center 1] kitty --session sessions/yazi", persistent = false })
|
||||
|
||||
"special:terminal, on-created-empty:[float; size 1400 1000; center 1] kitty, persistent:false"
|
||||
"special:yazi, on-created-empty:[float; size ${cfg.yaziSize}; center 1] kitty --session sessions/yazi, persistent:false"
|
||||
];
|
||||
-- window rules
|
||||
hl.window_rule({ match = { class = "com.gabm.satty" }, float = true })
|
||||
|
||||
windowrule = [
|
||||
# satty
|
||||
"float on, match:class com.gabm.satty"
|
||||
hl.window_rule({ match = { class = "imv" }, float = true, size = "1920 1080", move = "(cursor_x-(window_w*0.5)) (cursor_y-(window_h*0.5))" })
|
||||
hl.window_rule({ match = { class = "mpv" }, float = true, size = "1920 1080", move = "(cursor_x-(window_w*0.5)) (cursor_y-(window_h*0.5))" })
|
||||
|
||||
# imv
|
||||
"float on, match:class imv"
|
||||
"size 1920 1080, match:class imv"
|
||||
"move (cursor_x-(window_w*0.5)) (cursor_y-(window_h*0.5)), match:class imv"
|
||||
-- webcam overlay (`webcam` command) — small, pinned, bottom-right.
|
||||
-- Declared after the mpv class rule so its title match wins.
|
||||
hl.window_rule({
|
||||
match = { title = "webcam-overlay" },
|
||||
float = true,
|
||||
size = "360 240",
|
||||
move = "100%-380 100%-260",
|
||||
pin = true,
|
||||
no_initial_focus = true,
|
||||
border_size = 0,
|
||||
})
|
||||
|
||||
# mpv
|
||||
"float on, match:class mpv"
|
||||
"size 1920 1080, match:class mpv"
|
||||
"move (cursor_x-(window_w*0.5)) (cursor_y-(window_h*0.5)), match:class mpv"
|
||||
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 = [[org\.pulseaudio\.pavucontrol]] }, float = true, size = "1200 1200", move = "(cursor_x-(window_w*0.5)) (cursor_y-(window_h*0.5))" })
|
||||
|
||||
# webcam overlay (`webcam` command) — small, pinned, bottom-right.
|
||||
# Title rules come after the mpv class rules so they win.
|
||||
"float on, match:title webcam-overlay"
|
||||
"size 360 240, match:title webcam-overlay"
|
||||
"move 100%-380 100%-260, match:title webcam-overlay"
|
||||
"pin on, match:title webcam-overlay"
|
||||
"no_initial_focus on, match:title webcam-overlay"
|
||||
"border_size 0, match:title webcam-overlay"
|
||||
-- wine / game installers (Inno Setup temp windows, e.g. Heroic/GOG).
|
||||
-- Their class is the random "setup_*.tmp" filename, so match by suffix.
|
||||
hl.window_rule({ match = { class = [[.*\.tmp]] }, float = true, center = true })
|
||||
|
||||
# thunar
|
||||
"float on, match:class thunar"
|
||||
"size 1800 1200, match:class thunar"
|
||||
"move (cursor_x-(window_w*0.5)) (cursor_y-(window_h*0.5)), match:class thunar"
|
||||
-- metamask
|
||||
hl.window_rule({ match = { class = [[chrome-nkbihfbeogaeaoehlefnkodbefgpgknn-.*]] }, float = true })
|
||||
-- bitwarden, chrome
|
||||
hl.window_rule({ match = { class = [[chrome-nngceckbapebfimnlniiiahkandclblb-.*]] }, float = true })
|
||||
-- claude for chrome
|
||||
hl.window_rule({ match = { class = [[chrome-fcoeoabgfenejglbffodgkkbkcdhcgfn-.*]] }, float = true, move = "(cursor_x-(window_w*0.5)) (cursor_y-(window_h*0.5))" })
|
||||
|
||||
# bluetooth
|
||||
"float on, match:class \\.blueman-manager-wrapped"
|
||||
"size 1200 800, match:class \\.blueman-manager-wrapped"
|
||||
"move (cursor_x-(window_w*0.5)) (cursor_y-(window_h*0.5)), match:class \\.blueman-manager-wrapped"
|
||||
-- no gaps when only window
|
||||
hl.window_rule({ match = { float = false, workspace = "w[t1]" }, border_size = 0, rounding = 0 })
|
||||
hl.window_rule({ match = { float = false, workspace = "w[tg1]" }, border_size = 0, rounding = 0 })
|
||||
hl.window_rule({ match = { float = false, workspace = "f[1]" }, border_size = 0, rounding = 0 })
|
||||
|
||||
# pavucontrol
|
||||
"float on, match:class org\\.pulseaudio\\.pavucontrol"
|
||||
"size 1200 1200, match:class org\\.pulseaudio\\.pavucontrol"
|
||||
"move (cursor_x-(window_w*0.5)) (cursor_y-(window_h*0.5)), match:class org\\.pulseaudio\\.pavucontrol"
|
||||
hl.window_rule({ match = { workspace = "special:terminal" }, center = true })
|
||||
hl.window_rule({ match = { workspace = "special:yazi" }, center = true })
|
||||
|
||||
# wine / game installers (Inno Setup temp windows, e.g. Heroic/GOG).
|
||||
# Their class is the random "setup_*.tmp" filename, so match by suffix.
|
||||
"float on, match:class .*\\.tmp"
|
||||
"center 1, match:class .*\\.tmp"
|
||||
-- keybinds
|
||||
hl.bind(mod .. " + N", hl.dsp.exec_cmd("eww-panel"))
|
||||
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())
|
||||
hl.bind(mod .. " + F", hl.dsp.window.fullscreen({ mode = "fullscreen" }))
|
||||
hl.bind(mod .. " + SHIFT + F", hl.dsp.window.fullscreen({ mode = "maximized" }))
|
||||
|
||||
# metamask
|
||||
"float on, match:class chrome-nkbihfbeogaeaoehlefnkodbefgpgknn-.*"
|
||||
-- lock (routes through logind -> hypridle lock_cmd -> hyprlock)
|
||||
hl.bind(mod .. " + CTRL + L", hl.dsp.exec_cmd("loginctl lock-session"))
|
||||
|
||||
# bitwarden, chrome
|
||||
"float on, match:class chrome-nngceckbapebfimnlniiiahkandclblb-.*"
|
||||
hl.bind(mod .. " + space", hl.dsp.exec_cmd("fuzzel"))
|
||||
|
||||
# claude for chrome
|
||||
"float on, match:class chrome-fcoeoabgfenejglbffodgkkbkcdhcgfn-.*"
|
||||
"move (cursor_x-(window_w*0.5)) (cursor_y-(window_h*0.5)), match:class chrome-fcoeoabgfenejglbffodgkkbkcdhcgfn-.*"
|
||||
-- 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"))
|
||||
hl.bind("SHIFT + Print", hl.dsp.exec_cmd("hyprshot -m window --raw | satty --filename - --output-filename ~/downloads/screenshots/$(date +%Y-%m-%d_%H-%M-%S).png"))
|
||||
|
||||
# no gaps when only window
|
||||
"border_size 0, match:float 0, match:workspace w[t1]"
|
||||
"rounding 0, match:float 0, match:workspace w[t1]"
|
||||
"border_size 0, match:float 0, match:workspace w[tg1]"
|
||||
"rounding 0, match:float 0, match:workspace w[tg1]"
|
||||
"border_size 0, match:float 0, match:workspace f[1]"
|
||||
"rounding 0, match:float 0, match:workspace f[1]"
|
||||
-- move focus with mod + hjkl
|
||||
hl.bind(mod .. " + H", hl.dsp.focus({ direction = "left" }))
|
||||
hl.bind(mod .. " + J", hl.dsp.focus({ direction = "down" }))
|
||||
hl.bind(mod .. " + K", hl.dsp.focus({ direction = "up" }))
|
||||
hl.bind(mod .. " + L", hl.dsp.focus({ direction = "right" }))
|
||||
|
||||
"center 1, match:workspace special:terminal"
|
||||
"center 1, match:workspace special:yazi"
|
||||
];
|
||||
-- switch / move-to workspaces 1-6
|
||||
for i = 1, 6 do
|
||||
hl.bind(mod .. " + " .. i, hl.dsp.focus({ workspace = i }))
|
||||
hl.bind(mod .. " + SHIFT + " .. i, hl.dsp.window.move({ workspace = i }))
|
||||
end
|
||||
|
||||
"$mod" = "SUPER";
|
||||
-- move active window inside workspace
|
||||
hl.bind(mod .. " + SHIFT + H", hl.dsp.window.move({ direction = "left" }))
|
||||
hl.bind(mod .. " + SHIFT + J", hl.dsp.window.move({ direction = "down" }))
|
||||
hl.bind(mod .. " + SHIFT + K", hl.dsp.window.move({ direction = "up" }))
|
||||
hl.bind(mod .. " + SHIFT + L", hl.dsp.window.move({ direction = "right" }))
|
||||
|
||||
bind = [
|
||||
"$mod, n, exec, noctalia msg panel-toggle control-center"
|
||||
"$mod, t, exec, kitty"
|
||||
"$mod, v, togglefloating"
|
||||
"$mod, q, killactive"
|
||||
"$mod, f, fullscreen, 0"
|
||||
"$mod SHIFT, f, fullscreen, 1"
|
||||
-- scroll through workspaces with mod + scroll
|
||||
hl.bind(mod .. " + mouse_down", hl.dsp.focus({ workspace = "e+1" }))
|
||||
hl.bind(mod .. " + mouse_up", hl.dsp.focus({ workspace = "e-1" }))
|
||||
|
||||
# lock (routes through logind -> hypridle lock_cmd -> hyprlock)
|
||||
"$mod CTRL, l, exec, loginctl lock-session"
|
||||
-- special workspaces (toggle + recenter in one handler)
|
||||
hl.bind(mod .. " + X", function()
|
||||
hl.dispatch(hl.dsp.workspace.toggle_special("terminal"))
|
||||
hl.dispatch(hl.dsp.window.center())
|
||||
end)
|
||||
hl.bind(mod .. " + E", function()
|
||||
hl.dispatch(hl.dsp.workspace.toggle_special("yazi"))
|
||||
hl.dispatch(hl.dsp.window.center())
|
||||
end)
|
||||
|
||||
"$mod, space, exec, noctalia msg panel-toggle launcher"
|
||||
-- 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 })
|
||||
|
||||
# printscreen
|
||||
", Print, exec, hyprshot -m region --raw | satty --filename - --output-filename ~/downloads/screenshots/$(date +%Y-%m-%d_%H-%M-%S).png"
|
||||
"SHIFT, Print, exec, hyprshot -m window --raw | satty --filename - --output-filename ~/downloads/screenshots/$(date +%Y-%m-%d_%H-%M-%S).png"
|
||||
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 })
|
||||
|
||||
# move focus with mod + arrows
|
||||
"$mod, h, movefocus, l"
|
||||
"$mod, j, movefocus, d"
|
||||
"$mod, k, movefocus, u"
|
||||
"$mod, l, movefocus, r"
|
||||
|
||||
# Switch workspaces with mod + [0-9]
|
||||
"$mod, 1, workspace, 1"
|
||||
"$mod, 2, workspace, 2"
|
||||
"$mod, 3, workspace, 3"
|
||||
"$mod, 4, workspace, 4"
|
||||
"$mod, 5, workspace, 5"
|
||||
"$mod, 6, workspace, 6"
|
||||
|
||||
# Move active window to a workspace with mod + SHIFT + [0-9]
|
||||
"$mod SHIFT, 1, movetoworkspace, 1"
|
||||
"$mod SHIFT, 2, movetoworkspace, 2"
|
||||
"$mod SHIFT, 3, movetoworkspace, 3"
|
||||
"$mod SHIFT, 4, movetoworkspace, 4"
|
||||
"$mod SHIFT, 5, movetoworkspace, 5"
|
||||
"$mod SHIFT, 6, movetoworkspace, 6"
|
||||
|
||||
# Move active window inside workspace
|
||||
"$mod SHIFT, h, movewindow, l"
|
||||
"$mod SHIFT, j, movewindow, d"
|
||||
"$mod SHIFT, k, movewindow, u"
|
||||
"$mod SHIFT, l, movewindow, r"
|
||||
|
||||
# Scroll through existing workspaces with mod + scroll
|
||||
"$mod, mouse_down, workspace, e+1"
|
||||
"$mod, mouse_up, workspace, e-1"
|
||||
|
||||
# special workspaces
|
||||
"$mod, x, togglespecialworkspace, terminal"
|
||||
"$mod, x, centerwindow"
|
||||
"$mod, e, togglespecialworkspace, yazi"
|
||||
"$mod, e, centerwindow"
|
||||
];
|
||||
|
||||
bindel = [
|
||||
", XF86AudioRaiseVolume, exec, wpctl set-volume @DEFAULT_AUDIO_SINK@ 5%+"
|
||||
", XF86AudioLowerVolume, exec, wpctl set-volume @DEFAULT_AUDIO_SINK@ 5%-"
|
||||
", XF86MonBrightnessUp, exec, brightnessctl set 5%+"
|
||||
", XF86MonBrightnessDown, exec, brightnessctl set 5%-"
|
||||
];
|
||||
bindl = [
|
||||
", XF86AudioMute, exec, pactl set-sink-mute @DEFAULT_SINK@ toggle"
|
||||
", XF86AudioMicMute, exec, volumectl -m toggle-mute"
|
||||
", XF86AudioNext, exec, playerctl next"
|
||||
", XF86AudioPrev, exec, playerctl previous"
|
||||
", XF86AudioPlay, exec, playerctl play-pause"
|
||||
];
|
||||
|
||||
bindm = [
|
||||
# Move/resize windows with mod + LMB/RMB and dragging
|
||||
"$mod, mouse:272, movewindow"
|
||||
"$mod, mouse:273, resizewindow"
|
||||
];
|
||||
};
|
||||
-- move/resize windows with mod + LMB/RMB drag
|
||||
hl.bind(mod .. " + mouse:272", hl.dsp.window.drag(), { mouse = true })
|
||||
hl.bind(mod .. " + mouse:273", hl.dsp.window.resize(), { mouse = true })
|
||||
'';
|
||||
};
|
||||
|
||||
services.hyprpaper.enable = false;
|
||||
|
||||
# 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.
|
||||
# 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.
|
||||
programs.hyprlock = {
|
||||
enable = true;
|
||||
settings = {
|
||||
|
||||
@@ -0,0 +1,292 @@
|
||||
{
|
||||
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;
|
||||
}
|
||||
}
|
||||
'';
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
# 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 }'
|
||||
@@ -0,0 +1,32 @@
|
||||
# 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 }'
|
||||
@@ -0,0 +1,22 @@
|
||||
# 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
|
||||
@@ -0,0 +1,11 @@
|
||||
# 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"
|
||||
@@ -0,0 +1,43 @@
|
||||
# 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,115 @@
|
||||
{ pkgs, ... }:
|
||||
let
|
||||
# wl-kbptr's OpenCV target detection (the `detect` source used by SUPER+G) is
|
||||
# hardcoded for a ~1080p screen and finds almost nothing on a 4K one.
|
||||
#
|
||||
# src/target_detection.cpp:264 discards any candidate target with
|
||||
# `height >= 50 || width >= 500`. Every monitor here is 4K at scale = 1, so an
|
||||
# ordinary button or list row is ~50px tall and well over 500px wide — i.e.
|
||||
# basically every real control is thrown away for being "too big", and the
|
||||
# only survivors are emoji and avatars. Verified on-screen: stock, a full
|
||||
# Slack window got ~8 labels, all on images; patched, every channel, tab and
|
||||
# button gets one.
|
||||
#
|
||||
# Doubling the four thresholds (and the dilate kernel that merges glyph edges
|
||||
# into word blobs) is exactly the 1080p->4K ratio. None of these are exposed
|
||||
# as config options in 0.4.1, so patching is the only way in. Drop this
|
||||
# override if upstream makes detection DPI-aware.
|
||||
# Second bug, on the rotated monitor (DP-2, transform = 1): the pointer lands
|
||||
# in the wrong place because the rotation is applied twice.
|
||||
#
|
||||
# src/utils_wayland.c move_pointer() takes output->width/height — which come
|
||||
# from `xdg_output.logical_size`, i.e. already rotated — and then runs
|
||||
# _apply_transform() over them again before motion_absolute(). Hyprland's
|
||||
# virtual-pointer also works in logical space, so the second rotation is pure
|
||||
# error. Measured: asking for global (4000,1000) on DP-2 landed at
|
||||
# (4286,3726); the double-rotation arithmetic predicts (4301,3736).
|
||||
#
|
||||
# Passing TRANSFORM_NORMAL makes the call a no-op while keeping the (static)
|
||||
# function referenced, so no unused-function warning. Only correct for
|
||||
# compositors that expect logical coords; fine here, Hyprland-only config.
|
||||
wl-kbptr = pkgs.wl-kbptr.overrideAttrs (old: {
|
||||
postPatch = (old.postPatch or "") + ''
|
||||
substituteInPlace src/utils_wayland.c \
|
||||
--replace-fail \
|
||||
'&x, &y, &output_width, &output_height, state->current_output->transform' \
|
||||
'&x, &y, &output_width, &output_height, WL_OUTPUT_TRANSFORM_NORMAL'
|
||||
substituteInPlace src/target_detection.cpp \
|
||||
--replace-fail \
|
||||
'rect.height >= 50 || rect.width >= 500 || rect.height <= 3 ||' \
|
||||
'rect.height >= 100 || rect.width >= 1000 || rect.height <= 6 ||' \
|
||||
--replace-fail 'rect.width <= 7) {' 'rect.width <= 14) {' \
|
||||
--replace-fail 'if (rect.height <= 6) {' 'if (rect.height <= 12) {' \
|
||||
--replace-fail 'round(2.5 * scale), round(3.5 * scale)' \
|
||||
'round(5.0 * scale), round(7.0 * scale)'
|
||||
'';
|
||||
});
|
||||
in
|
||||
{
|
||||
home.packages = [ wl-kbptr ];
|
||||
|
||||
# wl-kbptr — move/click the pointer with the keyboard. We reorder the label
|
||||
# alphabet so the easiest tiles land on the home row, set a monospace label
|
||||
# font so labels stay legible over busy backgrounds, and roughly double every
|
||||
# size default. Run `wl-kbptr --help-config` for the full option list.
|
||||
#
|
||||
# The size bump is not taste, it's DPI: every monitor here is 4K at
|
||||
# `scale = 1` (see home/*/monitors.nix), and wl-kbptr paints raw cairo onto a
|
||||
# layer surface, so it never sees a scale factor — GDK_SCALE only fixes GTK
|
||||
# apps. Upstream's defaults are tuned for ~1080p and render half-size here.
|
||||
#
|
||||
# `label_font_size` is `min proportion max`, evaluated as
|
||||
# clamp(area_height * proportion, min, max). In floating/detect mode the
|
||||
# detected areas are buttons and icons, i.e. short, so it's the *minimum* that
|
||||
# is in force for nearly every label — raising max alone would change nothing.
|
||||
xdg.configFile."wl-kbptr/config".text = ''
|
||||
[general]
|
||||
modes=tile,bisect
|
||||
|
||||
[mode_tile]
|
||||
label_symbols=asdfghjklqwertyuiopzxcvbnm
|
||||
label_font_family=monospace
|
||||
label_font_size=20 50% 120
|
||||
|
||||
[mode_floating]
|
||||
label_symbols=asdfghjklqwertyuiopzxcvbnm
|
||||
label_font_family=monospace
|
||||
label_font_size=28 45% 80
|
||||
|
||||
[mode_bisect]
|
||||
label_font_family=monospace
|
||||
label_font_size=32
|
||||
label_padding=20
|
||||
pointer_size=32
|
||||
|
||||
[mode_split]
|
||||
pointer_size=32
|
||||
|
||||
[mode_click]
|
||||
button=left
|
||||
'';
|
||||
|
||||
wayland.windowManager.hyprland.extraConfig = ''
|
||||
-- wl-kbptr: keyboard pointer control.
|
||||
--
|
||||
-- SUPER+G "vimium for the desktop" — OpenCV detects clickable regions
|
||||
-- from a screencopy frame and labels them; type a label and it
|
||||
-- moves the pointer to that region's centre and left-clicks.
|
||||
-- Needs the HiDPI patch in kbptr.nix to find anything here.
|
||||
-- SUPER+SHIFT+G tile -> bisect, for anywhere detection misses. Type the tile
|
||||
-- label, then bisect with the home row; g/h/b left/right/middle
|
||||
-- click, Enter/Space just parks the cursor, Backspace steps back.
|
||||
--
|
||||
-- Both need wlr-layer-shell + wlr-virtual-pointer (+ wlr-screencopy for
|
||||
-- detect), all of which Hyprland implements.
|
||||
-- spelled out rather than reusing the `mod` local from default.nix: both
|
||||
-- chunks land in the same hyprland.lua but their relative order isn't pinned.
|
||||
-- Guarded with `pidof` the same way hypridle's lock_cmd is. wl-kbptr only
|
||||
-- exits on a keypress (main.c sets running=false solely from the keyboard
|
||||
-- handler), so an overlay that loses focus before you type a label just sits
|
||||
-- there holding an exclusive keyboard grab until it's killed from a shell —
|
||||
-- which happened here. The guard stops repeat presses stacking more of them.
|
||||
hl.bind("SUPER + G", hl.dsp.exec_cmd("pidof wl-kbptr || wl-kbptr -o modes=floating,click -o mode_floating.source=detect"))
|
||||
hl.bind("SUPER + SHIFT + G", hl.dsp.exec_cmd("pidof wl-kbptr || wl-kbptr"))
|
||||
'';
|
||||
}
|
||||
@@ -1,108 +0,0 @@
|
||||
{
|
||||
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
|
||||
xdg.configFile."hypr/hyprland.conf".force = true;
|
||||
xdg.configFile."gtk-3.0/gtk.css".force = true;
|
||||
|
||||
# source noctalia-generated color theme in hyprland (v5 path).
|
||||
# Hyprland globs every source path, so a missing file errors as
|
||||
# "globbing error: found no match" — pre-create an empty placeholder
|
||||
# so the first boot doesn't fail before noctalia has run.
|
||||
home.activation.ensureNoctaliaHyprConf = lib.hm.dag.entryAfter [ "writeBoundary" ] ''
|
||||
run mkdir -p "$HOME/.config/hypr"
|
||||
run touch "$HOME/.config/hypr/noctalia.conf"
|
||||
'';
|
||||
|
||||
wayland.windowManager.hyprland.settings.source = [
|
||||
"~/.config/hypr/noctalia.conf"
|
||||
];
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
{ 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,5 +1,10 @@
|
||||
{ pkgs, ... }:
|
||||
{ config, 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 = [
|
||||
@@ -107,4 +112,25 @@ 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,27 +1,108 @@
|
||||
{ pkgs, ... }:
|
||||
let
|
||||
clip2path = pkgs.writeShellScript "clip2path" ''
|
||||
set -e
|
||||
set -euo pipefail
|
||||
|
||||
if [ -n "$WAYLAND_DISPLAY" ]; then
|
||||
types=$(wl-paste --list-types)
|
||||
if grep -q '^image/' <<<"$types"; then
|
||||
ext=$(grep -m1 '^image/' <<<"$types" | cut -d/ -f2 | cut -d';' -f1)
|
||||
file="/tmp/clip_$(date +%s).''${ext}"
|
||||
wl-paste --type "image/''${ext}" > "$file"
|
||||
printf '%q' "$file" | kitty @ send-text --stdin
|
||||
jq=${pkgs.jq}/bin/jq
|
||||
scp=${pkgs.openssh}/bin/scp
|
||||
wlpaste=${pkgs.wl-clipboard}/bin/wl-paste
|
||||
xclip=${pkgs.xclip}/bin/xclip
|
||||
find=${pkgs.findutils}/bin/find
|
||||
|
||||
# kitty/kitten stay unpinned: remote control has to reach the running
|
||||
# instance, whose build may differ from pkgs.kitty.
|
||||
|
||||
"$find" /tmp -maxdepth 1 -name 'clip_*' -mtime +1 -delete 2>/dev/null || true
|
||||
|
||||
# Some apps advertise a lossy format before image/png.
|
||||
pick_type() {
|
||||
if grep -qx 'image/png' <<<"$1"; then
|
||||
echo image/png
|
||||
else
|
||||
wl-paste --no-newline | kitty @ send-text --stdin
|
||||
grep -m1 '^image/' <<<"$1" | cut -d';' -f1
|
||||
fi
|
||||
elif [ -n "$DISPLAY" ]; then
|
||||
types=$(xclip -selection clipboard -t TARGETS -o)
|
||||
if grep -q '^image/' <<<"$types"; then
|
||||
ext=$(grep -m1 '^image/' <<<"$types" | cut -d/ -f2 | cut -d';' -f1)
|
||||
file="/tmp/clip_$(date +%s).''${ext}"
|
||||
xclip -selection clipboard -t "image/''${ext}" -o > "$file"
|
||||
printf '%q' "$file" | kitty @ send-text --stdin
|
||||
}
|
||||
|
||||
ext_of() {
|
||||
local e=''${1#image/}
|
||||
e=''${e%%+*}
|
||||
e=''${e#x-}
|
||||
tr -cd 'a-zA-Z0-9' <<<"$e"
|
||||
}
|
||||
|
||||
# Destination of the ssh client running in the focused window, empty if
|
||||
# that window is local. Prefers the real ssh process over the `kitten ssh`
|
||||
# wrapper, whose own long options would confuse the parser below.
|
||||
ssh_dest() {
|
||||
local -a argv
|
||||
mapfile -t argv < <(kitty @ ls | "$jq" -r '
|
||||
[ .[] | select(.is_focused)
|
||||
| .tabs[] | select(.is_focused)
|
||||
| .windows[] | select(.is_focused)
|
||||
| .foreground_processes[]?
|
||||
| { b: ((.cmdline[0] // "") | split("/") | last), c: .cmdline }
|
||||
] as $p
|
||||
| ( [ $p[] | select(.b == "ssh") ] + [ $p[] | select(.b == "kitten") ] )
|
||||
| (.[0].c // [])[]')
|
||||
|
||||
[ ''${#argv[@]} -gt 0 ] || return 0
|
||||
if [ "''${argv[0]##*/}" = kitten ]; then
|
||||
[ "''${argv[1]:-}" = ssh ] || return 0
|
||||
argv=("''${argv[@]:2}")
|
||||
else
|
||||
xclip -selection clipboard -o | kitty @ send-text --stdin
|
||||
argv=("''${argv[@]:1}")
|
||||
fi
|
||||
|
||||
local valueflags=bcDEeFIiJLlmOopQRSWw a
|
||||
while [ ''${#argv[@]} -gt 0 ]; do
|
||||
a=''${argv[0]}
|
||||
case "$a" in
|
||||
--) echo "''${argv[1]:-}"; return 0 ;;
|
||||
-?)
|
||||
case "$valueflags" in *"''${a#-}"*) argv=("''${argv[@]:1}") ;; esac
|
||||
argv=("''${argv[@]:1}")
|
||||
;;
|
||||
-*) argv=("''${argv[@]:1}") ;;
|
||||
*) echo "$a"; return 0 ;;
|
||||
esac
|
||||
done
|
||||
}
|
||||
|
||||
send_path() {
|
||||
local file=$1 dest remote
|
||||
dest=$(ssh_dest)
|
||||
if [ -n "$dest" ]; then
|
||||
remote=/tmp/''${file##*/}
|
||||
if ! "$scp" -q -o BatchMode=yes -o ConnectTimeout=5 "$file" "$dest:$remote"; then
|
||||
kitten notify --app-name clip2path clip2path "copy to $dest failed, nothing pasted"
|
||||
return 1
|
||||
fi
|
||||
file=$remote
|
||||
fi
|
||||
printf '%q' "$file" | kitty @ send-text --stdin
|
||||
}
|
||||
|
||||
# Text goes through the real paste action. send-text has no bracketed
|
||||
# paste, so multi-line text would run line by line in a shell.
|
||||
if [ -n "''${WAYLAND_DISPLAY:-}" ]; then
|
||||
types=$("$wlpaste" --list-types)
|
||||
if grep -q '^image/' <<<"$types"; then
|
||||
type=$(pick_type "$types")
|
||||
file=$(mktemp --suffix=".$(ext_of "$type")" /tmp/clip_XXXXXXXX)
|
||||
"$wlpaste" --type "$type" > "$file"
|
||||
send_path "$file"
|
||||
else
|
||||
kitty @ action paste_from_clipboard
|
||||
fi
|
||||
elif [ -n "''${DISPLAY:-}" ]; then
|
||||
types=$("$xclip" -selection clipboard -t TARGETS -o)
|
||||
if grep -q '^image/' <<<"$types"; then
|
||||
type=$(pick_type "$types")
|
||||
file=$(mktemp --suffix=".$(ext_of "$type")" /tmp/clip_XXXXXXXX)
|
||||
"$xclip" -selection clipboard -t "$type" -o > "$file"
|
||||
send_path "$file"
|
||||
else
|
||||
kitty @ action paste_from_clipboard
|
||||
fi
|
||||
fi
|
||||
'';
|
||||
@@ -36,7 +117,7 @@ in
|
||||
{
|
||||
programs.kitty = {
|
||||
enable = true;
|
||||
# Fixed theme — noctalia no longer toggles kitty with dark/light mode.
|
||||
# Fixed theme — nothing toggles kitty with dark/light mode.
|
||||
themeFile = "Catppuccin-Mocha";
|
||||
settings = {
|
||||
confirm_os_window_close = 0;
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
{ 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 ];
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
{
|
||||
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,6 +1,6 @@
|
||||
{ inputs, pkgs, ... }:
|
||||
{
|
||||
imports = [ inputs.nvchad4nix.homeManagerModule ];
|
||||
imports = [ inputs.nvchad4nix.homeManagerModules.nvchad ];
|
||||
home.sessionVariables = {
|
||||
EDITOR = "nvim";
|
||||
};
|
||||
@@ -12,5 +12,14 @@
|
||||
|
||||
programs.nvchad = {
|
||||
enable = true;
|
||||
|
||||
# Neovim only auto-picks OSC 52 when it finds no clipboard tool, but
|
||||
# wl-clipboard is on every host — so over SSH it writes the *remote*
|
||||
# clipboard instead. Locally wl-copy stays: kitty refuses OSC 52 reads.
|
||||
extraConfig = ''
|
||||
if vim.env.SSH_TTY and vim.env.SSH_TTY ~= "" then
|
||||
vim.g.clipboard = "osc52"
|
||||
end
|
||||
'';
|
||||
};
|
||||
}
|
||||
|
||||
@@ -0,0 +1,198 @@
|
||||
{
|
||||
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" ];
|
||||
};
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
"""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,4 +1,9 @@
|
||||
{ pkgs, inputs, ... }:
|
||||
{
|
||||
lib,
|
||||
pkgs,
|
||||
inputs,
|
||||
...
|
||||
}:
|
||||
{
|
||||
imports = [
|
||||
# Prebuilt, weekly-updated nix-index database (backs `comma`).
|
||||
@@ -13,7 +18,9 @@
|
||||
# `flake` sets NH_FLAKE so `nh home switch` finds this repo without --flake.
|
||||
programs.nh = {
|
||||
enable = true;
|
||||
flake = "/home/naps62/projects/nixos-config";
|
||||
# mkDefault: hosts whose clone lives elsewhere (yolo, under ~/tea) override
|
||||
# this with a plain assignment.
|
||||
flake = lib.mkDefault "/home/naps62/projects/nixos-config";
|
||||
};
|
||||
|
||||
home.packages = with pkgs; [
|
||||
|
||||
@@ -2,5 +2,9 @@
|
||||
{
|
||||
home.packages = with pkgs; [
|
||||
typescript-language-server
|
||||
# nodejs_26, not the nixpkgs default 24: rev's package.json sets
|
||||
# engines >=26, and its systemd unit pins the same major.
|
||||
nodejs_26
|
||||
pnpm
|
||||
];
|
||||
}
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
Screenshots: stored in ~/downloads/screenshots, with date time in the filename
|
||||
@@ -1,100 +0,0 @@
|
||||
---
|
||||
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`
|
||||
@@ -1,9 +0,0 @@
|
||||
---
|
||||
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)
|
||||
@@ -1,12 +1,13 @@
|
||||
_:
|
||||
{
|
||||
# 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;
|
||||
@@ -16,10 +17,8 @@ _:
|
||||
"opencode/agents/designer-bold.md".source = ./agents/designer-bold.md;
|
||||
"opencode/agents/analyze-branch.md".source = ./agents/analyze-branch.md;
|
||||
|
||||
# Global commands
|
||||
# Global commands (opencode-specific ones only)
|
||||
"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;
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
{ 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 ];
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
{ 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 ];
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
{
|
||||
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;
|
||||
}
|
||||
@@ -0,0 +1,152 @@
|
||||
# Set prefix to Ctrl-a (more ergonomic than Ctrl-b)
|
||||
set -g prefix C-a
|
||||
unbind C-b
|
||||
bind C-a send-prefix
|
||||
|
||||
# Enable mouse support
|
||||
set -g mouse on
|
||||
|
||||
# Start windows and panes at 1, not 0
|
||||
set -g base-index 1
|
||||
setw -g pane-base-index 1
|
||||
|
||||
# Renumber windows when one is closed
|
||||
set -g renumber-windows on
|
||||
|
||||
# Increase scrollback buffer
|
||||
set -g history-limit 50000
|
||||
|
||||
# Low escape-time (0 breaks Alt keys over SSH; 25ms is a good balance)
|
||||
set -s escape-time 25
|
||||
|
||||
# Enable 256 colors
|
||||
set -g default-terminal "tmux-256color"
|
||||
set -ag terminal-overrides ",xterm-256color:RGB"
|
||||
set -ag terminal-overrides ",*256col*:Tc"
|
||||
|
||||
# smcup@/rmcup@ drops the alternate screen, so what tmux drew stays in the
|
||||
# outer terminal's scrollback instead of vanishing when tmux exits.
|
||||
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"
|
||||
|
||||
# -------------------------------------------------------------------
|
||||
# Status bar theme
|
||||
# -------------------------------------------------------------------
|
||||
set -g status-position bottom
|
||||
set -g status-interval 5
|
||||
set -g status-style "bg=#1a1b26,fg=#a9b1d6"
|
||||
|
||||
# Left: session name
|
||||
set -g status-left-length 30
|
||||
set -g status-left "#[fg=#1a1b26,bg=#7aa2f7,bold] #S #[fg=#7aa2f7,bg=#1a1b26,nobold] "
|
||||
|
||||
# Right: continuum status + time
|
||||
set -g status-right-length 50
|
||||
set -g status-right "#[fg=#565f89] #{?client_prefix,#[fg=#e0af68]PREFIX ,}#[fg=#565f89,bg=#1a1b26] %H:%M #[fg=#1a1b26,bg=#7aa2f7,bold] %d %b "
|
||||
|
||||
# Window tabs
|
||||
set -g window-status-format "#[fg=#565f89] #I #W "
|
||||
set -g window-status-current-format "#[fg=#7aa2f7,bg=#24283b,bold] #I #W #[fg=#24283b,bg=#1a1b26]"
|
||||
set -g window-status-separator ""
|
||||
|
||||
# Pane borders
|
||||
set -g pane-border-style "fg=#3b4261"
|
||||
set -g pane-active-border-style "fg=#7aa2f7"
|
||||
|
||||
# Message style
|
||||
set -g message-style "fg=#7aa2f7,bg=#1a1b26,bold"
|
||||
set -g message-command-style "fg=#7aa2f7,bg=#1a1b26"
|
||||
|
||||
# Copy mode
|
||||
setw -g mode-style "fg=#1a1b26,bg=#7aa2f7"
|
||||
|
||||
# Easy config reload
|
||||
bind r source-file ~/.tmux.conf \; display "Config reloaded!"
|
||||
|
||||
# Better pane splitting (use current path)
|
||||
bind \\ split-window -h -c "#{pane_current_path}"
|
||||
bind - split-window -v -c "#{pane_current_path}"
|
||||
unbind '"'
|
||||
unbind %
|
||||
|
||||
# New window in current path
|
||||
bind c new-window -c "#{pane_current_path}"
|
||||
|
||||
# Pane resize (if you ever use tmux panes)
|
||||
bind -r H resize-pane -L 5
|
||||
bind -r J resize-pane -D 5
|
||||
bind -r K resize-pane -U 5
|
||||
bind -r L resize-pane -R 5
|
||||
|
||||
# Quick window switching (prefix + number)
|
||||
bind 1 select-window -t 1
|
||||
bind 2 select-window -t 2
|
||||
bind 3 select-window -t 3
|
||||
bind 4 select-window -t 4
|
||||
bind 5 select-window -t 5
|
||||
bind 6 select-window -t 6
|
||||
bind 7 select-window -t 7
|
||||
bind 8 select-window -t 8
|
||||
bind 9 select-window -t 9
|
||||
|
||||
# Window navigation (prefix + h/l)
|
||||
bind h previous-window
|
||||
bind l next-window
|
||||
|
||||
# Session navigation (prefix + j/k/S)
|
||||
bind j switch-client -n # next session
|
||||
bind k switch-client -p # previous session
|
||||
bind S choose-tree -sZ # session tree picker
|
||||
|
||||
# Move windows left/right
|
||||
bind -r < swap-window -t -1\; select-window -t -1
|
||||
bind -r > swap-window -t +1\; select-window -t +1
|
||||
|
||||
# Enable vi mode for copy
|
||||
setw -g mode-keys vi
|
||||
bind -T copy-mode-vi v send -X begin-selection
|
||||
bind -T copy-mode-vi y send -X copy-selection-and-cancel
|
||||
|
||||
# -------------------------------------------------------------------
|
||||
# Clipboard (OSC 52)
|
||||
# -------------------------------------------------------------------
|
||||
set -g set-clipboard on
|
||||
|
||||
# tmux silently skips OSC 52 unless the outer terminal's terminfo claims the
|
||||
# capability. kitty's does; SSH sessions usually land on a bare
|
||||
# xterm-256color that does not, so declare both.
|
||||
set -ag terminal-features ",xterm-kitty:clipboard"
|
||||
set -ag terminal-features ",xterm-256color:clipboard"
|
||||
|
||||
# Let nvim's OSC 52 provider reach the outer terminal through tmux.
|
||||
set -g allow-passthrough on
|
||||
|
||||
# Drag-to-copy. No external command: set-clipboard routes this through
|
||||
# OSC 52, which works on Wayland and over SSH. xclip did neither.
|
||||
bind-key -T copy-mode-vi MouseDragEnd1Pane send-keys -X copy-pipe-and-cancel
|
||||
bind-key -T copy-mode MouseDragEnd1Pane send-keys -X copy-pipe-and-cancel
|
||||
|
||||
# -------------------------------------------------------------------
|
||||
# Mouse scrolling
|
||||
# -------------------------------------------------------------------
|
||||
# Scrolling up enters copy-mode, unless the pane's application wants the
|
||||
# mouse itself or we are already in copy-mode.
|
||||
bind-key -n WheelUpPane if-shell -F -t = "#{mouse_any_flag}" "send-keys -M" "if -Ft= '#{pane_in_mode}' 'send-keys -M' 'copy-mode -e'"
|
||||
|
||||
bind-key -T copy-mode-vi WheelUpPane send-keys -X scroll-up
|
||||
bind-key -T copy-mode-vi WheelDownPane send-keys -X scroll-down
|
||||
bind-key -T copy-mode WheelUpPane send-keys -X scroll-up
|
||||
bind-key -T copy-mode WheelDownPane send-keys -X scroll-down
|
||||
|
||||
|
||||
# tmux-resurrect settings
|
||||
set -g @resurrect-capture-pane-contents 'on'
|
||||
set -g @resurrect-strategy-nvim 'session'
|
||||
|
||||
# tmux-continuum settings (auto-save and auto-restore)
|
||||
set -g @continuum-restore 'on'
|
||||
set -g @continuum-save-interval '10'
|
||||
@@ -1,5 +1,17 @@
|
||||
{ pkgs, ... }:
|
||||
{
|
||||
programs.tmux = {
|
||||
enable = true;
|
||||
|
||||
# The @plugin lines and the tpm bootstrap are stripped from tmux.conf:
|
||||
# home-manager writes the run-shell lines for these itself, and tpm would
|
||||
# otherwise try to git-clone them into ~/.tmux/plugins at runtime.
|
||||
plugins = with pkgs.tmuxPlugins; [
|
||||
sensible
|
||||
resurrect
|
||||
continuum
|
||||
];
|
||||
|
||||
extraConfig = builtins.readFile ./tmux.conf;
|
||||
};
|
||||
}
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
{ 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
|
||||
];
|
||||
}
|
||||
@@ -10,7 +10,18 @@
|
||||
enableCompletion = true;
|
||||
autosuggestion.enable = true;
|
||||
syntaxHighlighting.enable = true;
|
||||
completionInit = "autoload -U compinit && compinit -u";
|
||||
# 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
|
||||
'';
|
||||
|
||||
shellAliases = {
|
||||
c = "cargo";
|
||||
@@ -78,6 +89,10 @@
|
||||
fzf = {
|
||||
enable = true;
|
||||
enableZshIntegration = true;
|
||||
# Both fzf and atuin bind Ctrl-R, and home-manager warns about the clash.
|
||||
# Atuin is sourced last so it already won; this just makes that explicit
|
||||
# and silences the warning. fzf keeps Ctrl-T and Alt-C.
|
||||
historyWidget.zsh.command = "";
|
||||
};
|
||||
|
||||
zoxide = {
|
||||
|
||||
+81
-76
@@ -5,6 +5,10 @@
|
||||
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
|
||||
@@ -20,109 +24,110 @@
|
||||
|
||||
home.sessionVariables = {
|
||||
LIBVA_DRIVER_NAME = "nvidia";
|
||||
GDM_BACKEND = "nvidia-drm";
|
||||
# Was GDM_BACKEND, which nothing reads (SDDM is the DM here).
|
||||
GBM_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 = {
|
||||
enable = true;
|
||||
nvidia = true;
|
||||
retroarch = true;
|
||||
moonlight = true;
|
||||
};
|
||||
|
||||
# 4K@1x monitors render the Electron AI apps tiny; scale their UI up. Tune to taste.
|
||||
custom.aiApps.deviceScaleFactor = "1.5";
|
||||
|
||||
wayland.windowManager.hyprland.settings = {
|
||||
exec-once = [
|
||||
# boot-into-lock: paired with SDDM autologin (host config), lock the
|
||||
# session the moment Hyprland starts so a cold boot lands on hyprlock, not
|
||||
# an open desktop. A brief flash before it paints is possible.
|
||||
"hyprlock"
|
||||
"$HOME/.local/bin/hyprpaper-rotate"
|
||||
];
|
||||
wayland.windowManager.hyprland.extraConfig = ''
|
||||
hl.on("hyprland.start", function()
|
||||
-- boot-into-lock: paired with SDDM autologin (host config), lock the
|
||||
-- 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")
|
||||
end)
|
||||
|
||||
windowrule = [
|
||||
# ethui-dev
|
||||
"workspace 2, match:title ethui-dev.*"
|
||||
"no_initial_focus on, match:title ethui-dev.*"
|
||||
"float on, match:title ethui-dev - dialog.*"
|
||||
-- ethui-dev
|
||||
hl.window_rule({ match = { title = "ethui-dev.*" }, workspace = "2", no_initial_focus = true })
|
||||
hl.window_rule({ match = { title = "ethui-dev - dialog.*" }, float = true })
|
||||
|
||||
"workspace 1, match:title ethui.*"
|
||||
"no_initial_focus on, match:title ethui.*"
|
||||
"float on, match:title ethui - dialog.*"
|
||||
hl.window_rule({ match = { title = "ethui.*" }, workspace = "1", no_initial_focus = true })
|
||||
hl.window_rule({ match = { title = "ethui - dialog.*" }, float = true })
|
||||
|
||||
"workspace 1, match:title ^\\[bevy\\].*"
|
||||
hl.window_rule({ match = { title = [[^\[bevy\].*]] }, workspace = "1" })
|
||||
|
||||
"workspace 3 silent, match:class bevy-.*"
|
||||
"no_initial_focus on, match:class bevy-.*"
|
||||
"fullscreen on, match:class bevy-.*"
|
||||
hl.window_rule({ match = { class = "bevy-.*" }, workspace = "3 silent", no_initial_focus = true, fullscreen = true })
|
||||
|
||||
"workspace 1 silent, match:title egui-.*"
|
||||
"border_size 0, match:title egui-.*"
|
||||
"float on, match:title egui-.*"
|
||||
"no_blur on, match:title egui-.*"
|
||||
"move 100%-w-20 100%-h-20, match:title egui-.*"
|
||||
hl.window_rule({
|
||||
match = { title = "egui-.*" },
|
||||
workspace = "1 silent",
|
||||
border_size = 0,
|
||||
float = true,
|
||||
no_blur = true,
|
||||
move = "100%-w-20 100%-h-20",
|
||||
})
|
||||
|
||||
# gaming: Steam/Proton titles run under XWayland with WM_CLASS
|
||||
# steam_app_<appid>. Send them fullscreen to workspace 3 (DP-1) — the same
|
||||
# streamed workspace as Big Picture below — instead of opening tiled on
|
||||
# whatever monitor Steam is on.
|
||||
# Native (non-Proton) games have their own class — grab it with
|
||||
# `hyprctl clients | grep -iE "class|title"` while the game runs and add a
|
||||
# matching line here.
|
||||
"workspace 3, match:class steam_app_.*"
|
||||
"fullscreen on, match:class steam_app_.*"
|
||||
-- gaming: Steam/Proton titles run under XWayland with WM_CLASS
|
||||
-- steam_app_<appid>. Send them fullscreen to workspace 3 (DP-1) — the same
|
||||
-- streamed workspace as Big Picture below — instead of opening tiled on
|
||||
-- whatever monitor Steam is on.
|
||||
-- Native (non-Proton) games have their own class — grab it with
|
||||
-- `hyprctl clients | grep -iE "class|title"` while the game runs and add a
|
||||
-- matching rule here.
|
||||
hl.window_rule({ match = { class = "steam_app_.*" }, workspace = "3", fullscreen = true })
|
||||
|
||||
# Steam Big Picture shares the plain `steam` WM_CLASS with the normal
|
||||
# client, so match its title instead. Without this it opens on whatever
|
||||
# monitor the cursor is on, not the streamed one. Pin it to ws3 — DP-1's
|
||||
# normal persistent workspace, which is what the stream captures. `.`
|
||||
# stands in for the literal spaces to keep the rule string unambiguous.
|
||||
"workspace 3, match:title Steam.Big.Picture.Mode"
|
||||
"fullscreen on, match:title Steam.Big.Picture.Mode"
|
||||
-- Steam Big Picture shares the plain `steam` WM_CLASS with the normal
|
||||
-- client, so match its title instead. Without this it opens on whatever
|
||||
-- monitor the cursor is on, not the streamed one. Pin it to ws3 — DP-1's
|
||||
-- normal persistent workspace, which is what the stream captures. `.`
|
||||
-- stands in for the literal spaces to keep the rule string unambiguous.
|
||||
hl.window_rule({ match = { title = "Steam.Big.Picture.Mode" }, workspace = "3", fullscreen = true })
|
||||
|
||||
# Heroic (GOG/Epic launcher) streamed via Moonlight — `heroic --console`.
|
||||
# Tile it on ws3 (DP-1, the captured screen) rather than letting it float
|
||||
# or land on whatever monitor the cursor is on. Games it launches are
|
||||
# Proton titles → caught by the steam_app_ rules above; native ones need
|
||||
# their own class added here.
|
||||
"workspace 3, match:class heroic"
|
||||
"tile on, match:class heroic"
|
||||
-- Heroic (GOG/Epic launcher) streamed via Moonlight — `heroic --console`.
|
||||
-- Tile it on ws3 (DP-1, the captured screen) rather than letting it float
|
||||
-- or land on whatever monitor the cursor is on. Games it launches are
|
||||
-- Proton titles → caught by the steam_app_ rules above; native ones need
|
||||
-- their own class added here.
|
||||
hl.window_rule({ match = { class = "heroic" }, workspace = "3", tile = true })
|
||||
|
||||
# RetroArch streamed via Moonlight (also registered as a Sunshine app).
|
||||
# Native Wayland app_id is `com.libretro.RetroArch` (set at map time) — the
|
||||
# exact string is needed so the rule applies on spawn; a loose `retroarch`
|
||||
# match misses at map and the window flashes onto the active workspace
|
||||
# first. `silent` sends it to ws3 (DP-1, the captured screen) without
|
||||
# yanking focus off whatever workspace you're driving from.
|
||||
"workspace 3 silent, match:class com.libretro.RetroArch"
|
||||
"fullscreen on, match:class com.libretro.RetroArch"
|
||||
];
|
||||
-- RetroArch streamed via Moonlight (also registered as a Sunshine app).
|
||||
-- Native Wayland app_id is `com.libretro.RetroArch` (set at map time) — the
|
||||
-- exact string is needed so the rule applies on spawn; a loose `retroarch`
|
||||
-- match misses at map and the window flashes onto the active workspace
|
||||
-- first. `silent` sends it to ws3 (DP-1, the captured screen) without
|
||||
-- yanking focus off whatever workspace you're driving from.
|
||||
hl.window_rule({ match = { class = "com.libretro.RetroArch" }, workspace = "3 silent", fullscreen = true })
|
||||
|
||||
render = {
|
||||
# Experiment: was `true`. With a fullscreen game direct-scanning-out,
|
||||
# Sunshine starting a capture forces the compositor off the direct path,
|
||||
# and that surface/context churn is a suspected trigger for Chromium's
|
||||
# `EGL_CONTEXT_LOST` crash (Big Picture drops to software rendering mid-
|
||||
# stream). Disabled to test whether the crashes stop. If it makes no
|
||||
# difference, flip back to `true` for the local fullscreen latency win.
|
||||
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.
|
||||
};
|
||||
-- Experiment: was `true`. With a fullscreen game direct-scanning-out,
|
||||
-- Sunshine starting a capture forces the compositor off the direct path,
|
||||
-- and that surface/context churn is a suspected trigger for Chromium's
|
||||
-- `EGL_CONTEXT_LOST` crash (Big Picture drops to software rendering mid-
|
||||
-- stream). Disabled to test whether the crashes stop. If it makes no
|
||||
-- difference, flip back to `true` for the local fullscreen latency win.
|
||||
hl.config({ render = { direct_scanout = false } })
|
||||
'';
|
||||
|
||||
programs.kitty.settings.font_size = 16;
|
||||
}
|
||||
|
||||
+16
-17
@@ -1,20 +1,19 @@
|
||||
_:
|
||||
{
|
||||
wayland.windowManager.hyprland.settings = {
|
||||
monitor = [
|
||||
# List HDMI-A-1 first so it becomes the primary monitor (ID 0)
|
||||
"HDMI-A-1, 3840x2160, 0x2160, 1"
|
||||
# Then the other monitors
|
||||
"DP-2, 3840x2160, 3840x180, 1, transform, 1"
|
||||
"DP-1, 3840x2160, 0x0, 1"
|
||||
];
|
||||
workspace = [
|
||||
"1, monitor:HDMI-A-1, default:true, persistent:true" # bottom-left (primary)
|
||||
"2, monitor:DP-2, persistent:true" # right (vertical)
|
||||
"3, monitor:DP-1, persistent:true" # top-left
|
||||
"4, monitor:HDMI-A-1" # bottom-left
|
||||
"5, monitor:DP-2" # right (vertical)
|
||||
"6, monitor:DP-1" # top-left
|
||||
];
|
||||
};
|
||||
# Hyprland 0.55+ is Lua-only (see home/common/programs/hyprland); monitors and
|
||||
# workspace rules are expressed with the hl.* API in raw Lua.
|
||||
wayland.windowManager.hyprland.extraConfig = ''
|
||||
-- monitors (HDMI-A-1 listed first so it becomes the primary, ID 0)
|
||||
hl.monitor({ output = "HDMI-A-1", mode = "3840x2160", position = "0x2160", scale = 1 })
|
||||
hl.monitor({ output = "DP-2", mode = "3840x2160", position = "3840x180", scale = 1, transform = 1 })
|
||||
hl.monitor({ output = "DP-1", mode = "3840x2160", position = "0x0", scale = 1 })
|
||||
|
||||
-- workspace -> monitor bindings
|
||||
hl.workspace_rule({ workspace = "1", monitor = "HDMI-A-1", default = true, persistent = true }) -- bottom-left (primary)
|
||||
hl.workspace_rule({ workspace = "2", monitor = "DP-2", persistent = true }) -- right (vertical)
|
||||
hl.workspace_rule({ workspace = "3", monitor = "DP-1", persistent = true }) -- top-left
|
||||
hl.workspace_rule({ workspace = "4", monitor = "HDMI-A-1" })
|
||||
hl.workspace_rule({ workspace = "5", monitor = "DP-2" })
|
||||
hl.workspace_rule({ workspace = "6", monitor = "DP-1" })
|
||||
'';
|
||||
}
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
{
|
||||
lib,
|
||||
pkgs,
|
||||
...
|
||||
}:
|
||||
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
|
||||
)
|
||||
);
|
||||
in
|
||||
{
|
||||
imports = [
|
||||
../common/programs/default.nix
|
||||
../common/programs/desktop
|
||||
../common/programs/hyprland
|
||||
../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
|
||||
# inside a Moonlight window, so the cursor has to be tellable at a glance from
|
||||
# the client's own. Same theme in both modes; darkman would otherwise swap it.
|
||||
custom.hyprland.cursorPackage = pkgs.bibata-cursors;
|
||||
custom.hyprland.cursorTheme = {
|
||||
dark = "Bibata-Modern-Amber";
|
||||
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 ];
|
||||
|
||||
# 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;
|
||||
};
|
||||
programs.nh.flake = "/home/naps62/tea/nixos-config";
|
||||
|
||||
# Idle lock and dpms-off blank the virtual output: Sunshine then captures a
|
||||
# flat frame and Moonlight goes black, with no console to unlock from.
|
||||
services.hypridle.enable = lib.mkForce false;
|
||||
|
||||
# Blur and shadow cost a fullscreen pass per frame, and every frame here is
|
||||
# also x264-encoded for the stream — on a virtio-gpu with no VirGL, in software.
|
||||
wayland.windowManager.hyprland.extraConfig = ''
|
||||
hl.config({
|
||||
decoration = {
|
||||
blur = { enabled = false },
|
||||
shadow = { enabled = false },
|
||||
},
|
||||
})
|
||||
'';
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
_: {
|
||||
# Hyprland 0.55+ is Lua-only (see home/common/programs/hyprland).
|
||||
#
|
||||
# Matches every output rather than naming one: the virtio-gpu connector name
|
||||
# varies by qemu display backend (Virtual-1 vs Virtual-0). Explicit mode, not
|
||||
# `preferred` — this is the resolution Sunshine streams.
|
||||
#
|
||||
# 2160p is near the ceiling: virtio-gpu has no NVENC/VAAPI, so Sunshine falls
|
||||
# back to x264, which measured 73fps here against 135fps at 1440p — a 60fps
|
||||
# client has only ~20% margin.
|
||||
wayland.windowManager.hyprland.extraConfig = ''
|
||||
hl.monitor({ output = "", mode = "3840x2160@60", position = "0x0", scale = 1 })
|
||||
'';
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"permission": {
|
||||
"bash": "allow"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
{
|
||||
pkgs,
|
||||
inputs,
|
||||
...
|
||||
}:
|
||||
# The user services this box exists to run.
|
||||
#
|
||||
# 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.
|
||||
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.
|
||||
toolPath = "%h/.local/bin:%h/.nix-profile/bin:/etc/profiles/per-user/naps62/bin:/run/current-system/sw/bin";
|
||||
|
||||
sem = inputs.sem.packages.${pkgs.system}.default;
|
||||
|
||||
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
|
||||
];
|
||||
|
||||
# maestro's and rev's units come from their flake modules, not from the
|
||||
# hand-written set below.
|
||||
#
|
||||
# settings stays empty on purpose: ~/.config/maestro/config.toml is a
|
||||
# hand-edited plain file, not nix-generated (module skips it iff `{}`).
|
||||
# There is no `web.enable`: the daemon serves the UI on `daemon.bind_addr`.
|
||||
services.maestro.enable = true;
|
||||
|
||||
# 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. All three apply straight
|
||||
# away: a maestro restart drops the daemon but not the sessions it manages,
|
||||
# so the interactive shells on this box survive it.
|
||||
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";
|
||||
};
|
||||
};
|
||||
|
||||
systemd.user.services = {
|
||||
aoe-web = {
|
||||
Unit = {
|
||||
Description = "aoe serve — Agent of Empires web dashboard";
|
||||
After = [ "network.target" ];
|
||||
# MUST stay 0: at RestartSec=2 a fast-crashing aoe 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).
|
||||
Environment = [
|
||||
"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" ];
|
||||
};
|
||||
};
|
||||
}
|
||||
# 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.
|
||||
@@ -0,0 +1,53 @@
|
||||
_:
|
||||
# Homelab host aliases, yolo-only. Ported from the hand-written ~/.ssh/config
|
||||
# on the Ubuntu box.
|
||||
{
|
||||
programs.ssh.settings = {
|
||||
# gpg-agent serves as the ssh agent here, and signing blocks forever on a
|
||||
# pinentry prompt that has no TTY in headless/agent sessions — `ssh-add -l`
|
||||
# works, but git push hangs right after "Server accepts key". The key file
|
||||
# has no passphrase, so read it directly and skip the agent.
|
||||
"github.com" = {
|
||||
IdentityAgent = "none";
|
||||
IdentitiesOnly = true;
|
||||
IdentityFile = "~/.ssh/id_ed25519";
|
||||
};
|
||||
|
||||
"grafana" = {
|
||||
HostName = "10.6.10.30";
|
||||
User = "root";
|
||||
IdentityFile = "~/.ssh/crowdsec-loki-10.6.10.30";
|
||||
};
|
||||
|
||||
"jellyfin" = {
|
||||
HostName = "10.6.10.18";
|
||||
User = "root";
|
||||
IdentityFile = "~/.ssh/crowdsec-loki-10.6.10.30";
|
||||
};
|
||||
|
||||
"authelia" = {
|
||||
HostName = "10.6.10.42";
|
||||
User = "root";
|
||||
IdentityFile = "~/.ssh/authelia-10.6.10.42";
|
||||
};
|
||||
|
||||
"whisper" = {
|
||||
HostName = "10.6.10.43";
|
||||
User = "root";
|
||||
IdentityFile = "~/.ssh/wyoming-voice";
|
||||
IdentitiesOnly = true;
|
||||
StrictHostKeyChecking = "accept-new";
|
||||
};
|
||||
|
||||
"proxmox" = {
|
||||
HostName = "10.1.10.3";
|
||||
Port = 22022;
|
||||
User = "root";
|
||||
IdentityFile = "~/.ssh/proxmox-temp";
|
||||
# Was `crowdsec-loki` in the hand-written config, which is no longer a
|
||||
# Host — same machine, now aliased `grafana`.
|
||||
ProxyJump = "grafana";
|
||||
StrictHostKeyChecking = "accept-new";
|
||||
};
|
||||
};
|
||||
}
|
||||
@@ -17,10 +17,12 @@
|
||||
../common/features/docker.nix
|
||||
../common/features/appimage.nix
|
||||
../common/features/fonts.nix
|
||||
../common/features/kdeconnect.nix
|
||||
../common/features/nix-ld.nix
|
||||
../common/features/bluetooth.nix
|
||||
../common/features/ledger.nix
|
||||
../common/features/smb-mounts.nix
|
||||
../common/features/transmission.nix
|
||||
../common/features/home
|
||||
];
|
||||
|
||||
@@ -34,6 +36,12 @@
|
||||
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 = "/mnt/yolo";
|
||||
}
|
||||
];
|
||||
|
||||
boot.kernelParams = [
|
||||
|
||||
@@ -6,6 +6,11 @@
|
||||
settings = {
|
||||
General = {
|
||||
Enable = "Source,Sink,Media,Socket";
|
||||
# Required for LE Audio (BAP): ISO sockets / CIS live behind
|
||||
# BlueZ's experimental gate. Without this the XM6 falls back to
|
||||
# classic A2DP, and mic use forces the HSP/HFP downgrade.
|
||||
Experimental = true;
|
||||
KernelExperimental = true;
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
@@ -1,7 +1,4 @@
|
||||
{ pkgs, ... }:
|
||||
let
|
||||
sddm-noctalia-theme = pkgs.callPackage ../../../../pkgs/sddm-noctalia-theme/package.nix { };
|
||||
in
|
||||
{
|
||||
services.xserver = {
|
||||
enable = true;
|
||||
@@ -15,7 +12,7 @@ in
|
||||
services.displayManager.sddm = {
|
||||
enable = true;
|
||||
wayland.enable = true;
|
||||
theme = "sddm-noctalia";
|
||||
theme = "sddm-astronaut-theme";
|
||||
package = pkgs.kdePackages.sddm;
|
||||
extraPackages = with pkgs.kdePackages; [
|
||||
qtmultimedia
|
||||
@@ -26,6 +23,6 @@ in
|
||||
};
|
||||
|
||||
environment.systemPackages = with pkgs; [
|
||||
sddm-noctalia-theme
|
||||
sddm-astronaut
|
||||
];
|
||||
}
|
||||
|
||||
@@ -10,9 +10,24 @@
|
||||
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;
|
||||
|
||||
# Route the Secret portal (org.freedesktop.portal.Secret) to gnome-keyring.
|
||||
# Hyprland's portal doesn't implement Secret, and the preferred backend list
|
||||
# (hyprland;gtk) has no Secret provider — so without this, the portal exposes
|
||||
# no Secret interface at all. Electron apps (e.g. Claude Desktop) fetch their
|
||||
# token-encryption key through this portal; when it's missing, os_crypt init
|
||||
# fails (prev_init_success:false) and they can't persist a login across boots.
|
||||
xdg.portal.config.common."org.freedesktop.impl.portal.Secret" = "gnome-keyring";
|
||||
|
||||
services = {
|
||||
# thumbnail support for images
|
||||
tumbler.enable = true;
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
{ inputs, lib, ... }:
|
||||
{
|
||||
imports = [ inputs.nix-flatpak.nixosModules.nix-flatpak ];
|
||||
|
||||
services.flatpak = {
|
||||
enable = true;
|
||||
|
||||
# mkOptionDefault so the module's default `flathub` remote is kept rather
|
||||
# than replaced.
|
||||
remotes = lib.mkOptionDefault [
|
||||
{
|
||||
name = "flathub-beta";
|
||||
location = "https://flathub.org/beta-repo/flathub-beta.flatpakrepo";
|
||||
}
|
||||
];
|
||||
|
||||
packages = [
|
||||
# PrusaSlicer 3.x. As of 3.0 Prusa ships no Linux binary at all — Flathub
|
||||
# is the only channel, and the alphas live in flathub-beta. Runs alongside
|
||||
# pkgs.prusa-slicer (2.9.x, in home/common/programs/3d.nix): 3.x keeps its
|
||||
# profiles in a separate PrusaSlicer3-dev config dir.
|
||||
{
|
||||
appId = "com.prusa3d.PrusaSlicer";
|
||||
origin = "flathub-beta";
|
||||
}
|
||||
];
|
||||
};
|
||||
}
|
||||
@@ -47,6 +47,11 @@
|
||||
# the module's own systemd.user.services.sunshine definition.
|
||||
systemd.user.services.sunshine.environment.LD_LIBRARY_PATH = "/run/opengl-driver/lib";
|
||||
|
||||
# hardware.uinput (pulled in by services.sunshine) makes /dev/uinput
|
||||
# root:uinput 0660 and adds nobody to the group, so sunshine's virtual mouse
|
||||
# and keyboard die with "Permission denied" and the stream takes no input.
|
||||
users.groups.uinput.members = [ "naps62" ];
|
||||
|
||||
# DualSense/DualShock emulation. Sunshine presents a *real* PS5 HID (touchpad,
|
||||
# motion, LEDs, rumble) via /dev/uhid, not plain uinput — a uinput pad can't
|
||||
# carry those features. The kernel uhid node is root-only (0600) with no udev
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
{ pkgs, ... }:
|
||||
{
|
||||
# Clipboard (and file) sync between hosts over the LAN. Moonlight/Sunshine
|
||||
# carry no clipboard channel at all, so this is the only path between the
|
||||
# streaming client and its host.
|
||||
programs.kdeconnect = {
|
||||
enable = true;
|
||||
package = pkgs.kdePackages.kdeconnect-kde;
|
||||
};
|
||||
}
|
||||
@@ -7,5 +7,30 @@
|
||||
alsa.enable = true;
|
||||
alsa.support32Bit = true;
|
||||
pulse.enable = true;
|
||||
|
||||
# Disable the LC3-SWB HFP codec. BlueZ 5.86's experimental LC3 super-wideband
|
||||
# headset path (lit up by hardware.bluetooth Experimental=true) is broken for
|
||||
# the Sony WF-1000XM6: when a call opens the mic, WirePlumber auto-switches to
|
||||
# the highest-priority headset profile (LC3-SWB), the audio link fails to set
|
||||
# up ("Hands-Free Voice gateway: Transport endpoint is not connected"), and the
|
||||
# buds reset with no call audio/mic. Omitting lc3_swb from the codec allow-list
|
||||
# makes autoswitch fall back to mSBC, which is stable wideband voice. Everything
|
||||
# else (A2DP LDAC/AAC/SBC, and lc3 for any future LE Audio) stays enabled.
|
||||
wireplumber.extraConfig."51-disable-lc3swb-hfp" = {
|
||||
"monitor.bluez.properties" = {
|
||||
"bluez5.codecs" = [
|
||||
"sbc"
|
||||
"sbc_xq"
|
||||
"aac"
|
||||
"aptx"
|
||||
"aptx_hd"
|
||||
"aptx_ll"
|
||||
"ldac"
|
||||
"lc3"
|
||||
"cvsd"
|
||||
"msbc"
|
||||
];
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
_:
|
||||
# Sunshine as a remote desktop. Same wlr-screencopy capture as
|
||||
# features/gaming/sunshine.nix, minus its NVIDIA workarounds: no cudaSupport
|
||||
# (virtio-gpu has no NVENC, so this software-encodes) and no uhid rule.
|
||||
#
|
||||
# First run: https://<host>:47990 to set web-UI credentials, then pair Moonlight.
|
||||
{
|
||||
# Sunshine opens /dev/dri/renderD* once per encoder probe and never closes
|
||||
# them — ~973 fds against the 1024 default. Mesa then cannot dlopen the GBM
|
||||
# driver ("Too many open files"), capture falls back to a broken path, and
|
||||
# moonlight shows a black, artefacted picture rather than any error.
|
||||
systemd.user.services.sunshine.serviceConfig.LimitNOFILE = 65536;
|
||||
|
||||
# hardware.uinput (pulled in by services.sunshine) makes /dev/uinput
|
||||
# root:uinput 0660 and adds nobody to the group, so sunshine's virtual mouse
|
||||
# and keyboard die with "Permission denied" and the stream takes no input.
|
||||
users.groups.uinput.members = [ "naps62" ];
|
||||
|
||||
services.sunshine = {
|
||||
enable = true;
|
||||
autoStart = true;
|
||||
openFirewall = true;
|
||||
capSysAdmin = true; # kms capture reads the scanout framebuffer via DRM
|
||||
|
||||
settings = {
|
||||
# MUST be set. Auto-probe tries portalgrab first, and
|
||||
# xdg-desktop-portal-hyprland implements no RemoteDesktop interface — the
|
||||
# probe then hangs forever instead of falling back, so sunshine never
|
||||
# binds its ports and the unit sits "active" doing nothing.
|
||||
#
|
||||
# "kms", not "wlr": the wlr backend takes wlr-screencopy's dmabuf path,
|
||||
# which on this virtio-gpu returns empty buffers — sunshine encodes a flat
|
||||
# frame (~50-byte P-frames) and every client shows black, with no error on
|
||||
# either side. grim still works because it uses the shm path, and sunshine
|
||||
# exposes no way to force shm.
|
||||
#
|
||||
# "wlr", not "wlgrab": wlgrab is the name that backend logs under, but it
|
||||
# is not accepted as a value — sunshine rejects it with "Unable to
|
||||
# initialize capture method", then still starts and binds its ports with
|
||||
# no encoder, so the failure only shows up as a 503 in moonlight.
|
||||
capture = "kms";
|
||||
|
||||
# The web UI is only reachable over the network here — there is no local
|
||||
# browser — and sunshine CSRF-rejects any origin but localhost unless it
|
||||
# is listed. Both addresses: .250.1 now, .10.2 after the IP handover.
|
||||
#
|
||||
# Bare comma-separated, NOT a JSON array: sunshine splits this on commas
|
||||
# and never strips brackets or quotes, so "[...]" makes every entry fail
|
||||
# its starts_with("https://") check.
|
||||
csrf_allowed_origins = "https://10.7.250.1:47990,https://10.7.10.2:47990";
|
||||
|
||||
# At the default 20, sunshine's FEC mangles large frames and moonlight
|
||||
# draws horizontal bands over an otherwise-fine stream — no error on
|
||||
# either side, and the network shows 0% loss. konishi carries the same
|
||||
# workaround in its hand-written ~/.config/sunshine/sunshine.conf.
|
||||
fec_percentage = 1;
|
||||
};
|
||||
};
|
||||
}
|
||||
@@ -40,7 +40,10 @@ in
|
||||
};
|
||||
mountPoint = lib.mkOption {
|
||||
type = lib.types.str;
|
||||
description = "Local mount point, e.g. \"/mnt/retroarch\".";
|
||||
# MUST NOT live under $HOME: starship and eww stat every entry of the
|
||||
# home dir, so a mount point there triggers the automount on every
|
||||
# prompt and blocks for mount-timeout while the VPN is down.
|
||||
description = "Local mount point outside $HOME, e.g. \"/mnt/retroarch\".";
|
||||
};
|
||||
user = lib.mkOption {
|
||||
type = lib.types.str;
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
{ pkgs, ... }:
|
||||
{
|
||||
services.transmission = {
|
||||
enable = true;
|
||||
package = pkgs.transmission_4;
|
||||
openRPCPort = true;
|
||||
settings = {
|
||||
rpc-bind-address = "0.0.0.0";
|
||||
rpc-whitelist-enabled = false;
|
||||
rpc-host-whitelist-enabled = false;
|
||||
rpc-authentication-required = false;
|
||||
download-dir = "/var/lib/transmission/Downloads";
|
||||
};
|
||||
};
|
||||
}
|
||||
@@ -20,7 +20,7 @@
|
||||
let
|
||||
authorizedKeys = pkgs.fetchurl {
|
||||
url = "https://github.com/naps62.keys";
|
||||
sha256 = "sha256-KNei7flY0a+dIHdJjeU1+MQGAZRoz8RJnNS75svDIBY=";
|
||||
sha256 = "sha256-sjZVywzw6q4v0QMkOpjxnjAz1MononyY+qtEFX2U8hI=";
|
||||
};
|
||||
in
|
||||
pkgs.lib.splitString "\n" (builtins.readFile authorizedKeys);
|
||||
|
||||
@@ -33,6 +33,12 @@
|
||||
|
||||
services.dbus.packages = with pkgs; [ gcr ];
|
||||
|
||||
# NixOS provides /bin/sh but not /bin/bash. Some third-party scripts
|
||||
# hardcode #!/bin/bash, so provide it too.
|
||||
system.activationScripts.binbash = ''
|
||||
ln -sf ${pkgs.bash}/bin/bash /bin/bash
|
||||
'';
|
||||
|
||||
# Compressed RAM swap — better behaviour under memory pressure.
|
||||
zramSwap.enable = true;
|
||||
|
||||
|
||||
@@ -23,6 +23,14 @@
|
||||
# GC is handled by `nh clean` below (keep-N / keep-since semantics).
|
||||
# The two are mutually exclusive — nh asserts if nix.gc.automatic is on.
|
||||
gc.automatic = false;
|
||||
|
||||
# Unauthenticated GitHub API calls (flake input fetches) are capped at
|
||||
# 60/hr and 429 quickly. The token itself must not land in /nix/store
|
||||
# (world-readable), so it lives in a plain file outside Nix's management —
|
||||
# !include reads it at nix.conf parse time instead of embedding it.
|
||||
extraOptions = ''
|
||||
!include /etc/nix/github-token.conf
|
||||
'';
|
||||
};
|
||||
|
||||
# nh: ergonomic nixos-rebuild wrapper. Auto-detects the target from the
|
||||
@@ -30,7 +38,9 @@
|
||||
# no host arg. Runs as root, so its `clean` prunes system + user profiles.
|
||||
programs.nh = {
|
||||
enable = true;
|
||||
flake = "/home/naps62/projects/nixos-config";
|
||||
# mkDefault: hosts whose clone lives elsewhere (yolo, under ~/tea) override
|
||||
# this with a plain assignment.
|
||||
flake = lib.mkDefault "/home/naps62/projects/nixos-config";
|
||||
clean = {
|
||||
enable = true;
|
||||
extraArgs = "--keep 10 --keep-since 7d";
|
||||
|
||||
@@ -13,18 +13,25 @@
|
||||
../common/features/pipewire.nix
|
||||
../common/features/docker.nix
|
||||
../common/features/fonts.nix
|
||||
../common/features/kdeconnect.nix
|
||||
../common/features/nix-ld.nix
|
||||
../common/features/bluetooth.nix
|
||||
../common/features/ledger.nix
|
||||
../common/features/wine.nix
|
||||
../common/features/gaming
|
||||
../common/features/appimage.nix
|
||||
../common/features/flatpak.nix
|
||||
../common/features/smb-mounts.nix
|
||||
../common/features/transmission.nix
|
||||
../common/features/home
|
||||
];
|
||||
|
||||
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 = [
|
||||
@@ -33,6 +40,12 @@
|
||||
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 = "/mnt/yolo";
|
||||
}
|
||||
];
|
||||
|
||||
# Arm Wake-on-LAN (magic packet) on the Intel igc NIC and re-apply it on every
|
||||
|
||||
@@ -0,0 +1,86 @@
|
||||
# yolo — Proxmox VM settings
|
||||
|
||||
Hypervisor-side configuration for VM 132. Nix cannot express any of it, and
|
||||
three of these are load-bearing in ways that fail silently if changed.
|
||||
|
||||
```
|
||||
qm create 132 --name yolo \
|
||||
--cores 8 --sockets 1 --cpu host \
|
||||
--memory 16384 \
|
||||
--machine q35 \
|
||||
--bios ovmf \
|
||||
--ostype l26 \
|
||||
--scsihw virtio-scsi-single \
|
||||
--scsi0 data:400,discard=on,iothread=1,ssd=1 \
|
||||
--efidisk0 data:1,efitype=4m,pre-enrolled-keys=0 \
|
||||
--net0 virtio,bridge=vmbr0,tag=7,firewall=1 \
|
||||
--vga virtio-gl \
|
||||
--serial0 socket \
|
||||
--agent 1 \
|
||||
--onboot 1 \
|
||||
--args '-display egl-headless,gl=core,rendernode=/dev/dri/renderD128'
|
||||
```
|
||||
|
||||
After creating the disk, drop its ZFS reservation — the `data` pool has no
|
||||
`sparse` flag, so a 400G zvol otherwise reserves all 400G up front:
|
||||
|
||||
```
|
||||
zfs set refreservation=none data/vm-132-disk-1
|
||||
```
|
||||
|
||||
## The four that matter
|
||||
|
||||
**`--vga virtio-gl`.** Lets mesa allocate GBM buffers. With plain `virtio`
|
||||
every capture fails with `Failed to create GBM buffer`, and sunshine streams a
|
||||
black picture while reporting no error at all.
|
||||
|
||||
**`--args '-display egl-headless,...,rendernode=/dev/dri/renderD128'`.** Pins
|
||||
host-side rendering to the UHD 770. Without it the guest renders on llvmpipe —
|
||||
see below.
|
||||
|
||||
**`--serial0 socket`.** `virtio-gl` renders through a GL context with no QEMU
|
||||
console surface, so the Proxmox noVNC console and `qm screendump` both go dark.
|
||||
This plus `boot.kernelParams = [ "console=ttyS0,115200" ]` keeps
|
||||
`qm terminal 132` working as the only out-of-band way in.
|
||||
|
||||
**`--bios ovmf`.** `hosts/common/features/user.nix` uses systemd-boot, which
|
||||
needs UEFI. SeaBIOS gives an unbootable disk.
|
||||
|
||||
## Why the render node has to be pinned
|
||||
|
||||
pve 8.4.1 hardcodes `-display egl-headless,gl=core` in
|
||||
`/usr/share/perl5/PVE/QemuServer.pm` with no `rendernode=`. qemu then picks a
|
||||
node itself by scanning `/dev/dri` in `readdir` order and taking the first
|
||||
`renderD*` that opens. Here that lands on renderD129 — the nvidia card, which
|
||||
is reserved for LXCs. mesa cannot drive it, so virglrenderer falls back to
|
||||
software and the guest reports `virgl (LLVMPIPE)`: Hyprland manages ~2fps at
|
||||
2560x1440 while sunshine asks for 60, and moonlight disconnects.
|
||||
|
||||
The `--args` line overrides this. PVE appends `args` after its own `-display`,
|
||||
and qemu's last `-display` wins, so rendering is pinned to renderD128 (the UHD
|
||||
770, `i915`) without touching the packaged perl — which any pve-manager upgrade
|
||||
would revert. Confirm from the guest:
|
||||
|
||||
```
|
||||
grep "Renderer:" /run/user/1000/hypr/*/hyprland.log
|
||||
```
|
||||
|
||||
`virgl (Mesa Intel(R) Graphics (RPL-S))` is correct; `LLVMPIPE` means the
|
||||
override is not taking. On the host, `ls -l /proc/$(cat
|
||||
/var/run/qemu-server/132.pid)/fd | grep dri` shows which node qemu actually
|
||||
holds.
|
||||
|
||||
Forcing the EGL vendor instead (`__EGL_VENDOR_LIBRARY_FILENAMES=.../50_mesa.json`)
|
||||
does not help and was tried: the vendor was never the problem, the node was.
|
||||
|
||||
Encoding stays on x264. virgl accelerates GL only — the guest sees a
|
||||
virtio-gpu, not the Intel device, so there is no VAAPI/QuickSync encode path.
|
||||
Not a bottleneck: x264 manages 1440p at 3x realtime here.
|
||||
|
||||
## Network
|
||||
|
||||
`tag=7` puts the guest on the VLAN behind `10.7.10.1`. That VLAN is the
|
||||
UNTRUSTED zone: internet and DNS are allowed, private networks are denied by
|
||||
default. Reaching `10.6.10.0/24` or the wireguard clients on `10.10.0.0/24`
|
||||
needs an OPNsense pass rule on the UNTRUSTED interface, above
|
||||
"Allow access to the Internet but block access to private networks".
|
||||
@@ -0,0 +1,124 @@
|
||||
{
|
||||
lib,
|
||||
pkgs,
|
||||
...
|
||||
}:
|
||||
{
|
||||
imports = [
|
||||
./hardware-configuration.nix
|
||||
../common/global
|
||||
../common/features/user.nix
|
||||
../common/features/networking.nix
|
||||
../common/features/display
|
||||
../common/features/pipewire.nix
|
||||
../common/features/remote-desktop.nix
|
||||
../common/features/docker.nix
|
||||
../common/features/fonts.nix
|
||||
../common/features/kdeconnect.nix
|
||||
../common/features/nix-ld.nix
|
||||
../common/features/appimage.nix
|
||||
../common/features/transmission.nix
|
||||
../common/features/home
|
||||
];
|
||||
|
||||
# Hypervisor-side settings this host depends on are in ./PROXMOX.md —
|
||||
# `vga: virtio-gl` in particular, without which sunshine captures a black
|
||||
# screen and reports no error.
|
||||
networking.hostName = "yolo";
|
||||
|
||||
# Hung shutdown waiting on systemd-zram-setup@zram0: deactivating zram swap
|
||||
# has to fault every stored page back into RAM first. Redundant here anyway —
|
||||
# this VM has a real 8G swap partition, and the host manages its own memory.
|
||||
zramSwap.enable = lib.mkForce false;
|
||||
|
||||
boot.kernelParams = [
|
||||
# When the host pages this VM's memory out, KVM's async page fault parks
|
||||
# the faulting task in an uninterruptible wait instead of stalling the
|
||||
# whole vCPU. If the "page ready" wakeup is ever dropped, that task is
|
||||
# wedged forever and SIGKILL cannot touch it — it took out maestro-web,
|
||||
# nix activation generators and a dozen agent sessions, a few per day,
|
||||
# until a reboot. Disabling async PF makes host page-ins stall the vCPU
|
||||
# synchronously: slower under host memory pressure, but nothing hangs.
|
||||
"no-kvmapf"
|
||||
|
||||
# The only out-of-band way in. `vga: virtio-gl` renders through a GL
|
||||
# context with no QEMU console surface, so noVNC and screendump both go
|
||||
# dark; this pairs with the VM's serial0 socket to keep `qm terminal`
|
||||
# working. Listed last so it wins /dev/console and gets the getty.
|
||||
"console=tty1"
|
||||
"console=ttyS0,115200"
|
||||
];
|
||||
|
||||
# Not a security downgrade: naps62 is in `docker`, which is already
|
||||
# root-equivalent, and sshd is key-only — so the prompt guards nothing while
|
||||
# blocking unattended rebuilds (nh shells out to sudo and needs a TTY).
|
||||
security.sudo.wheelNeedsPassword = false;
|
||||
|
||||
# Nix shells out to git for the `type = "git"` flake inputs (hyprland).
|
||||
# Without it at system level, a fresh install cannot build the home config
|
||||
# that would have provided git — so neither rebuild works.
|
||||
environment.systemPackages = [ pkgs.git ];
|
||||
|
||||
# This clone lives under ~/tea, not the ~/projects path global/nix.nix assumes.
|
||||
programs.nh.flake = "/home/naps62/tea/nixos-config";
|
||||
|
||||
services = {
|
||||
# Lets the Proxmox host drive clean shutdowns/reboots and report the guest's
|
||||
# IP. The qemu-guest profile in hardware-configuration.nix only sets up the
|
||||
# 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.
|
||||
displayManager = {
|
||||
autoLogin = {
|
||||
enable = true;
|
||||
user = "naps62";
|
||||
};
|
||||
defaultSession = "hyprland";
|
||||
};
|
||||
|
||||
# A Proxmox guest has no emulated sound card, so PipeWire starts with no
|
||||
# sink and Sunshine has nothing to capture — the stream is silent. This sink
|
||||
# is the only one, so it wins the default and Sunshine records its monitor.
|
||||
pipewire.extraConfig.pipewire."10-null-sink" = {
|
||||
"context.objects" = [
|
||||
{
|
||||
factory = "adapter";
|
||||
args = {
|
||||
"factory.name" = "support.null-audio-sink";
|
||||
"node.name" = "sunshine-sink";
|
||||
"node.description" = "Sunshine";
|
||||
"media.class" = "Audio/Sink";
|
||||
"audio.position" = "FL,FR";
|
||||
};
|
||||
}
|
||||
];
|
||||
};
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
{
|
||||
config,
|
||||
lib,
|
||||
modulesPath,
|
||||
...
|
||||
}:
|
||||
|
||||
# Proxmox guest (VM 132), q35 + OVMF. Hand-written rather than generated:
|
||||
# filesystems are matched by label, set when the disk is partitioned, so this
|
||||
# stays valid if the VM is ever rebuilt from scratch.
|
||||
{
|
||||
imports = [
|
||||
(modulesPath + "/profiles/qemu-guest.nix")
|
||||
];
|
||||
|
||||
boot = {
|
||||
initrd.availableKernelModules = [
|
||||
"ahci"
|
||||
"xhci_pci"
|
||||
"virtio_pci"
|
||||
"virtio_scsi"
|
||||
"sd_mod"
|
||||
"sr_mod"
|
||||
];
|
||||
# In the initrd, not kernelModules: otherwise there is no DRM device until
|
||||
# late boot and SDDM races it.
|
||||
initrd.kernelModules = [ "virtio_gpu" ];
|
||||
kernelModules = [ "kvm-intel" ];
|
||||
extraModulePackages = [ ];
|
||||
};
|
||||
|
||||
fileSystems."/" = {
|
||||
device = "/dev/disk/by-label/nixos";
|
||||
fsType = "ext4";
|
||||
};
|
||||
|
||||
fileSystems."/boot" = {
|
||||
device = "/dev/disk/by-label/BOOT";
|
||||
fsType = "vfat";
|
||||
options = [
|
||||
"fmask=0077"
|
||||
"dmask=0077"
|
||||
];
|
||||
};
|
||||
|
||||
swapDevices = [ { device = "/dev/disk/by-label/swap"; } ];
|
||||
|
||||
networking.useDHCP = lib.mkDefault true;
|
||||
|
||||
nixpkgs.hostPlatform = lib.mkDefault "x86_64-linux";
|
||||
hardware.cpu.intel.updateMicrocode = lib.mkDefault config.hardware.enableRedistributableFirmware;
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user