dendritic migration

dendritic migration
This commit is contained in:
2026-03-05 10:58:00 +00:00
parent 05544d0597
commit e463c42740
142 changed files with 4411 additions and 2779 deletions

124
modules/_darwin/dock.nix Normal file
View File

@@ -0,0 +1,124 @@
{
config,
lib,
pkgs,
...
}:
with lib; let
cfg = config.local.dock;
inherit (pkgs) stdenv dockutil;
in {
options = {
local.dock = {
enable =
mkOption {
description = "Enable dock";
default = stdenv.isDarwin;
example = false;
};
entries =
mkOption {
description = "Entries on the Dock";
type = with types;
listOf (submodule {
options = {
path = lib.mkOption {type = str;};
section =
lib.mkOption {
type = str;
default = "apps";
};
options =
lib.mkOption {
type = str;
default = "";
};
};
});
default = [
{path = "/Applications/Helium.app/";}
{path = "/Applications/Ghostty.app/";}
{path = "/System/Applications/Calendar.app/";}
{path = "/System/Applications/Mail.app/";}
{path = "/System/Applications/Notes.app/";}
{path = "/System/Applications/Music.app/";}
{path = "/System/Applications/System Settings.app/";}
{
path = "${config.users.users.cschmatzler.home}/Downloads";
section = "others";
options = "--sort name --view grid --display stack";
}
];
};
username =
mkOption {
description = "Username to apply the dock settings to";
type = types.str;
default = "cschmatzler";
};
};
};
config =
mkIf cfg.enable (
let
normalize = path:
if hasSuffix ".app" path
then path + "/"
else path;
entryURI = path:
"file://"
+ (
builtins.replaceStrings
[
" "
"!"
"\""
"#"
"$"
"%"
"&"
"'"
"("
")"
]
[
"%20"
"%21"
"%22"
"%23"
"%24"
"%25"
"%26"
"%27"
"%28"
"%29"
]
(normalize path)
);
wantURIs = concatMapStrings (entry: "${entryURI entry.path}\n") cfg.entries;
createEntries =
concatMapStrings (
entry: "${dockutil}/bin/dockutil --no-restart --add '${entry.path}' --section ${entry.section} ${entry.options}\n"
)
cfg.entries;
in {
system.activationScripts.postActivation.text = ''
echo >&2 "Setting up the Dock for ${cfg.username}..."
su ${cfg.username} -s /bin/sh <<-'USERBLOCK'
haveURIs="$(${dockutil}/bin/dockutil --list | ${pkgs.coreutils}/bin/cut -f2)"
if ! diff -wu <(echo -n "$haveURIs") <(echo -n '${wantURIs}') >&2 ; then
echo >&2 "Resetting Dock."
${dockutil}/bin/dockutil --no-restart --remove all
${createEntries}
killall Dock
else
echo >&2 "Dock setup complete."
fi
USERBLOCK
'';
}
);
}

View File

@@ -0,0 +1,37 @@
{
disko.devices = {
disk = {
main = {
type = "disk";
device = "/dev/sda";
content = {
type = "gpt";
partitions = {
boot = {
size = "1M";
type = "EF02";
};
ESP = {
size = "512M";
type = "EF00";
content = {
type = "filesystem";
format = "vfat";
mountpoint = "/boot";
mountOptions = ["umask=0077"];
};
};
root = {
size = "100%";
content = {
type = "filesystem";
format = "ext4";
mountpoint = "/";
};
};
};
};
};
};
};
}

View File

@@ -0,0 +1,18 @@
{
lib,
modulesPath,
...
}: {
imports = [
(modulesPath + "/profiles/qemu-guest.nix")
];
boot.initrd.availableKernelModules = ["ahci" "xhci_pci" "virtio_pci" "virtio_scsi" "sd_mod" "sr_mod"];
boot.initrd.kernelModules = [];
boot.kernelModules = [];
boot.extraModulePackages = [];
nixpkgs.hostPlatform = lib.mkDefault "x86_64-linux";
networking.useDHCP = lib.mkDefault true;
}

View File

@@ -0,0 +1,60 @@
{
services.adguardhome = {
enable = true;
host = "0.0.0.0";
port = 10000;
settings = {
dhcp = {
enabled = false;
};
dns = {
upstream_dns = [
"1.1.1.1"
"1.0.0.1"
];
};
filtering = {
protection_enabled = true;
filtering_enabled = true;
safe_search = {
enabled = false;
};
safebrowsing_enabled = true;
blocked_response_ttl = 10;
filters_update_interval = 24;
blocked_services = {
ids = [
"reddit"
"twitter"
];
};
};
filters = [
{
enabled = true;
url = "https://cdn.jsdelivr.net/gh/hagezi/dns-blocklists@latest/adblock/pro.txt";
name = "HaGeZi Multi PRO";
id = 1;
}
{
enabled = true;
url = "https://cdn.jsdelivr.net/gh/hagezi/dns-blocklists@latest/adblock/tif.txt";
name = "HaGeZi Threat Intelligence Feeds";
id = 2;
}
{
enabled = true;
url = "https://cdn.jsdelivr.net/gh/hagezi/dns-blocklists@latest/adblock/gambling.txt";
name = "HaGeZi Gambling";
id = 3;
}
{
enabled = true;
url = "https://cdn.jsdelivr.net/gh/hagezi/dns-blocklists@latest/adblock/nsfw.txt";
name = "HaGeZi NSFW";
id = 4;
}
];
};
};
}

View File

@@ -0,0 +1,10 @@
{...}: {
services.caddy.virtualHosts."cache.manticore-hippocampus.ts.net" = {
extraConfig = ''
tls {
get_certificate tailscale
}
reverse_proxy localhost:32843
'';
};
}

View File

@@ -0,0 +1,38 @@
{config, ...}: {
services.tailscale.extraSetFlags = ["--accept-routes=false"];
networking = {
useDHCP = false;
interfaces.eno1.ipv4.addresses = [
{
address = "192.168.1.10";
prefixLength = 24;
}
];
defaultGateway = "192.168.1.1";
nameservers = ["1.1.1.1"];
firewall = {
enable = true;
trustedInterfaces = ["eno1" "tailscale0"];
allowedUDPPorts = [
53
config.services.tailscale.port
];
allowedTCPPorts = [
22
53
];
checkReversePath = "loose";
};
};
fileSystems."/" = {
device = "/dev/disk/by-label/NIXROOT";
fsType = "ext4";
};
fileSystems."/boot" = {
device = "/dev/disk/by-label/NIXBOOT";
fsType = "vfat";
};
}

View File

@@ -0,0 +1,73 @@
{config, ...}: {
services.caddy = {
enable = true;
globalConfig = ''
admin off
'';
virtualHosts."docs.manticore-hippocampus.ts.net" = {
extraConfig = ''
tls {
get_certificate tailscale
}
reverse_proxy localhost:${toString config.services.paperless.port}
'';
};
virtualHosts."docs-ai.manticore-hippocampus.ts.net" = {
extraConfig = ''
tls {
get_certificate tailscale
}
reverse_proxy localhost:3000
'';
};
};
virtualisation.oci-containers = {
backend = "docker";
containers.paperless-ai = {
image = "clusterzx/paperless-ai:latest";
autoStart = true;
volumes = [
"paperless-ai-data:/app/data"
];
environment = {
PUID = "1000";
PGID = "1000";
PAPERLESS_AI_PORT = "3000";
# Initial setup wizard will configure the rest
PAPERLESS_AI_INITIAL_SETUP = "yes";
# Paperless-ngx API URL accessible from container (using host network)
PAPERLESS_API_URL = "http://127.0.0.1:${toString config.services.paperless.port}/api";
};
extraOptions = [
"--network=host"
];
};
};
services.redis.servers.paperless = {
enable = true;
port = 6379;
bind = "127.0.0.1";
settings = {
maxmemory = "256mb";
maxmemory-policy = "allkeys-lru";
};
};
services.paperless = {
enable = true;
address = "0.0.0.0";
passwordFile = config.sops.secrets.tahani-paperless-password.path;
settings = {
PAPERLESS_DBENGINE = "sqlite";
PAPERLESS_REDIS = "redis://127.0.0.1:6379";
PAPERLESS_CONSUMER_IGNORE_PATTERN = [
".DS_STORE/*"
"desktop.ini"
];
PAPERLESS_OCR_LANGUAGE = "deu+eng";
PAPERLESS_CSRF_TRUSTED_ORIGINS = "https://docs.manticore-hippocampus.ts.net";
};
};
}

View File

@@ -0,0 +1,20 @@
{
inputs,
input,
prev,
}: let
naersk-lib = prev.callPackage inputs.naersk {};
manifest = (prev.lib.importTOML "${input}/Cargo.toml").package;
in
naersk-lib.buildPackage {
pname = manifest.name;
version = manifest.version;
src = input;
nativeBuildInputs = [prev.pkg-config];
buildInputs = [prev.openssl];
OPENSSL_NO_VENDOR = 1;
doCheck = false;
}

View File

@@ -0,0 +1,14 @@
{
user = "cschmatzler";
sshKeys = [
"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIHfRZQ+7ejD3YHbyMTrV0gN1Gc0DxtGgl5CVZSupo5ws"
"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIL/I+/2QT47raegzMIyhwMEPKarJP/+Ox9ewA4ZFJwk/"
];
stateVersions = {
darwin = 6;
nixos = "25.11";
homeManager = "25.11";
};
}

View File

@@ -0,0 +1,10 @@
{pkgs}:
pkgs.writeShellScriptBin "open-project" ''
TARGET=$(fd -t d --exact-depth 1 . $HOME/Projects |
sed "s~$HOME/Projects/~~" |
fzf --prompt "project > ")
if [ -n "$TARGET" ]; then
echo "$HOME/Projects/$TARGET"
fi
''

66
modules/_lib/packages.nix Normal file
View File

@@ -0,0 +1,66 @@
{
inputs,
lib,
pkgs,
...
}:
with pkgs;
[
(callPackage ./open-project.nix {})
age
alejandra
ast-grep
bun
delta
devenv
dig
docker
docker-compose
dust
fastfetch
fd
gh
git
glow
gnumake
gnupg
hledger
htop
hyperfine
jj-ryu
jj-starship
jq
killall
lsof
nodejs_24
nurl
openssh
ouch
ov
pnpm
postgresql_17
sd
serie
sops
sqlite
tea
tokei
tree
tree-sitter
tuicr
vivid
]
++ lib.optionals stdenv.isDarwin [
_1password-gui
alcove
dockutil
mas
raycast
tailscale
xcodes
]
++ lib.optionals stdenv.isLinux [
gcc15
ghostty.terminfo
lm_sensors
]

View File

@@ -0,0 +1,11 @@
{pkgs}: let
wallpaper =
pkgs.fetchurl {
url = "https://misc-assets.raycast.com/wallpapers/bright-rain.png";
sha256 = "sha256-wQT4I2X3gS6QFsEb7MdRsn4oX7FNkflukXPGMFbJZ10=";
};
in
pkgs.writeShellScriptBin "set-wallpaper-script" ''
set -e
/usr/bin/osascript -e "tell application \"Finder\" to set desktop picture to POSIX file \"${wallpaper}\""
''

View File

@@ -0,0 +1,34 @@
{
programs.nixvim = {
autoGroups = {
Christoph = {};
};
autoCmd = [
{
event = "BufWritePre";
group = "Christoph";
pattern = "*";
command = "%s/\\s\\+$//e";
}
{
event = "BufReadPost";
group = "Christoph";
pattern = "*";
command = "normal zR";
}
{
event = "FileReadPost";
group = "Christoph";
pattern = "*";
command = "normal zR";
}
{
event = "FileType";
group = "Christoph";
pattern = "*.ex,*.exs,*.heex";
command = "setlocal expandtab tabstop=2 shiftwidth=2 softtabstop=2";
}
];
};
}

View File

@@ -0,0 +1,38 @@
{
imports = [
./autocmd.nix
./mappings.nix
./options.nix
./plugins/blink-cmp.nix
./plugins/conform.nix
./plugins/grug-far.nix
./plugins/harpoon.nix
./plugins/hunk.nix
./plugins/jj-diffconflicts.nix
./plugins/lsp.nix
./plugins/mini.nix
./plugins/oil.nix
./plugins/toggleterm.nix
./plugins/treesitter.nix
./plugins/zk.nix
];
programs.nixvim = {
enable = true;
defaultEditor = true;
luaLoader.enable = true;
colorschemes.catppuccin = {
enable = true;
settings = {
flavour = "latte";
};
};
extraConfigLua = ''
vim.ui.select = MiniPick.ui_select
'';
};
home.shellAliases = {
v = "nvim";
};
}

View File

@@ -0,0 +1,297 @@
{
programs.nixvim.keymaps = [
# clipboard - OSC52 yank and paste
{
mode = ["n" "v"];
key = "<leader>y";
action = ''"+y'';
options.desc = "Yank to system clipboard (OSC52)";
}
# e - explore/edit
{
mode = "n";
key = "<leader>ef";
action = ":lua require('oil').open()<CR>";
options.desc = "File directory";
}
{
mode = "n";
key = "<leader>er";
action = ":lua require('grug-far').open()<CR>";
options.desc = "Search and replace";
}
# f - find
{
mode = "n";
key = "<leader>f/";
action = ":Pick history scope='/'<CR>";
options.desc = "'/' history";
}
{
mode = "n";
key = "<leader>f:";
action = ":Pick history scope=':'<CR>";
options.desc = "':' history";
}
{
mode = "n";
key = "<leader>fa";
action = ":Pick git_hunks scope='staged'<CR>";
options.desc = "Added hunks (all)";
}
{
mode = "n";
key = "<leader>fA";
action = ":Pick git_hunks path='%' scope='staged'<CR>";
options.desc = "Added hunks (buffer)";
}
{
mode = "n";
key = "<leader>fb";
action = ":Pick buffers<CR>";
options.desc = "Buffers";
}
{
mode = "n";
key = "<leader>fd";
action = ":Pick diagnostic scope='all'<CR>";
options.desc = "Diagnostic (workspace)";
}
{
mode = "n";
key = "<leader>fD";
action = ":Pick diagnostic scope='current'<CR>";
options.desc = "Diagnostic (buffer)";
}
{
mode = "n";
key = "<leader>ff";
action = ":Pick files<CR>";
options.desc = "Search files";
}
{
mode = "n";
key = "<leader>fg";
action = ":Pick grep_live<CR>";
options.desc = "Grep";
}
{
mode = "n";
key = "<leader>fm";
action = ":Pick git_hunks<CR>";
options.desc = "Modified hunks (all)";
}
{
mode = "n";
key = "<leader>fM";
action = ":Pick git_hunks path='%'<CR>";
options.desc = "Modified hunks (buffer)";
}
{
mode = "n";
key = "<leader>fr";
action = ":Pick lsp scope='references'<CR>";
options.desc = "References (LSP)";
}
{
mode = "n";
key = "<leader>fs";
action = ":Pick lsp scope='workspace_symbol'<CR>";
options.desc = "Symbols (LSP, workspace)";
}
{
mode = "n";
key = "<leader>fS";
action = ":Pick lsp scope='document_symbol'<CR>";
options.desc = "Symbols (LSP, buffer)";
}
{
mode = "n";
key = "<leader>fv";
action = ":Pick visit_paths cwd=\"\"<CR>";
options.desc = "Visit paths (all)";
}
{
mode = "n";
key = "<leader>fV";
action = ":Pick visit_paths<CR>";
options.desc = "Visit paths (cwd)";
}
# g - git
{
mode = "n";
key = "<leader>gc";
action = ":JJDiffConflicts<CR>";
options.desc = "Resolve conflicts";
}
{
mode = "n";
key = "<leader>gg";
action.__raw = ''
function()
require('toggleterm.terminal').Terminal:new({ cmd = 'jjui', direction = 'float' }):toggle()
end
'';
options.desc = "jjui";
}
# l - lsp/formatter
{
mode = "n";
key = "<leader>la";
action = ":lua vim.lsp.buf.code_action()<CR>";
options.desc = "Actions";
}
{
mode = "n";
key = "<leader>ld";
action = ":lua vim.diagnostic.open_float({ severity = { min = vim.diagnostic.severity.HINT } })<CR>";
options.desc = "Diagnostics popup";
}
{
mode = "n";
key = "<leader>lf";
action = ":lua require('conform').format({ lsp_fallback = true })<CR>";
options.desc = "Format";
}
{
mode = "n";
key = "<leader>li";
action = ":lua vim.lsp.buf.hover()<CR>";
options.desc = "Information";
}
{
mode = "n";
key = "<leader>lj";
action = ":lua vim.diagnostic.goto_next()<CR>";
options.desc = "Next diagnostic";
}
{
mode = "n";
key = "<leader>lk";
action = ":lua vim.diagnostic.goto_prev()<CR>";
options.desc = "Prev diagnostic";
}
{
mode = "n";
key = "<leader>lr";
action = ":lua vim.lsp.buf.rename()<CR>";
options.desc = "Rename";
}
{
mode = "n";
key = "<leader>lR";
action = ":lua vim.lsp.buf.references()<CR>";
options.desc = "References";
}
{
mode = "n";
key = "<leader>ls";
action = ":lua vim.lsp.buf.definition()<CR>";
options.desc = "Source definition";
}
# other
{
mode = "n";
key = "<leader>j";
action = ":lua require('mini.jump2d').start(require('mini.jump2d').builtin_opts.query)<CR>";
options.desc = "Jump to character";
}
{
mode = "n";
key = "<leader>a";
action = ":lua require('harpoon'):list():add()<CR>";
options.desc = "Add harpoon";
}
{
mode = "n";
key = "<C-e>";
action = ":lua require('harpoon').ui:toggle_quick_menu(require('harpoon'):list())<CR>";
options.desc = "Toggle harpoon quick menu";
}
{
mode = "n";
key = "<leader>1";
action = ":lua require('harpoon'):list():select(1)<CR>";
options.desc = "Go to harpoon 1";
}
{
mode = "n";
key = "<leader>2";
action = ":lua require('harpoon'):list():select(2)<CR>";
options.desc = "Go to harpoon 2";
}
{
mode = "n";
key = "<leader>3";
action = ":lua require('harpoon'):list():select(3)<CR>";
options.desc = "Go to harpoon 3";
}
{
mode = "n";
key = "<leader>4";
action = ":lua require('harpoon'):list():select(4)<CR>";
options.desc = "Go to harpoon 4";
}
# z - zk (notes)
{
mode = "n";
key = "<leader>zn";
action = ":ZkNew { title = vim.fn.input('Title: ') }<CR>";
options.desc = "New note";
}
{
mode = "n";
key = "<leader>zo";
action = ":ZkNotes { sort = { 'modified' } }<CR>";
options.desc = "Open notes";
}
{
mode = "n";
key = "<leader>zt";
action = ":ZkTags<CR>";
options.desc = "Browse tags";
}
{
mode = "n";
key = "<leader>zf";
action = ":ZkNotes { sort = { 'modified' }, match = { vim.fn.input('Search: ') } }<CR>";
options.desc = "Find notes";
}
{
mode = "v";
key = "<leader>zf";
action = ":'<,'>ZkMatch<CR>";
options.desc = "Find notes matching selection";
}
{
mode = "n";
key = "<leader>zb";
action = ":ZkBacklinks<CR>";
options.desc = "Backlinks";
}
{
mode = "n";
key = "<leader>zl";
action = ":ZkLinks<CR>";
options.desc = "Outbound links";
}
{
mode = "n";
key = "<leader>zi";
action = ":ZkInsertLink<CR>";
options.desc = "Insert link";
}
{
mode = "v";
key = "<leader>zi";
action = ":'<,'>ZkInsertLinkAtSelection<CR>";
options.desc = "Insert link at selection";
}
{
mode = "v";
key = "<leader>zc";
action = ":'<,'>ZkNewFromTitleSelection<CR>";
options.desc = "Create note from selection";
}
];
}

View File

@@ -0,0 +1,17 @@
{
programs.nixvim = {
globals = {
clipboard = "osc52";
};
opts = {
expandtab = false;
tabstop = 2;
ignorecase = true;
list = false;
mouse = "";
relativenumber = true;
shiftwidth = 2;
smartcase = true;
};
};
}

View File

@@ -0,0 +1,17 @@
{
programs.nixvim.plugins.blink-cmp = {
enable = true;
settings = {
signature.enabled = true;
completion = {
accept = {
auto_brackets = {
enabled = true;
semantic_token_resolution.enabled = false;
};
};
documentation.auto_show = true;
};
};
};
}

View File

@@ -0,0 +1,14 @@
{
programs.nixvim.plugins.conform-nvim = {
enable = true;
settings = {
format_on_save = {};
formatters_by_ft = {
nix = ["alejandra"];
javascript = ["prettier"];
typescript = ["prettier"];
vue = ["prettier"];
};
};
};
}

View File

@@ -0,0 +1,7 @@
{
programs.nixvim.plugins = {
grug-far = {
enable = true;
};
};
}

View File

@@ -0,0 +1,7 @@
{
programs.nixvim.plugins = {
harpoon = {
enable = true;
};
};
}

View File

@@ -0,0 +1,7 @@
{
programs.nixvim.plugins = {
hunk = {
enable = true;
};
};
}

View File

@@ -0,0 +1,14 @@
{pkgs, ...}: {
programs.nixvim.extraPlugins = [
(pkgs.vimUtils.buildVimPlugin {
name = "jj-diffconflicts";
src =
pkgs.fetchFromGitHub {
owner = "rafikdraoui";
repo = "jj-diffconflicts";
rev = "main";
hash = "sha256-nzjRWHrE2jIcaDoPbixzpvflrtLhPZrihOEQWwqqU0s=";
};
})
];
}

View File

@@ -0,0 +1,17 @@
{
programs.nixvim.plugins = {
lsp = {
enable = true;
inlayHints = true;
servers = {
cssls.enable = true;
dockerls.enable = true;
jsonls.enable = true;
nil_ls.enable = true;
vtsls.enable = true;
yamlls.enable = true;
zk.enable = true;
};
};
};
}

View File

@@ -0,0 +1,158 @@
{
programs.nixvim.plugins.mini = {
enable = true;
modules = {
ai = {
custom_textobjects = {
B.__raw = "require('mini.extra').gen_ai_spec.buffer()";
F.__raw = "require('mini.ai').gen_spec.treesitter({ a = '@function.outer', i = '@function.inner' })";
};
};
align = {};
basics = {
options = {
basic = true;
extra_ui = true;
};
mappings = {
basic = false;
};
autocommands = {
basic = true;
};
};
bracketed = {};
clue = {
clues.__raw = ''
{
{ mode = 'n', keys = '<Leader>e', desc = '+Explore/+Edit' },
{ mode = 'n', keys = '<Leader>f', desc = '+Find' },
{ mode = 'n', keys = '<Leader>g', desc = '+Git' },
{ mode = 'n', keys = '<Leader>l', desc = '+LSP' },
{ mode = 'x', keys = '<Leader>l', desc = '+LSP' },
require("mini.clue").gen_clues.builtin_completion(),
require("mini.clue").gen_clues.g(),
require("mini.clue").gen_clues.marks(),
require("mini.clue").gen_clues.registers(),
require("mini.clue").gen_clues.windows({ submode_resize = true }),
require("mini.clue").gen_clues.z(),
}
'';
triggers = [
{
mode = "n";
keys = "<Leader>";
}
{
mode = "x";
keys = "<Leader>";
}
{
mode = "n";
keys = "[";
}
{
mode = "n";
keys = "]";
}
{
mode = "x";
keys = "[";
}
{
mode = "x";
keys = "]";
}
{
mode = "i";
keys = "<C-x>";
}
{
mode = "n";
keys = "g";
}
{
mode = "x";
keys = "g";
}
{
mode = "n";
keys = "\"";
}
{
mode = "x";
keys = "\"";
}
{
mode = "i";
keys = "<C-r>";
}
{
mode = "c";
keys = "<C-r>";
}
{
mode = "n";
keys = "<C-w>";
}
{
mode = "n";
keys = "z";
}
{
mode = "x";
keys = "z";
}
{
mode = "n";
keys = "'";
}
{
mode = "n";
keys = "`";
}
{
mode = "x";
keys = "'";
}
{
mode = "x";
keys = "`";
}
];
};
cmdline = {};
comment = {};
diff = {};
extra = {};
git = {};
icons = {};
indentscope = {
settings = {
symbol = "|";
};
};
jump = {};
jump2d = {
settings = {
spotter.__raw = "require('mini.jump2d').gen_spotter.pattern('[^%s%p]+')";
labels = "asdfghjkl";
view = {
dim = true;
n_steps_ahead = 2;
};
};
};
move = {};
pairs = {};
pick = {};
starter = {};
statusline = {};
surround = {};
trailspace = {};
visits = {};
};
mockDevIcons = true;
};
}

View File

@@ -0,0 +1,27 @@
{
programs.nixvim.plugins.oil = {
enable = true;
settings = {
keymaps = {
"<C-r>" = "actions.refresh";
"<leader>qq" = "actions.close";
};
skip_confirm_for_simple_edits = true;
constrain_cursor = "editable";
default_file_explorer = true;
view_options = {
show_hidden = true;
};
win_options = {
concealcursor = "ncv";
conceallevel = 3;
cursorcolumn = false;
foldcolumn = "0";
list = false;
signcolumn = "no";
spell = false;
wrap = false;
};
};
};
}

View File

@@ -0,0 +1,20 @@
{
programs.nixvim.plugins.toggleterm = {
enable = true;
settings = {
open_mapping = null;
direction = "float";
float_opts = {
border = "curved";
winblend = 3;
};
size = 20;
hide_numbers = true;
shade_terminals = true;
shading_factor = 2;
start_in_insert = true;
close_on_exit = true;
shell = "fish";
};
};
}

View File

@@ -0,0 +1,40 @@
{pkgs, ...}: {
programs.nixvim = {
plugins.treesitter = {
enable = true;
nixGrammars = true;
grammarPackages = pkgs.vimPlugins.nvim-treesitter.allGrammars;
settings = {
highlight.enable = true;
indent.enable = true;
};
};
# Register missing treesitter predicates for compatibility with newer grammars
extraConfigLuaPre = ''
do
local query = require("vim.treesitter.query")
local predicates = query.list_predicates()
if not vim.tbl_contains(predicates, "is-not?") then
query.add_predicate("is-not?", function(match, pattern, source, predicate)
local dominated_by = predicate[2]
local dominated = false
for _, node in pairs(match) do
if type(node) == "userdata" then
local current = node:parent()
while current do
if current:type() == dominated_by then
dominated = true
break
end
current = current:parent()
end
end
end
return not dominated
end, { force = true, all = true })
end
end
'';
};
}

View File

@@ -0,0 +1,6 @@
{
programs.nixvim.plugins.zk = {
enable = true;
settings = {};
};
}

View File

@@ -0,0 +1,11 @@
# Global AGENTS.md
## Version Control
- Use `jj` for VCS, not `git`
- `jj tug` is an alias for `jj bookmark move --from closest_bookmark(@-) --to @-`
## Scripting
- Always use Nushell (`nu`) for scripting
- Never use Python, Perl, Lua, awk, or any other scripting language

View File

@@ -0,0 +1,45 @@
---
description: Reviews code for quality, bugs, security, and best practices
mode: subagent
temperature: 0.1
tools:
write: false
edit: false
permission:
edit: deny
webfetch: allow
---
You are a code reviewer. Provide actionable feedback on code changes.
**Diffs alone are not enough.** Read the full file(s) being modified to understand context. Code that looks wrong in isolation may be correct given surrounding logic.
## What to Look For
**Bugs** — Primary focus.
- Logic errors, off-by-one mistakes, incorrect conditionals
- Missing guards, unreachable code paths, broken error handling
- Edge cases: null/empty inputs, race conditions
- Security: injection, auth bypass, data exposure
**Structure** — Does the code fit the codebase?
- Follows existing patterns and conventions?
- Uses established abstractions?
- Excessive nesting that could be flattened?
**Performance** — Only flag if obviously problematic.
- O(n²) on unbounded data, N+1 queries, blocking I/O on hot paths
## Before You Flag Something
- **Be certain.** Don't flag something as a bug if you're unsure — investigate first.
- **Don't invent hypothetical problems.** If an edge case matters, explain the realistic scenario.
- **Don't be a zealot about style.** Some "violations" are acceptable when they're the simplest option.
- Only review the changes — not pre-existing code that wasn't modified.
## Output
- Be direct about bugs and why they're bugs
- Communicate severity honestly — don't overstate
- Include file paths and line numbers
- Suggest fixes when appropriate
- Matter-of-fact tone, no flattery

View File

@@ -0,0 +1,49 @@
---
description: Turn pasted Albanian lesson into translated notes and solved exercises in zk
---
Process the pasted Albanian lesson content and create two `zk` notes: one for lesson material and one for exercises.
<lesson-material>
$ARGUMENTS
</lesson-material>
Requirements:
1. Parse the lesson content and produce two markdown outputs:
- `material` output: lesson material only.
- `exercises` output: exercises and solutions.
2. Use today's date in both notes (date in title and inside content).
3. In the `material` output:
- Keep clean markdown structure with headings and bullet points.
- Do not add a top-level title heading (no `# ...`) because `zk new --title` already sets the note title.
- Translate examples, dialogues, and all lesson texts into English when not already translated.
- For bigger reading passages, include a word-by-word breakdown.
- For declension/conjugation/grammar tables, provide a complete table of possibilities relevant to the topic.
- Spell out numbers only when the source token is Albanian; do not spell out English numbers.
4. In the `exercises` output:
- Include every exercise in markdown.
- Do not add a top-level title heading (no `# ...`) because `zk new --title` already sets the note title.
- Translate each exercise to English.
- Solve all non-free-writing tasks (multiple choice, fill in the blanks, etc.) and include example solutions.
- For free-writing tasks, provide expanded examples using basic vocabulary from the lesson (if prompted for 3, provide 10).
- Translate free-writing example answers into English.
- Spell out numbers only when the source token is Albanian; do not spell out English numbers.
Execution steps:
1. Generate two markdown contents in memory (do not create temporary files):
- `MATERIAL_CONTENT`
- `EXERCISES_CONTENT`
2. Set `TODAY="$(date +%F)"` once and reuse it for both notes.
3. Create note 1 with `zk` by piping markdown directly to stdin:
- Title format: `Albanian Lesson Material - YYYY-MM-DD`
- Command pattern:
- `printf "%s\n" "$MATERIAL_CONTENT" | zk new --interactive --title "Albanian Lesson Material - $TODAY" --date "$TODAY" --print-path`
4. Create note 2 with `zk` by piping markdown directly to stdin:
- Title format: `Albanian Lesson Exercises - YYYY-MM-DD`
- Command pattern:
- `printf "%s\n" "$EXERCISES_CONTENT" | zk new --interactive --title "Albanian Lesson Exercises - $TODAY" --date "$TODAY" --print-path`
5. Print both created note paths and a short checklist of what was included.
If no lesson material was provided in `$ARGUMENTS`, stop and ask the user to paste it.

View File

@@ -0,0 +1,10 @@
---
description: Review changes with parallel @code-review subagents
---
Review the code changes using THREE (3) @code-review subagents and correlate results into a summary ranked by severity. Use the provided user guidance to steer the review and focus on specific code paths, changes, and/or areas of concern. Once all three @code-review subagents return their findings and you have correlated and summarized the results, consult the @oracle subagent to perform a deep review on the findings focusing on accuracy and correctness by evaluating the surrounding code, system, subsystems, abstractions, and overall architecture of each item. Apply any recommendations from the oracle. NEVER SKIP ORACLE REVIEW.
Guidance: $ARGUMENTS
First, call `skill({ name: 'vcs-detect' })` to determine whether the repo uses git or jj, then use the appropriate VCS commands throughout.
Review uncommitted changes by default. If no uncommitted changes, review the last commit. If the user provides a pull request/merge request number or link, use CLI tools (gh/glab) to fetch it and then perform your review.

View File

@@ -0,0 +1,81 @@
---
description: Triage inbox one message at a time with himalaya only
---
Process email with strict manual triage using Himalaya only.
Hard requirements:
- Use `himalaya` for every mailbox interaction (folders, listing, reading, moving, deleting, attachments).
- Process exactly one message ID at a time. Never run bulk actions on multiple IDs.
- Do not use pattern-matching commands or searches (`grep`, `rg`, `awk`, `sed`, `himalaya envelope list` query filters, etc.).
- Always inspect current folders first, then triage.
- Treat this as a single deterministic run over a snapshot of message IDs discovered during this run.
Workflow:
1. Run `himalaya folder list` first and use those folders as the primary taxonomy.
2. Use this existing folder set as defaults when it fits:
- `INBOX`
- `Correspondence`
- `Orders and Invoices`
- `Payments`
- `Outgoing Shipments`
- `Newsletters and Marketing`
- `Junk`
- `Deleted Messages`
3. Determine source folder:
- If `$ARGUMENTS` is a single known folder name (matches a folder from step 1), use that as source.
- Otherwise use `INBOX`.
4. Build a run scope safely:
- List with fixed page size `20` and JSON output: `himalaya envelope list -f "<source>" -p 1 -s 20 --output json`.
- Start at page `1`. Enumerate IDs in returned order.
- Process each ID fully before touching the next ID.
- Keep an in-memory reviewed set for this run to avoid reprocessing IDs already handled or intentionally left untouched.
- When all IDs on the current page are in the reviewed set, advance to the next page.
- Stop when a page returns fewer results than the page size (end of folder) and all its IDs are in the reviewed set.
5. For each single envelope ID, do all checks before any move/delete:
- Check envelope flags from the JSON listing (seen/answered/flagged) before reading.
- Read the message: `himalaya message read -f "<source>" <id>`.
- If needed for classification, inspect attachments: `himalaya attachment download -f "<source>" <id> --dir /tmp/himalaya-triage`.
- If attachments are downloaded, inspect them and `rm` the downloaded files from `/tmp/himalaya-triage` after use.
- Move: `himalaya message move -f "<source>" "<destination>" <id>`.
- Delete: `himalaya message delete -f "<source>" <id>`.
6. Classification precedence (higher rule wins on conflict):
- **Actionable and unhandled** — if the message needs a reply, requires manual payment, needs a confirmation, or demands any human action, AND has NOT been replied to (no `answered` flag), leave it in the source folder untouched. This is the highest-priority rule: anything that still needs attention stays in `INBOX`.
- Human correspondence already handled — freeform natural-language messages written by a human that have been replied to (`answered` flag set): move to `Correspondence`.
- Human communication not yet replied to but not clearly actionable — when in doubt whether a human message requires action, leave it untouched.
- Clearly ephemeral automated/system message (alerts, bot/status updates, OTP/2FA, password reset codes, login codes) with no archival value: move to `Deleted Messages`.
- Automatic payment transaction notifications (charge/payment confirmations, receipts, failed-payment notices, provider payment events such as Klarna/PayPal/Stripe) that are purely informational and require no action: move to `Payments`.
- Subscription renewal notifications (auto-renew reminders, "will renew soon", price-change notices without a concrete transaction) are operational alerts, not payment records: move to `Deleted Messages`.
- Installment plan activation notifications (e.g. Barclays installment purchase confirmations) are operational confirmations, not payment records: move to `Deleted Messages`.
- "Kontoauszug verfügbar/ist online" notifications are availability alerts, not payment records: move to `Deleted Messages`.
- Orders/invoices/business records: move to `Orders and Invoices`.
- Shipping/tracking notifications (dispatch confirmations, carrier updates, delivery ETAs) without invoice or order-document value: move to `Deleted Messages`.
- Marketing/newsletters: move to `Newsletters and Marketing`.
- Delivery/submission confirmations for items you shipped outbound: move to `Outgoing Shipments`.
- Long-term but uncategorized messages: create a concise new folder and move there.
7. Folder creation rule:
- Create a new folder only if no existing folder fits and the message should be kept.
- Naming constraints: concise topic name, avoid duplicates, and avoid broad catch-all names.
- Command: `himalaya folder add "<new-folder>"`.
Execution rules:
- Never perform bulk operations. One message ID per `read`, `move`, `delete`, and attachment command.
- Always use page size 20 for envelope listing (`-s 20`).
- If any single-ID command fails, log the error and continue with the next unreviewed ID.
- Never skip reading message content before deciding.
- Keep decisions conservative: when in doubt about whether something needs action, leave it in `INBOX`.
- Never move or delete unhandled actionable messages.
- Never move human communications that haven't been replied to, unless clearly non-actionable.
- Define "processed" as "reviewed once in this run" (including intentionally untouched human messages).
- Include only messages observed during this run's listings; if new mail arrives mid-run, leave it for the next run.
- Report a compact action log at the end with:
- source folder,
- total reviewed IDs,
- counts by action (untouched/moved-to-folder/deleted),
- per-destination-folder counts,
- created folders,
- short rationale for non-obvious classifications.
<user-request>
$ARGUMENTS
</user-request>

View File

@@ -0,0 +1,17 @@
---
description: Dialogue-driven spec development through skeptical questioning
---
Develop implementation-ready specs through iterative dialogue and skeptical questioning.
First, invoke the skill tool to load the spec-planner skill:
```
skill({ name: 'spec-planner' })
```
Then follow the skill instructions to develop the spec.
<user-request>
$ARGUMENTS
</user-request>

View File

@@ -0,0 +1,17 @@
---
description: Add AI session summary to GitHub PR or GitLab MR description
---
Update the PR/MR description with an AI session export summary.
First, invoke the skill tool to load the session-export skill:
```
skill({ name: 'session-export' })
```
Then follow the skill instructions to export the session summary.
<user-request>
$ARGUMENTS
</user-request>

View File

@@ -0,0 +1,18 @@
import type { Plugin } from "@opencode-ai/plugin";
const GIT_PATTERN = /(?:^|[;&|]\s*|&&\s*|\|\|\s*|\$\(\s*|`\s*)git\s/;
export const BlockGitPlugin: Plugin = async () => {
return {
"tool.execute.before": async (input, output) => {
if (input.tool === "bash") {
const command = output.args.command as string;
if (GIT_PATTERN.test(command)) {
throw new Error(
"This project uses jj, only use `jj` commands, not `git`.",
);
}
}
},
};
};

View File

@@ -0,0 +1,19 @@
import type { Plugin } from "@opencode-ai/plugin";
const SCRIPTING_PATTERN =
/(?:^|[;&|]\s*|&&\s*|\|\|\s*|\$\(\s*|`\s*)(?:python[23]?|perl|ruby|php|lua|bash\s+-c|sh\s+-c)\s/;
export const BlockScriptingPlugin: Plugin = async () => {
return {
"tool.execute.before": async (input, output) => {
if (input.tool === "bash") {
const command = output.args.command as string;
if (SCRIPTING_PATTERN.test(command)) {
throw new Error(
"Do not use python, perl, ruby, php, lua, or inline bash/sh for scripting. Use `nu -c` instead.",
);
}
}
},
};
};

View File

@@ -0,0 +1,41 @@
---
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.
---
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.

View File

@@ -0,0 +1,123 @@
---
name: librarian
description: Multi-repository codebase exploration. Research library internals, find code patterns, understand architecture, compare implementations across GitHub/npm/PyPI/crates. Use when needing deep understanding of how libraries work, finding implementations across open source, or exploring remote repository structure.
references:
- references/tool-routing.md
- references/opensrc-api.md
- references/opensrc-examples.md
- references/linking.md
- references/diagrams.md
---
# Librarian Skill
Deep codebase exploration across remote repositories.
## How to Use This Skill
### Reference Structure
| File | Purpose | When to Read |
|------|---------|--------------|
| `tool-routing.md` | Tool selection decision trees | **Always read first** |
| `opensrc-api.md` | API reference, types | Writing opensrc code |
| `opensrc-examples.md` | JavaScript patterns, workflows | Implementation examples |
| `linking.md` | GitHub URL patterns | Formatting responses |
| `diagrams.md` | Mermaid patterns | Visualizing architecture |
### Reading Order
1. **Start** with `tool-routing.md` → choose tool strategy
2. **If using opensrc:**
- Read `opensrc-api.md` for API details
- Read `opensrc-examples.md` for patterns
3. **Before responding:** `linking.md` + `diagrams.md` for output formatting
## Tool Arsenal
| Tool | Best For | Limitations |
|------|----------|-------------|
| **grep_app** | Find patterns across ALL public GitHub | Literal search only |
| **context7** | Library docs, API examples, usage | Known libraries only |
| **opensrc** | Fetch full source for deep exploration | Must fetch before read |
## Quick Decision Trees
### "How does X work?"
```
Known library?
├─ Yes → context7.resolve-library-id → context7.query-docs
│ └─ Need internals? → opensrc.fetch → read source
└─ No → grep_app search → opensrc.fetch top result
```
### "Find pattern X"
```
Specific repo?
├─ Yes → opensrc.fetch → opensrc.grep → read matches
└─ No → grep_app (broad) → opensrc.fetch interesting repos
```
### "Explore repo structure"
```
1. opensrc.fetch(target)
2. opensrc.tree(source.name) → quick overview
3. opensrc.files(source.name, "**/*.ts") → detailed listing
4. Read: README, package.json, src/index.*
5. Create architecture diagram (see diagrams.md)
```
### "Compare X vs Y"
```
1. opensrc.fetch(["X", "Y"])
2. Use source.name from results for subsequent calls
3. opensrc.grep(pattern, { sources: [nameX, nameY] })
4. Read comparable files, synthesize differences
```
## Critical: Source Naming Convention
**After fetching, always use `source.name` for subsequent calls:**
```javascript
const [{ source }] = await opensrc.fetch("vercel/ai");
const files = await opensrc.files(source.name, "**/*.ts");
```
| Type | Fetch Spec | Source Name |
|------|------------|-------------|
| npm | `"zod"` | `"zod"` |
| npm scoped | `"@tanstack/react-query"` | `"@tanstack/react-query"` |
| pypi | `"pypi:requests"` | `"requests"` |
| crates | `"crates:serde"` | `"serde"` |
| GitHub | `"vercel/ai"` | `"github.com/vercel/ai"` |
| GitLab | `"gitlab:org/repo"` | `"gitlab.com/org/repo"` |
## When NOT to Use opensrc
| Scenario | Use Instead |
|----------|-------------|
| Simple library API questions | context7 |
| Finding examples across many repos | grep_app |
| Very large monorepos (>10GB) | Clone locally |
| Private repositories | Direct access |
## Output Guidelines
1. **Comprehensive final message** - only last message returns to main agent
2. **Parallel tool calls** - maximize efficiency
3. **Link every file reference** - see `linking.md`
4. **Diagram complex relationships** - see `diagrams.md`
5. **Never mention tool names** - say "I'll search" not "I'll use opensrc"
## References
- [Tool Routing Decision Trees](references/tool-routing.md)
- [opensrc API Reference](references/opensrc-api.md)
- [opensrc Code Examples](references/opensrc-examples.md)
- [GitHub Linking Patterns](references/linking.md)
- [Mermaid Diagram Patterns](references/diagrams.md)

View File

@@ -0,0 +1,51 @@
# Mermaid Diagram Patterns
Create diagrams for:
- Architecture (component relationships)
- Data flow (request → response)
- Dependencies (import graph)
- Sequences (step-by-step processes)
## Architecture
```mermaid
graph TD
A[Client] --> B[API Gateway]
B --> C[Auth Service]
B --> D[Data Service]
D --> E[(Database)]
```
## Flow
```mermaid
flowchart LR
Input --> Parse --> Validate --> Transform --> Output
```
## Sequence
```mermaid
sequenceDiagram
Client->>+Server: Request
Server->>+DB: Query
DB-->>-Server: Result
Server-->>-Client: Response
```
## When to Use
| Type | Use For |
|------|---------|
| `graph TD` | Component hierarchy, dependencies |
| `flowchart LR` | Data transformation, pipelines |
| `sequenceDiagram` | Request/response, multi-party interaction |
| `classDiagram` | Type relationships, inheritance |
| `stateDiagram` | State machines, lifecycle |
## Tips
- Keep nodes short (3-4 words max)
- Use subgraphs for grouping related components
- Arrow labels for relationship types
- Prefer LR (left-right) for flows, TD (top-down) for hierarchies

View File

@@ -0,0 +1,61 @@
# GitHub Linking Patterns
All file/dir/code refs → fluent markdown links. Never raw URLs.
## URL Formats
### File
```
https://github.com/{owner}/{repo}/blob/{ref}/{path}
```
### File + Lines
```
https://github.com/{owner}/{repo}/blob/{ref}/{path}#L{start}-L{end}
```
### Directory
```
https://github.com/{owner}/{repo}/tree/{ref}/{path}
```
### GitLab (note `/-/blob/`)
```
https://gitlab.com/{owner}/{repo}/-/blob/{ref}/{path}
```
## Ref Resolution
| Source | Use as ref |
|--------|------------|
| Known version | `v{version}` |
| Default branch | `main` or `master` |
| opensrc fetch | ref from result |
| Specific commit | full SHA |
## Examples
### Correct
```markdown
The [`parseAsync`](https://github.com/colinhacks/zod/blob/main/src/types.ts#L450-L480) method handles...
```
### Wrong
```markdown
See https://github.com/colinhacks/zod/blob/main/src/types.ts#L100
The parseAsync method in src/types.ts handles...
```
## Line Numbers
- Single: `#L42`
- Range: `#L42-L50`
- Prefer ranges for context (2-5 lines around key code)
## Registry → GitHub
| Registry | Find repo in |
|----------|--------------|
| npm | `package.json``repository` |
| PyPI | `pyproject.toml` or setup.py |
| crates | `Cargo.toml` |

View File

@@ -0,0 +1,235 @@
# opensrc API Reference
## Tool
Use the **opensrc MCP server** via single tool:
| Tool | Purpose |
|------|---------|
| `opensrc_execute` | All operations (fetch, read, grep, files, remove, etc.) |
Takes a `code` parameter: JavaScript async arrow function executed server-side. Source trees stay on server, only results return.
## API Surface
### Read Operations
```typescript
// List all fetched sources
opensrc.list(): Source[]
// Check if source exists
opensrc.has(name: string, version?: string): boolean
// Get source metadata
opensrc.get(name: string): Source | undefined
// List files with optional glob
opensrc.files(sourceName: string, glob?: string): Promise<FileEntry[]>
// Get directory tree structure (default depth: 3)
opensrc.tree(sourceName: string, options?: { depth?: number }): Promise<TreeNode>
// Regex search file contents
opensrc.grep(pattern: string, options?: GrepOptions): Promise<GrepResult[]>
// AST-based semantic code search
opensrc.astGrep(sourceName: string, pattern: string, options?: AstGrepOptions): Promise<AstGrepMatch[]>
// Read single file
opensrc.read(sourceName: string, filePath: string): Promise<string>
// Batch read multiple files (supports globs!)
opensrc.readMany(sourceName: string, paths: string[]): Promise<Record<string, string>>
// Parse fetch spec
opensrc.resolve(spec: string): Promise<ParsedSpec>
```
### Mutation Operations
```typescript
// Fetch packages/repos
opensrc.fetch(specs: string | string[], options?: { modify?: boolean }): Promise<FetchedSource[]>
// Remove sources
opensrc.remove(names: string[]): Promise<RemoveResult>
// Clean by type
opensrc.clean(options?: CleanOptions): Promise<RemoveResult>
```
## Types
### Source
```typescript
interface Source {
type: "npm" | "pypi" | "crates" | "repo";
name: string; // Use this for all subsequent calls
version?: string;
ref?: string;
path: string;
fetchedAt: string;
repository: string;
}
```
### FetchedSource
```typescript
interface FetchedSource {
source: Source; // IMPORTANT: use source.name for subsequent calls
alreadyExists: boolean;
}
```
### GrepOptions
```typescript
interface GrepOptions {
sources?: string[]; // Filter to specific sources
include?: string; // File glob pattern (e.g., "*.ts")
maxResults?: number; // Limit results (default: 100)
}
```
### GrepResult
```typescript
interface GrepResult {
source: string;
file: string;
line: number;
content: string;
}
```
### AstGrepOptions
```typescript
interface AstGrepOptions {
glob?: string; // File glob pattern (e.g., "**/*.ts")
lang?: string | string[]; // Language(s): "js", "ts", "tsx", "html", "css"
limit?: number; // Max results (default: 1000)
}
```
### AstGrepMatch
```typescript
interface AstGrepMatch {
file: string;
line: number;
column: number;
endLine: number;
endColumn: number;
text: string; // Matched code text
metavars: Record<string, string>; // Captured $VAR → text
}
```
#### AST Pattern Syntax
| Pattern | Matches |
|---------|---------|
| `$NAME` | Single node, captures to metavars |
| `$$$ARGS` | Zero or more nodes (variadic), captures |
| `$_` | Single node, no capture |
| `$$$` | Zero or more nodes, no capture |
### FileEntry
```typescript
interface FileEntry {
path: string;
size: number;
isDirectory: boolean;
}
```
### TreeNode
```typescript
interface TreeNode {
name: string;
type: "file" | "dir";
children?: TreeNode[]; // only for dirs
}
```
### CleanOptions
```typescript
interface CleanOptions {
packages?: boolean;
repos?: boolean;
npm?: boolean;
pypi?: boolean;
crates?: boolean;
}
```
### RemoveResult
```typescript
interface RemoveResult {
success: boolean;
removed: string[];
}
```
## Error Handling
Operations throw on errors. Wrap in try/catch if needed:
```javascript
async () => {
try {
const content = await opensrc.read("zod", "missing.ts");
return content;
} catch (e) {
return { error: e.message };
}
}
```
`readMany` returns errors as string values prefixed with `[Error:`:
```javascript
const files = await opensrc.readMany("zod", ["exists.ts", "missing.ts"]);
// { "exists.ts": "content...", "missing.ts": "[Error: ENOENT...]" }
// Filter successful reads
const successful = Object.entries(files)
.filter(([_, content]) => !content.startsWith("[Error:"));
```
## Package Spec Formats
| Format | Example | Source Name After Fetch |
|--------|---------|------------------------|
| `<name>` | `"zod"` | `"zod"` |
| `<name>@<version>` | `"zod@3.22.0"` | `"zod"` |
| `pypi:<name>` | `"pypi:requests"` | `"requests"` |
| `crates:<name>` | `"crates:serde"` | `"serde"` |
| `owner/repo` | `"vercel/ai"` | `"github.com/vercel/ai"` |
| `owner/repo@ref` | `"vercel/ai@v1.0.0"` | `"github.com/vercel/ai"` |
| `gitlab:owner/repo` | `"gitlab:org/repo"` | `"gitlab.com/org/repo"` |
## Critical Pattern
**Always capture `source.name` from fetch results:**
```javascript
async () => {
const [{ source }] = await opensrc.fetch("vercel/ai");
// GitHub repos: "vercel/ai" → "github.com/vercel/ai"
const sourceName = source.name;
// Use sourceName for ALL subsequent calls
const files = await opensrc.files(sourceName, "src/**/*.ts");
return files;
}
```

View File

@@ -0,0 +1,336 @@
# opensrc Code Examples
## Workflow: Fetch → Explore
### Basic Fetch and Explore with tree()
```javascript
async () => {
const [{ source }] = await opensrc.fetch("vercel/ai");
// Get directory structure first
const tree = await opensrc.tree(source.name, { depth: 2 });
return tree;
}
```
### Fetch and Read Key Files
```javascript
async () => {
const [{ source }] = await opensrc.fetch("vercel/ai");
const sourceName = source.name; // "github.com/vercel/ai"
const files = await opensrc.readMany(sourceName, [
"package.json",
"README.md",
"src/index.ts"
]);
return { sourceName, files };
}
```
### readMany with Globs
```javascript
async () => {
const [{ source }] = await opensrc.fetch("zod");
// Read all package.json files in monorepo
const files = await opensrc.readMany(source.name, [
"packages/*/package.json" // globs supported!
]);
return Object.keys(files);
}
```
### Batch Fetch Multiple Packages
```javascript
async () => {
const results = await opensrc.fetch(["zod", "valibot", "yup"]);
const names = results.map(r => r.source.name);
// Compare how each handles string validation
const comparisons = {};
for (const name of names) {
const matches = await opensrc.grep("string.*validate|validateString", {
sources: [name],
include: "*.ts",
maxResults: 10
});
comparisons[name] = matches.map(m => `${m.file}:${m.line}`);
}
return comparisons;
}
```
## Search Patterns
### Grep → Read Context
```javascript
async () => {
const matches = await opensrc.grep("export function parse\\(", {
sources: ["zod"],
include: "*.ts"
});
if (matches.length === 0) return "No matches";
const match = matches[0];
const content = await opensrc.read(match.source, match.file);
const lines = content.split("\n");
// Return 40 lines starting from match
return {
file: match.file,
code: lines.slice(match.line - 1, match.line + 39).join("\n")
};
}
```
### Search Across All Fetched Sources
```javascript
async () => {
const sources = opensrc.list();
const results = {};
for (const source of sources) {
const errorHandling = await opensrc.grep("throw new|catch \\(|\\.catch\\(", {
sources: [source.name],
include: "*.ts",
maxResults: 20
});
results[source.name] = {
type: source.type,
errorPatterns: errorHandling.length
};
}
return results;
}
```
## AST-Based Search
Use `astGrep` for semantic code search with pattern matching.
### Find Function Declarations
```javascript
async () => {
const [{ source }] = await opensrc.fetch("lodash");
const fns = await opensrc.astGrep(source.name, "function $NAME($$$ARGS) { $$$BODY }", {
lang: "js",
limit: 20
});
return fns.map(m => ({
file: m.file,
line: m.line,
name: m.metavars.NAME
}));
}
```
### Find React Hooks Usage
```javascript
async () => {
const [{ source }] = await opensrc.fetch("vercel/ai");
const stateHooks = await opensrc.astGrep(
source.name,
"const [$STATE, $SETTER] = useState($$$INIT)",
{ lang: ["ts", "tsx"], limit: 50 }
);
return stateHooks.map(m => ({
file: m.file,
state: m.metavars.STATE,
setter: m.metavars.SETTER
}));
}
```
### Find Class Definitions with Context
```javascript
async () => {
const [{ source }] = await opensrc.fetch("zod");
const classes = await opensrc.astGrep(source.name, "class $NAME", {
glob: "**/*.ts"
});
const details = [];
for (const cls of classes.slice(0, 5)) {
const content = await opensrc.read(source.name, cls.file);
const lines = content.split("\n");
details.push({
name: cls.metavars.NAME,
file: cls.file,
preview: lines.slice(cls.line - 1, cls.line + 9).join("\n")
});
}
return details;
}
```
### Compare Export Patterns Across Libraries
```javascript
async () => {
const results = await opensrc.fetch(["zod", "valibot"]);
const names = results.map(r => r.source.name);
const exports = {};
for (const name of names) {
const matches = await opensrc.astGrep(name, "export const $NAME = $_", {
lang: "ts",
limit: 30
});
exports[name] = matches.map(m => m.metavars.NAME);
}
return exports;
}
```
### grep vs astGrep
| Use Case | Tool |
|----------|------|
| Text/regex pattern | `grep` |
| Function declarations | `astGrep`: `function $NAME($$$) { $$$ }` |
| Arrow functions | `astGrep`: `const $N = ($$$) => $_` |
| Class definitions | `astGrep`: `class $NAME extends $PARENT` |
| Import statements | `astGrep`: `import { $$$IMPORTS } from "$MOD"` |
| JSX components | `astGrep`: `<$COMP $$$PROPS />` |
## Repository Exploration
### Find Entry Points
```javascript
async () => {
const name = "github.com/vercel/ai";
const allFiles = await opensrc.files(name, "**/*.{ts,js}");
const entryPoints = allFiles.filter(f =>
f.path.match(/^(src\/)?(index|main|mod)\.(ts|js)$/) ||
f.path.includes("/index.ts")
);
// Read all entry points
const contents = {};
for (const ep of entryPoints.slice(0, 5)) {
contents[ep.path] = await opensrc.read(name, ep.path);
}
return {
totalFiles: allFiles.length,
entryPoints: entryPoints.map(f => f.path),
contents
};
}
```
### Explore Package Structure
```javascript
async () => {
const name = "zod";
// Get all TypeScript files
const tsFiles = await opensrc.files(name, "**/*.ts");
// Group by directory
const byDir = {};
for (const f of tsFiles) {
const dir = f.path.split("/").slice(0, -1).join("/") || ".";
byDir[dir] = (byDir[dir] || 0) + 1;
}
// Read key files
const pkg = await opensrc.read(name, "package.json");
const readme = await opensrc.read(name, "README.md");
return {
structure: byDir,
package: JSON.parse(pkg),
readmePreview: readme.slice(0, 500)
};
}
```
## Batch Operations
### Read Many with Error Handling
```javascript
async () => {
const files = await opensrc.readMany("zod", [
"src/index.ts",
"src/types.ts",
"src/ZodError.ts",
"src/helpers/parseUtil.ts"
]);
// files is Record<string, string> - errors start with "[Error:"
const successful = Object.entries(files)
.filter(([_, content]) => !content.startsWith("[Error:"))
.map(([path, content]) => ({ path, lines: content.split("\n").length }));
return successful;
}
```
### Parallel Grep Across Multiple Sources
```javascript
async () => {
const targets = ["zod", "valibot"];
const pattern = "export (type|interface)";
const results = await Promise.all(
targets.map(async (name) => {
const matches = await opensrc.grep(pattern, {
sources: [name],
include: "*.ts",
maxResults: 50
});
return { name, count: matches.length, matches };
})
);
return results;
}
```
## Workflow Checklist
### Comprehensive Repository Analysis
```
Repository Analysis Progress:
- [ ] 1. Fetch repository
- [ ] 2. Read package.json + README
- [ ] 3. Identify entry points (src/index.*)
- [ ] 4. Read main entry file
- [ ] 5. Map exports and public API
- [ ] 6. Trace key functionality
- [ ] 7. Create architecture diagram
```
### Library Comparison
```
Comparison Progress:
- [ ] 1. Fetch all libraries
- [ ] 2. Grep for target pattern in each
- [ ] 3. Read matching implementations
- [ ] 4. Create comparison table
- [ ] 5. Synthesize findings
```

View File

@@ -0,0 +1,109 @@
# Tool Routing
## Decision Flowchart
```mermaid
graph TD
Q[User Query] --> T{Query Type?}
T -->|Understand/Explain| U[UNDERSTAND]
T -->|Find/Search| F[FIND]
T -->|Explore/Architecture| E[EXPLORE]
T -->|Compare| C[COMPARE]
U --> U1{Known library?}
U1 -->|Yes| U2[context7.resolve-library-id]
U2 --> U3[context7.query-docs]
U3 --> U4{Need source?}
U4 -->|Yes| U5[opensrc.fetch → read]
U1 -->|No| U6[grep_app → opensrc.fetch]
F --> F1{Specific repo?}
F1 -->|Yes| F2[opensrc.fetch → grep → read]
F1 -->|No| F3[grep_app broad search]
F3 --> F4[opensrc.fetch interesting repos]
E --> E1[opensrc.fetch]
E1 --> E2[opensrc.files]
E2 --> E3[Read entry points]
E3 --> E4[Create diagram]
C --> C1["opensrc.fetch([X, Y])"]
C1 --> C2[grep same pattern]
C2 --> C3[Read comparable files]
C3 --> C4[Synthesize comparison]
```
## Query Type Detection
| Keywords | Query Type | Start With |
|----------|------------|------------|
| "how does", "why does", "explain", "purpose of" | UNDERSTAND | context7 |
| "find", "where is", "implementations of", "examples of" | FIND | grep_app |
| "explore", "walk through", "architecture", "structure" | EXPLORE | opensrc |
| "compare", "vs", "difference between" | COMPARE | opensrc |
## UNDERSTAND Queries
```
Known library? → context7.resolve-library-id → context7.query-docs
└─ Need source? → opensrc.fetch → read
Unknown? → grep_app search → opensrc.fetch top result → read
```
**When to transition context7 → opensrc:**
- Need implementation details (not just API docs)
- Question about internals/private methods
- Tracing code flow through library
## FIND Queries
```
Specific repo? → opensrc.fetch → opensrc.grep → read matches
Broad search? → grep_app → analyze → opensrc.fetch interesting repos
```
**grep_app query tips:**
- Use literal code patterns: `useState(` not "react hooks"
- Filter by language: `language: ["TypeScript"]`
- Narrow by repo: `repo: "vercel/"` for org
## EXPLORE Queries
```
1. opensrc.fetch(target)
2. opensrc.files → understand structure
3. Identify entry points: README, package.json, src/index.*
4. Read entry → internals
5. Create architecture diagram
```
## COMPARE Queries
```
1. opensrc.fetch([X, Y])
2. Extract source.name from each result
3. opensrc.grep same pattern in both
4. Read comparable files
5. Synthesize → comparison table
```
## Tool Capabilities
| Tool | Best For | Not For |
|------|----------|---------|
| **grep_app** | Broad search, unknown scope, finding repos | Semantic queries |
| **context7** | Library APIs, best practices, common patterns | Library internals |
| **opensrc** | Deep exploration, reading internals, tracing flow | Initial discovery |
## Anti-patterns
| Don't | Do |
|-------|-----|
| grep_app for known library docs | context7 first |
| opensrc.fetch before knowing target | grep_app to discover |
| Multiple small reads | opensrc.readMany batch |
| Describe without linking | Link every file ref |
| Text for complex relationships | Mermaid diagram |
| Use tool names in responses | "I'll search..." not "I'll use opensrc" |

View File

@@ -0,0 +1,122 @@
---
name: session-export
description: Update GitHub PR descriptions with AI session export summaries. Use when user asks to add session summary to PR/MR, document AI assistance in PR/MR, or export conversation summary to PR/MR description.
---
# Session Export
Update PR/MR descriptions with a structured summary of the AI-assisted conversation.
## Output Format
```markdown
> [!NOTE]
> This PR was written with AI assistance.
<details><summary>AI Session Export</summary>
<p>
```json
{
"info": {
"title": "<brief task description>",
"agent": "opencode",
"models": ["<model(s) used>"]
},
"summary": [
"<action 1>",
"<action 2>",
...
]
}
```
</p>
</details>
```
## Workflow
### 1. Export Session Data
Get session data using OpenCode CLI:
```bash
opencode export [sessionID]
```
Returns JSON with session info including models used. Use current session if no sessionID provided.
### 2. Generate Summary JSON
From exported data and conversation context, create summary:
- **title**: 2-5 word task description (lowercase)
- **agent**: always "opencode"
- **models**: array from export data
- **summary**: array of terse action statements
- Use past tense ("added", "fixed", "created")
- Start with "user requested..." or "user asked..."
- Chronological order
- Attempt to keep the summary to a max of 25 turns ("user requested", "agent did")
- **NEVER include sensitive data**: API keys, credentials, secrets, tokens, passwords, env vars
### 3. Update PR/MR Description
**GitHub:**
```bash
gh pr edit <PR_NUMBER> --body "$(cat <<'EOF'
<existing description>
> [!NOTE]
> This PR was written with AI assistance.
<details><summary>AI Session Export</summary>
...
</details>
EOF
)"
```
### 4. Preserve Existing Content
Always fetch and preserve existing PR/MR description:
```bash
# GitHub
gh pr view <PR_NUMBER> --json body -q '.body'
Append session export after existing content with blank line separator.
## Example Summary
For a session where user asked to add dark mode:
```json
{
"info": {
"title": "dark mode implementation",
"agent": "opencode",
"models": ["claude sonnet 4"]
},
"summary": [
"user requested dark mode toggle in settings",
"agent explored existing theme system",
"agent created ThemeContext for state management",
"agent added DarkModeToggle component",
"agent updated CSS variables for dark theme",
"agent ran tests and fixed 2 failures",
"agent committed changes"
]
}
```
## Security
**NEVER include in summary:**
- API keys, tokens, secrets
- Passwords, credentials
- Environment variable values
- Private URLs with auth tokens
- Personal identifiable information
- Internal hostnames/IPs

View File

@@ -0,0 +1,70 @@
---
name: vcs-detect
description: Detect whether the current project uses jj (Jujutsu) or git for version control. Run this BEFORE any VCS command to use the correct tool.
---
# VCS Detection Skill
Detect the version control system in use before running any VCS commands.
## Why This Matters
- jj (Jujutsu) and git have different CLIs and workflows
- Running `git` commands in a jj repo (or vice versa) causes errors
- Some repos use jj with git colocated (both `.jj/` and `.git/` exist)
## Detection Logic
Both `jj root` and `git rev-parse --show-toplevel` walk up the filesystem to find repo root.
**Priority order:**
1. `jj root` succeeds → jj (handles colocated too)
2. `git rev-parse` succeeds → git
3. Both fail → no VCS
## Detection Command
```bash
if jj root &>/dev/null; then echo "jj"
elif git rev-parse --show-toplevel &>/dev/null; then echo "git"
else echo "none"
fi
```
## Command Mappings
| Operation | git | jj |
|-----------|-----|-----|
| Status | `git status` | `jj status` |
| Log | `git log` | `jj log` |
| Diff | `git diff` | `jj diff` |
| Commit | `git commit` | `jj commit` / `jj describe` |
| Branch list | `git branch` | `jj branch list` |
| New branch | `git checkout -b <name>` | `jj branch create <name>` |
| Push | `git push` | `jj git push` |
| Pull/Fetch | `git pull` / `git fetch` | `jj git fetch` |
| Rebase | `git rebase` | `jj rebase` |
## Usage
Before any VCS operation:
1. Run detection command
2. Use appropriate CLI based on result
3. If `none`, warn user directory is not version controlled
## Example Integration
```
User: Show me the git log
Agent: [Runs detection] -> Result: jj
Agent: [Runs `jj log` instead of `git log`]
```
## Colocated Repos
When both `.jj/` and `.git/` exist, the repo is "colocated":
- jj manages the working copy
- git is available for compatibility (GitHub, etc.)
- **Always prefer jj commands** in colocated repos

View File

130
modules/ai-tools.nix Normal file
View File

@@ -0,0 +1,130 @@
{inputs, ...}: {
den.aspects.ai-tools.homeManager = {
pkgs,
inputs',
...
}: {
programs.opencode = {
enable = true;
package = inputs'.llm-agents.packages.opencode;
settings = {
model = "anthropic/claude-opus-4-6";
small_model = "anthropic/claude-haiku-4-5";
theme = "catppuccin";
plugin = ["oh-my-opencode@latest" "opencode-anthropic-auth@latest"];
permission = {
read = {
"*" = "allow";
"*.env" = "deny";
"*.env.*" = "deny";
"*.envrc" = "deny";
"secrets/*" = "deny";
};
};
agent = {
plan = {
model = "anthropic/claude-opus-4-6";
};
explore = {
model = "anthropic/claude-haiku-4-5";
};
};
instructions = [
"CLAUDE.md"
"AGENT.md"
# "AGENTS.md"
"AGENTS.local.md"
];
formatter = {
mix = {
disabled = true;
};
};
mcp = {
opensrc = {
enabled = true;
type = "local";
command = ["bunx" "opensrc-mcp"];
};
};
};
};
home.packages = [
inputs'.llm-agents.packages.claude-code
];
xdg.configFile = {
"opencode/agent" = {
source = ./_opencode/agent;
recursive = true;
};
"opencode/command" = {
source = ./_opencode/command;
recursive = true;
};
"opencode/skill" = {
source = ./_opencode/skill;
recursive = true;
};
"opencode/tool" = {
source = ./_opencode/tool;
recursive = true;
};
"opencode/plugin" = {
source = ./_opencode/plugin;
recursive = true;
};
"opencode/AGENTS.md".source = ./_opencode/AGENTS.md;
"opencode/oh-my-opencode.json".text =
builtins.toJSON {
"$schema" = "https://raw.githubusercontent.com/code-yeongyu/oh-my-opencode/master/assets/oh-my-opencode.schema.json";
disabled_skills = ["playwright" "dev-browser"];
git_master = {
commit_footer = false;
include_co_authored_by = false;
};
runtime_fallback = true;
agents = {
explore = {
model = "opencode-go/minimax-m2.5";
fallback_models = ["anthropic/claude-haiku-4-5"];
};
librarian = {
model = "opencode-go/minimax-m2.5";
fallback_models = ["opencode-go/glm-5"];
};
sisyphus = {
fallback_models = ["opencode-go/kimi-k2.5" "opencode-go/glm-5"];
};
};
categories = {
"visual-engineering" = {
fallback_models = ["opencode-go/glm-5" "opencode-go/kimi-k2.5"];
};
ultrabrain = {
fallback_models = ["opencode-go/kimi-k2.5" "opencode-go/glm-5"];
};
deep = {
fallback_models = ["opencode-go/kimi-k2.5" "opencode-go/glm-5"];
};
artistry = {
fallback_models = ["opencode-go/kimi-k2.5" "opencode-go/glm-5"];
};
quick = {
fallback_models = ["opencode-go/minimax-m2.5"];
};
"unspecified-low" = {
fallback_models = ["opencode-go/minimax-m2.5" "opencode-go/kimi-k2.5"];
};
"unspecified-high" = {
fallback_models = ["opencode-go/kimi-k2.5" "opencode-go/glm-5"];
};
writing = {
fallback_models = ["opencode-go/kimi-k2.5" "opencode-go/minimax-m2.5"];
};
};
};
};
};
}

26
modules/apps.nix Normal file
View File

@@ -0,0 +1,26 @@
{inputs, ...}: {
perSystem = {
pkgs,
system,
...
}: let
mkApp = name: {
type = "app";
program = "${(pkgs.writeShellScriptBin name ''
PATH=${pkgs.git}/bin:$PATH
echo "Running ${name} for ${system}"
exec ${inputs.self}/apps/${system}/${name} "$@"
'')}/bin/${name}";
};
appNames = ["apply" "build" "build-switch" "rollback"];
in {
apps =
pkgs.lib.genAttrs appNames mkApp
// {
deploy = {
type = "app";
program = "${inputs.deploy-rs.packages.${system}.deploy-rs}/bin/deploy";
};
};
};
}

17
modules/atuin.nix Normal file
View File

@@ -0,0 +1,17 @@
{...}: {
den.aspects.atuin.homeManager = {...}: {
programs.atuin = {
enable = true;
enableNushellIntegration = true;
flags = [
"--disable-up-arrow"
];
settings = {
style = "compact";
inline_height = 0;
show_help = false;
show_tabs = false;
};
};
};
}

29
modules/chidi.nix Normal file
View File

@@ -0,0 +1,29 @@
{den, ...}: {
den.aspects.chidi.includes = [
den.aspects.darwin-system
den.aspects.core
den.aspects.tailscale
den.aspects.desktop
den.aspects.terminal
den.aspects.atuin
den.aspects.dev-tools
den.aspects.neovim
den.aspects.ai-tools
den.aspects.zellij
den.aspects.zk
];
den.aspects.chidi.darwin = {pkgs, ...}: {
networking.hostName = "chidi";
networking.computerName = "chidi";
environment.systemPackages = with pkgs; [
slack
];
};
den.aspects.chidi.homeManager = {...}: {
fonts.fontconfig.enable = true;
programs.git.settings.user.email = "christoph@tuist.dev";
};
}

33
modules/core.nix Normal file
View File

@@ -0,0 +1,33 @@
{...}: {
den.aspects.core.os = {pkgs, ...}: {
programs.fish.enable = true;
environment.shells = [pkgs.nushell];
nixpkgs = {
config = {
allowUnfree = true;
};
};
nix = {
package = pkgs.nix;
settings = {
substituters = [
"https://nix-community.cachix.org"
"https://cache.nixos.org"
];
trusted-public-keys = [
"cache.nixos.org-1:6NCHdD59X431o0gWypbMrAURkbJ16ZPMQFGspcDShjY="
"nix-community.cachix.org-1:mB9FSh9qf2dCimDSUo8Zy7bkq5CX+/rkCWyvRCYg3Fs="
];
};
gc = {
automatic = true;
options = "--delete-older-than 30d";
};
extraOptions = ''
experimental-features = nix-command flakes
'';
};
};
}

141
modules/darwin.nix Normal file
View File

@@ -0,0 +1,141 @@
{inputs, ...}: {
den.aspects.darwin-system.darwin = {pkgs, ...}: {
imports = [
inputs.nix-homebrew.darwinModules.nix-homebrew
inputs.home-manager.darwinModules.home-manager
./_darwin/dock.nix
];
system.primaryUser = "cschmatzler";
system.defaults = {
NSGlobalDomain = {
AppleInterfaceStyle = null;
AppleShowAllExtensions = true;
ApplePressAndHoldEnabled = false;
KeyRepeat = 2;
InitialKeyRepeat = 15;
"com.apple.mouse.tapBehavior" = 1;
"com.apple.sound.beep.volume" = 0.0;
"com.apple.sound.beep.feedback" = 0;
AppleShowScrollBars = "WhenScrolling";
NSAutomaticCapitalizationEnabled = false;
NSAutomaticDashSubstitutionEnabled = false;
NSAutomaticPeriodSubstitutionEnabled = false;
NSAutomaticQuoteSubstitutionEnabled = false;
NSAutomaticSpellingCorrectionEnabled = false;
NSDocumentSaveNewDocumentsToCloud = false;
NSNavPanelExpandedStateForSaveMode = true;
NSNavPanelExpandedStateForSaveMode2 = true;
PMPrintingExpandedStateForPrint = true;
PMPrintingExpandedStateForPrint2 = true;
};
dock = {
autohide = true;
show-recents = false;
launchanim = true;
orientation = "bottom";
tilesize = 60;
minimize-to-application = true;
mru-spaces = false;
expose-group-apps = true;
wvous-bl-corner = 1;
wvous-br-corner = 1;
wvous-tl-corner = 1;
wvous-tr-corner = 1;
};
finder = {
_FXShowPosixPathInTitle = false;
AppleShowAllFiles = true;
FXEnableExtensionChangeWarning = false;
FXPreferredViewStyle = "clmv";
ShowPathbar = true;
ShowStatusBar = true;
};
trackpad = {
Clicking = true;
TrackpadThreeFingerDrag = true;
};
screencapture = {
location = "~/Screenshots";
type = "png";
disable-shadow = true;
};
screensaver = {
askForPassword = true;
askForPasswordDelay = 5;
};
loginwindow = {
GuestEnabled = false;
DisableConsoleAccess = true;
};
spaces.spans-displays = false;
WindowManager.StandardHideWidgets = true;
menuExtraClock = {
Show24Hour = true;
ShowDate = 1;
ShowDayOfWeek = true;
ShowSeconds = false;
};
CustomUserPreferences = {
"com.apple.desktopservices" = {
DSDontWriteNetworkStores = true;
DSDontWriteUSBStores = true;
};
"com.apple.AdLib" = {
allowApplePersonalizedAdvertising = false;
};
"com.apple.Spotlight" = {
MenuItemHidden = true;
};
};
};
nix = {
settings.trusted-users = ["@admin" "cschmatzler"];
gc.interval = {
Weekday = 0;
Hour = 2;
Minute = 0;
};
};
users.users.cschmatzler = {
name = "cschmatzler";
home = "/Users/cschmatzler";
isHidden = false;
shell = pkgs.nushell;
};
home-manager.useGlobalPkgs = true;
nix-homebrew = {
enable = true;
user = "cschmatzler";
mutableTaps = true;
taps = {
"homebrew/homebrew-core" = inputs.homebrew-core;
"homebrew/homebrew-cask" = inputs.homebrew-cask;
};
};
homebrew = {
enable = true;
casks = [
"ghostty@tip"
"helium-browser"
"tidal"
];
};
};
}

31
modules/defaults.nix Normal file
View File

@@ -0,0 +1,31 @@
{
den,
lib,
...
}: {
options.flake = {
darwinConfigurations =
lib.mkOption {
type = lib.types.lazyAttrsOf lib.types.raw;
default = {};
};
deploy =
lib.mkOption {
type = lib.types.lazyAttrsOf lib.types.raw;
default = {};
};
};
config = {
den.default.nixos.system.stateVersion = "25.11";
den.default.darwin.system.stateVersion = 6;
den.default.homeManager.home.stateVersion = "25.11";
den.default.includes = [
den.provides.define-user
den.provides.inputs'
];
den.base.user.classes = lib.mkDefault ["homeManager"];
};
}

61
modules/dendritic.nix Normal file
View File

@@ -0,0 +1,61 @@
{inputs, ...}: {
imports = [
inputs.den.flakeModule
(inputs.flake-file.flakeModules.dendritic or {})
];
# Declare all framework and module inputs via flake-file
flake-file.inputs = {
den.url = "github:vic/den";
flake-file.url = "github:vic/flake-file";
import-tree.url = "github:vic/import-tree";
flake-aspects.url = "github:vic/flake-aspects";
nixpkgs.url = "github:nixos/nixpkgs/master";
flake-parts = {
url = "github:hercules-ci/flake-parts";
inputs.nixpkgs-lib.follows = "nixpkgs";
};
home-manager = {
url = "github:nix-community/home-manager";
inputs.nixpkgs.follows = "nixpkgs";
};
darwin = {
url = "github:LnL7/nix-darwin/master";
inputs.nixpkgs.follows = "nixpkgs";
};
deploy-rs.url = "github:serokell/deploy-rs";
disko = {
url = "github:nix-community/disko";
inputs.nixpkgs.follows = "nixpkgs";
};
nix-homebrew.url = "github:zhaofengli-wip/nix-homebrew";
homebrew-core = {
url = "github:homebrew/homebrew-core";
flake = false;
};
homebrew-cask = {
url = "github:homebrew/homebrew-cask";
flake = false;
};
nixvim.url = "github:nix-community/nixvim";
llm-agents.url = "github:numtide/llm-agents.nix";
# Overlay inputs
himalaya.url = "github:pimalaya/himalaya";
jj-ryu = {
url = "github:dmmulroy/jj-ryu";
flake = false;
};
jj-starship.url = "github:dmmulroy/jj-starship";
zjstatus.url = "github:dj95/zjstatus";
tuicr.url = "github:agavra/tuicr";
naersk = {
url = "github:nix-community/naersk/master";
inputs.nixpkgs.follows = "nixpkgs";
};
# Secrets inputs
sops-nix = {
url = "github:Mic92/sops-nix";
inputs.nixpkgs.follows = "nixpkgs";
};
};
}

26
modules/deploy.nix Normal file
View File

@@ -0,0 +1,26 @@
{
inputs,
config,
...
}: {
flake.deploy.nodes = {
michael = {
hostname = "michael";
sshUser = "cschmatzler";
profiles.system = {
user = "root";
path = inputs.deploy-rs.lib.x86_64-linux.activate.nixos config.flake.nixosConfigurations.michael;
};
};
tahani = {
hostname = "tahani";
sshUser = "cschmatzler";
profiles.system = {
user = "root";
path = inputs.deploy-rs.lib.x86_64-linux.activate.nixos config.flake.nixosConfigurations.tahani;
};
};
};
flake.checks.x86_64-linux = inputs.deploy-rs.lib.x86_64-linux.deployChecks config.flake.deploy;
}

144
modules/desktop.nix Normal file
View File

@@ -0,0 +1,144 @@
{...}: {
den.aspects.desktop.homeManager = {...}: {
programs.aerospace = {
enable = true;
launchd.enable = true;
settings = {
start-at-login = true;
accordion-padding = 30;
default-root-container-layout = "tiles";
default-root-container-orientation = "auto";
on-focused-monitor-changed = [
"move-mouse monitor-lazy-center"
];
workspace-to-monitor-force-assignment = {
"1" = "main";
"2" = "main";
"3" = "main";
"4" = "main";
"5" = "main";
"6" = "main";
"7" = "main";
"8" = "main";
"9" = "secondary";
};
gaps = {
inner = {
horizontal = 8;
vertical = 8;
};
outer = {
left = 8;
right = 8;
top = 8;
bottom = 8;
};
};
on-window-detected = [
{
"if" = {
"app-id" = "com.apple.systempreferences";
};
run = "layout floating";
}
{
"if" = {
"app-id" = "com.mitchellh.ghostty";
};
run = ["layout tiling" "move-node-to-workspace 3"];
}
{
"if" = {
"app-id" = "net.imput.helium";
};
run = "move-node-to-workspace 2";
}
{
"if" = {
"app-id" = "com.tinyspeck.slackmacgap";
};
run = "move-node-to-workspace 5";
}
{
"if" = {
"app-id" = "net.whatsapp.WhatsApp";
};
run = "move-node-to-workspace 5";
}
{
"if" = {
"app-id" = "com.tidal.desktop";
};
run = "move-node-to-workspace 6";
}
];
mode = {
main.binding = {
"alt-enter" = "exec-and-forget open -a Ghostty";
"alt-h" = "focus left";
"alt-j" = "focus down";
"alt-k" = "focus up";
"alt-l" = "focus right";
"alt-shift-h" = "move left";
"alt-shift-j" = "move down";
"alt-shift-k" = "move up";
"alt-shift-l" = "move right";
"alt-ctrl-h" = "focus-monitor --wrap-around left";
"alt-ctrl-j" = "focus-monitor --wrap-around down";
"alt-ctrl-k" = "focus-monitor --wrap-around up";
"alt-ctrl-l" = "focus-monitor --wrap-around right";
"alt-ctrl-shift-h" = "move-node-to-monitor --focus-follows-window --wrap-around left";
"alt-ctrl-shift-j" = "move-node-to-monitor --focus-follows-window --wrap-around down";
"alt-ctrl-shift-k" = "move-node-to-monitor --focus-follows-window --wrap-around up";
"alt-ctrl-shift-l" = "move-node-to-monitor --focus-follows-window --wrap-around right";
"alt-space" = "layout tiles accordion";
"alt-shift-space" = "layout floating tiling";
"alt-slash" = "layout horizontal vertical";
"alt-f" = "fullscreen";
"alt-tab" = "workspace-back-and-forth";
"alt-shift-tab" = "move-workspace-to-monitor --wrap-around next";
"alt-r" = "mode resize";
"alt-shift-semicolon" = "mode service";
"alt-1" = "workspace 1";
"alt-2" = "workspace 2";
"alt-3" = "workspace 3";
"alt-4" = "workspace 4";
"alt-5" = "workspace 5";
"alt-6" = "workspace 6";
"alt-7" = "workspace 7";
"alt-8" = "workspace 8";
"alt-9" = "workspace 9";
"alt-shift-1" = "move-node-to-workspace --focus-follows-window 1";
"alt-shift-2" = "move-node-to-workspace --focus-follows-window 2";
"alt-shift-3" = "move-node-to-workspace --focus-follows-window 3";
"alt-shift-4" = "move-node-to-workspace --focus-follows-window 4";
"alt-shift-5" = "move-node-to-workspace --focus-follows-window 5";
"alt-shift-6" = "move-node-to-workspace --focus-follows-window 6";
"alt-shift-7" = "move-node-to-workspace --focus-follows-window 7";
"alt-shift-8" = "move-node-to-workspace --focus-follows-window 8";
"alt-shift-9" = "move-node-to-workspace --focus-follows-window 9";
};
resize.binding = {
"h" = "resize width -50";
"j" = "resize height +50";
"k" = "resize height -50";
"l" = "resize width +50";
"enter" = "mode main";
"esc" = "mode main";
};
service.binding = {
"esc" = "mode main";
"r" = ["reload-config" "mode main"];
"b" = ["balance-sizes" "mode main"];
"f" = ["layout floating tiling" "mode main"];
"backspace" = ["close-all-windows-but-current" "mode main"];
};
};
};
};
};
}

371
modules/dev-tools.nix Normal file
View File

@@ -0,0 +1,371 @@
{...}: {
den.aspects.dev-tools.homeManager = {...}: let
name = "Christoph Schmatzler";
in {
# Git configuration
programs.git = {
enable = true;
ignores = ["*.swp"];
settings = {
user.name = name;
init.defaultBranch = "main";
core = {
editor = "vim";
autocrlf = "input";
pager = "delta";
};
credential = {
helper = "!gh auth git-credential";
"https://github.com".useHttpPath = true;
"https://gist.github.com".useHttpPath = true;
};
pull.rebase = true;
rebase.autoStash = true;
interactive.diffFilter = "delta --color-only";
delta = {
navigate = true;
line-numbers = true;
syntax-theme = "GitHub";
side-by-side = true;
pager = "less -FRX";
};
pager = {
diff = "delta";
log = "delta";
show = "delta";
};
};
lfs = {
enable = true;
};
};
# Git shell aliases
home.shellAliases = {
g = "git";
ga = "git add";
gaa = "git add --all";
gapa = "git add --patch";
gau = "git add --update";
gav = "git add --verbose";
gap = "git apply";
gapt = "git apply --3way";
gb = "git branch";
gba = "git branch --all";
gbd = "git branch --delete";
gbD = "git branch --delete --force";
gbl = "git blame -w";
gbnm = "git branch --no-merged";
gbr = "git branch --remote";
gbs = "git bisect";
gbsb = "git bisect bad";
gbsg = "git bisect good";
gbsn = "git bisect new";
gbso = "git bisect old";
gbsr = "git bisect reset";
gbss = "git bisect start";
gc = "git commit --verbose";
gca = "git commit --verbose --all";
gcam = "git commit --all --message";
gcas = "git commit --all --signoff";
gcasm = "git commit --all --signoff --message";
gcb = "git checkout -b";
gcB = "git checkout -B";
gcf = "git config --list";
gclean = "git clean --interactive -d";
gcl = "git clone --recurse-submodules";
gclf = "git clone --recursive --shallow-submodules --filter=blob:none --also-filter-submodules";
gcm = "git checkout main";
gcmsg = "git commit --message";
gcn = "git commit --verbose --no-edit";
gco = "git checkout";
gcor = "git checkout --recurse-submodules";
gcount = "git shortlog --summary --numbered";
gcp = "git cherry-pick";
gcpa = "git cherry-pick --abort";
gcpc = "git cherry-pick --continue";
gcs = "git commit --gpg-sign";
gcss = "git commit --gpg-sign --signoff";
gcssm = "git commit --gpg-sign --signoff --message";
gcsm = "git commit --signoff --message";
gd = "git diff";
gdca = "git diff --cached";
gdcw = "git diff --cached --word-diff";
gds = "git diff --staged";
gdw = "git diff --word-diff";
gdt = "git diff-tree --no-commit-id --name-only -r";
gdup = "git diff @{upstream}";
gf = "git fetch";
gfa = "git fetch --all --tags --prune";
gfo = "git fetch origin";
gg = "git gui citool";
gga = "git gui citool --amend";
ghh = "git help";
gignore = "git update-index --assume-unchanged";
gl = "git pull";
glg = "git log --stat";
glgp = "git log --stat --patch";
glgg = "git log --graph";
glgga = "git log --graph --decorate --all";
glgm = "git log --graph --max-count=10";
glo = "git log --oneline --decorate";
glog = "git log --oneline --decorate --graph";
gloga = "git log --oneline --decorate --graph --all";
glol = "git log --graph --pretty=\"%Cred%h%Creset -%C(auto)%d%Creset %s %Cgreen(%ar) %C(bold blue)<%an>%Creset\"";
glola = "git log --graph --pretty=\"%Cred%h%Creset -%C(auto)%d%Creset %s %Cgreen(%ar) %C(bold blue)<%an>%Creset\" --all";
glols = "git log --graph --pretty=\"%Cred%h%Creset -%C(auto)%d%Creset %s %Cgreen(%ar) %C(bold blue)<%an>%Creset\" --stat";
glod = "git log --graph --pretty=\"%Cred%h%Creset -%C(auto)%d%Creset %s %Cgreen(%ad) %C(bold blue)<%an>%Creset\"";
glods = "git log --graph --pretty=\"%Cred%h%Creset -%C(auto)%d%Creset %s %Cgreen(%ad) %C(bold blue)<%an>%Creset\" --date=short";
glum = "git pull upstream main";
gm = "git merge";
gma = "git merge --abort";
gmc = "git merge --continue";
gms = "git merge --squash";
gmff = "git merge --ff-only";
gmtl = "git mergetool --no-prompt";
gmtlvim = "git mergetool --no-prompt --tool=vimdiff";
gmum = "git merge upstream/main";
gmom = "git merge origin/main";
gp = "git push";
gpd = "git push --dry-run";
gpf = "git push --force-with-lease";
gpod = "git push origin --delete";
gpr = "git pull --rebase";
gpra = "git pull --rebase --autostash";
gprav = "git pull --rebase --autostash -v";
gprom = "git pull --rebase origin main";
gpromi = "git pull --rebase=interactive origin main";
gprv = "git pull --rebase -v";
gprum = "git pull --rebase upstream main";
gprumi = "git pull --rebase=interactive upstream main";
gpv = "git push --verbose";
gpu = "git push upstream";
gr = "git remote";
gra = "git remote add";
grb = "git rebase";
grba = "git rebase --abort";
grbc = "git rebase --continue";
grbd = "git rebase develop";
grbi = "git rebase --interactive";
grbm = "git rebase main";
grbo = "git rebase --onto";
grbom = "git rebase origin/main";
grbs = "git rebase --skip";
grbum = "git rebase upstream/main";
grev = "git revert";
greva = "git revert --abort";
grevc = "git revert --continue";
grf = "git reflog";
grh = "git reset";
grhh = "git reset --hard";
grhk = "git reset --keep";
grhs = "git reset --soft";
grm = "git rm";
grmc = "git rm --cached";
grmv = "git remote rename";
grrm = "git remote remove";
grs = "git restore";
grset = "git remote set-url";
grss = "git restore --source";
grst = "git restore --staged";
gru = "git reset --";
grup = "git remote update";
grv = "git remote --verbose";
gsb = "git status --short --branch";
gsh = "git show";
gsi = "git submodule init";
gsps = "git show --pretty=short --show-signature";
gss = "git status --short";
gst = "git status";
gsta = "git stash push";
gstaa = "git stash apply";
gstall = "git stash --all";
gstc = "git stash clear";
gstd = "git stash drop";
gstl = "git stash list";
gstp = "git stash pop";
gsts = "git stash show --patch";
gstu = "git stash push --include-untracked";
gsu = "git submodule update";
gsw = "git switch";
gswc = "git switch --create";
gswd = "git switch develop";
gswm = "git switch main";
gta = "git tag --annotate";
gts = "git tag --sign";
gunignore = "git update-index --no-assume-unchanged";
gwch = "git whatchanged -p --abbrev-commit --pretty=medium";
gwt = "git worktree";
gwta = "git worktree add";
gwtls = "git worktree list";
gwtmv = "git worktree move";
gwtrm = "git worktree remove";
lg = "lazygit";
};
# Complex git aliases that require pipes/subshells — nushell `alias` can't
# handle these, so they're defined as custom commands instead.
programs.nushell.extraConfig = ''
def ggpull [] { git pull origin (git branch --show-current | str trim) }
def ggpush [] { git push origin (git branch --show-current | str trim) }
def ggsup [] { git branch $"--set-upstream-to=origin/(git branch --show-current | str trim)" }
def gluc [] { git pull upstream (git branch --show-current | str trim) }
def gpsup [] { git push --set-upstream origin (git branch --show-current | str trim) }
def gpsupf [] { git push --set-upstream origin (git branch --show-current | str trim) --force-with-lease }
def groh [] { git reset $"origin/(git branch --show-current | str trim)" --hard }
def --env grt [] {
let toplevel = (do { git rev-parse --show-toplevel } | complete | get stdout | str trim)
if ($toplevel | is-not-empty) { cd $toplevel } else { cd . }
}
def gfg [...pattern: string] { git ls-files | lines | where {|f| $f =~ ($pattern | str join ".*") } }
def gignored [] { git ls-files -v | lines | where {|l| ($l | str substring 0..1) =~ "[a-z]" } }
def gpoat [] { git push origin --all; git push origin --tags }
def gtv [] { git tag | lines | sort }
def gwipe [] { git reset --hard; git clean --force -df }
def gunwip [] {
let msg = (git rev-list --max-count=1 --format="%s" HEAD | lines | get 1)
if ($msg | str contains "--wip--") { git reset HEAD~1 }
}
def gwip [] {
git add -A
let deleted = (git ls-files --deleted | lines)
if ($deleted | is-not-empty) { git rm ...$deleted }
git commit --no-verify --no-gpg-sign --message "--wip-- [skip ci]"
}
'';
# Jujutsu configuration
programs.jujutsu = {
enable = true;
settings = {
user = {
name = name;
email = "christoph@schmatzler.com";
};
git = {
sign-on-push = true;
subprocess = true;
write-change-id-header = true;
private-commits = "description(glob:'wip:*') | description(glob:'WIP:*') | description(exact:'')";
};
fsmonitor = {
backend = "watchman";
};
ui = {
default-command = "status";
diff-formatter = ":git";
pager = ["delta" "--pager" "less -FRX"];
diff-editor = ["nvim" "-c" "DiffEditor $left $right $output"];
movement = {
edit = true;
};
};
aliases = {
n = ["new"];
tug = ["bookmark" "move" "--from" "closest_bookmark(@-)" "--to" "@-"];
stack = ["log" "-r" "stack()"];
retrunk = ["rebase" "-d" "trunk()"];
bm = ["bookmark"];
gf = ["git" "fetch"];
gp = ["git" "push"];
};
revset-aliases = {
"closest_bookmark(to)" = "heads(::to & bookmarks())";
"closest_pushable(to)" = "heads(::to & mutable() & ~description(exact:\"\") & (~empty() | merges()))";
"mine()" = "author(\"christoph@schmatzler.com\")";
"wip()" = "mine() ~ immutable()";
"open()" = "mine() ~ ::trunk()";
"current()" = "@:: & mutable()";
"stack()" = "reachable(@, mutable())";
};
templates = {
draft_commit_description = ''
concat(
coalesce(description, default_commit_description, "\n"),
surround(
"\nJJ: This commit contains the following changes:\n", "",
indent("JJ: ", diff.stat(72)),
),
"\nJJ: ignore-rest\n",
diff.git(),
)
'';
};
};
};
# Lazygit configuration
programs.lazygit = {
enable = true;
settings = {
git = {
commit.signOff = true;
pagers = [
{
delta = {
colorArg = "always";
pager = "DELTA_FEATURES=decorations delta --light --paging=never --line-numbers --hyperlinks --hyperlinks-file-link-format=\"lazygit-edit://{path}:{line}\"";
};
}
];
};
gui = {
authorColors = {
"*" = "#7287fd";
};
theme = {
activeBorderColor = [
"#8839ef"
"bold"
];
inactiveBorderColor = [
"#6c6f85"
];
optionsTextColor = [
"#1e66f5"
];
selectedLineBgColor = [
"#ccd0da"
];
cherryPickedCommitBgColor = [
"#bcc0cc"
];
cherryPickedCommitFgColor = [
"#8839ef"
];
defaultFgColor = [
"#4c4f69"
];
searchingActiveBorderColor = [
"#df8e1d"
];
unstagedChangesColor = [
"#d20f39"
];
};
};
};
};
# JJUI configuration
programs.jjui = {
enable = true;
};
# Direnv configuration
programs.direnv = {
enable = true;
nix-direnv.enable = true;
};
# Mise configuration
programs.mise = {
enable = true;
enableNushellIntegration = true;
globalConfig.settings = {
auto_install = false;
};
};
};
}

54
modules/email.nix Normal file
View File

@@ -0,0 +1,54 @@
{...}: {
den.aspects.email.homeManager = {pkgs, ...}: {
programs.himalaya = {
enable = true;
package =
pkgs.writeShellApplication {
name = "himalaya";
runtimeInputs = [pkgs.bash pkgs.coreutils pkgs.himalaya];
text = ''
exec env RUST_LOG="warn,imap_codec::response=error" ${pkgs.himalaya}/bin/himalaya "$@"
'';
};
};
programs.mbsync.enable = true;
services.mbsync = {
enable = true;
frequency = "*:0/5";
};
accounts.email = {
accounts."christoph@schmatzler.com" = {
primary = true;
maildir.path = "christoph@schmatzler.com";
address = "christoph@schmatzler.com";
userName = "christoph.schmatzler@icloud.com";
realName = "Christoph Schmatzler";
passwordCommand = ["cat" "/run/secrets/tahani-email-password"];
folders = {
inbox = "INBOX";
drafts = "Drafts";
sent = "Sent Messages";
trash = "Deleted Messages";
};
smtp = {
host = "smtp.mail.me.com";
port = 587;
tls.useStartTls = true;
};
himalaya.enable = true;
mbsync = {
enable = true;
create = "both";
expunge = "both";
};
imap = {
host = "imap.mail.me.com";
port = 993;
tls.enable = true;
};
};
};
};
}

View File

@@ -1,198 +0,0 @@
{
config,
lib,
pkgs,
...
}:
with lib; let
cfg = config.my.gitea;
in {
options.my.gitea = {
enable = mkEnableOption "Gitea git hosting service";
litestream = {
bucket =
mkOption {
type = types.str;
description = "S3 bucket name for Litestream database replication";
};
secretFile =
mkOption {
type = types.path;
description = "Path to the environment file containing S3 credentials for Litestream";
};
};
restic = {
bucket =
mkOption {
type = types.str;
description = "S3 bucket name for Restic repository backups";
};
passwordFile =
mkOption {
type = types.path;
description = "Path to the file containing the Restic repository password";
};
environmentFile =
mkOption {
type = types.path;
description = "Path to the environment file containing S3 credentials for Restic";
};
};
s3 = {
endpoint =
mkOption {
type = types.str;
default = "s3.eu-central-003.backblazeb2.com";
description = "S3 endpoint URL";
};
};
};
config =
mkIf cfg.enable {
networking.firewall.allowedTCPPorts = [80 443];
services.redis.servers.gitea = {
enable = true;
port = 6380;
bind = "127.0.0.1";
settings = {
maxmemory = "64mb";
maxmemory-policy = "allkeys-lru";
};
};
services.gitea = {
enable = true;
database = {
type = "sqlite3";
path = "/var/lib/gitea/data/gitea.db";
};
settings = {
server = {
ROOT_URL = "https://git.schmatzler.com/";
DOMAIN = "git.schmatzler.com";
HTTP_ADDR = "127.0.0.1";
HTTP_PORT = 3000;
LANDING_PAGE = "explore";
};
service.DISABLE_REGISTRATION = true;
security.INSTALL_LOCK = true;
cache = {
ADAPTER = "redis";
HOST = "redis://127.0.0.1:6380/0?pool_size=100&idle_timeout=180s";
ITEM_TTL = "16h";
};
"cache.last_commit" = {
ITEM_TTL = "8760h";
COMMITS_COUNT = 100;
};
session = {
PROVIDER = "redis";
PROVIDER_CONFIG = "redis://127.0.0.1:6380/1?pool_size=100&idle_timeout=180s";
COOKIE_SECURE = true;
SAME_SITE = "strict";
};
api.ENABLE_SWAGGER = false;
};
};
services.litestream = {
enable = true;
environmentFile = cfg.litestream.secretFile;
settings = {
dbs = [
{
path = "/var/lib/gitea/data/gitea.db";
replicas = [
{
type = "s3";
bucket = cfg.litestream.bucket;
path = "gitea";
endpoint = cfg.s3.endpoint;
}
];
}
];
};
};
systemd.services.litestream = {
serviceConfig = {
User = mkForce "gitea";
Group = mkForce "gitea";
};
};
services.caddy = {
enable = true;
virtualHosts."git.schmatzler.com".extraConfig = ''
header {
Strict-Transport-Security "max-age=31536000; includeSubDomains"
X-Content-Type-Options "nosniff"
X-Frame-Options "DENY"
Referrer-Policy "strict-origin-when-cross-origin"
}
reverse_proxy localhost:3000
'';
};
services.restic.backups.gitea = {
repository = "s3:${cfg.s3.endpoint}/${cfg.restic.bucket}";
paths = ["/var/lib/gitea"];
exclude = [
"/var/lib/gitea/log"
"/var/lib/gitea/data/gitea.db"
"/var/lib/gitea/data/gitea.db-shm"
"/var/lib/gitea/data/gitea.db-wal"
];
passwordFile = cfg.restic.passwordFile;
environmentFile = cfg.restic.environmentFile;
pruneOpts = [
"--keep-daily 7"
"--keep-weekly 4"
"--keep-monthly 6"
];
timerConfig = {
OnCalendar = "daily";
Persistent = true;
RandomizedDelaySec = "1h";
};
};
systemd.services.restic-backups-gitea = {
wants = ["restic-init-gitea.service"];
after = ["restic-init-gitea.service"];
serviceConfig = {
User = mkForce "gitea";
Group = mkForce "gitea";
};
};
systemd.services.restic-init-gitea = {
description = "Initialize Restic repository for Gitea backups";
wantedBy = ["multi-user.target"];
after = ["network-online.target"];
wants = ["network-online.target"];
path = [pkgs.restic];
serviceConfig = {
Type = "oneshot";
User = "gitea";
Group = "gitea";
RemainAfterExit = true;
EnvironmentFile = cfg.restic.environmentFile;
};
script = ''
export RESTIC_PASSWORD=$(cat ${cfg.restic.passwordFile})
restic -r s3:${cfg.s3.endpoint}/${cfg.restic.bucket} snapshots &>/dev/null || \
restic -r s3:${cfg.s3.endpoint}/${cfg.restic.bucket} init
'';
};
};
}

6
modules/hosts.nix Normal file
View File

@@ -0,0 +1,6 @@
{...}: {
den.hosts.aarch64-darwin.chidi.users.cschmatzler = {};
den.hosts.aarch64-darwin.jason.users.cschmatzler = {};
den.hosts.x86_64-linux.michael.users.cschmatzler = {};
den.hosts.x86_64-linux.tahani.users.cschmatzler = {};
}

25
modules/jason.nix Normal file
View File

@@ -0,0 +1,25 @@
{den, ...}: {
den.aspects.jason.includes = [
den.aspects.darwin-system
den.aspects.core
den.aspects.tailscale
den.aspects.desktop
den.aspects.terminal
den.aspects.atuin
den.aspects.dev-tools
den.aspects.neovim
den.aspects.ai-tools
den.aspects.zellij
den.aspects.zk
];
den.aspects.jason.darwin = {...}: {
networking.hostName = "jason";
networking.computerName = "jason";
};
den.aspects.jason.homeManager = {...}: {
fonts.fontconfig.enable = true;
programs.git.settings.user.email = "christoph@schmatzler.com";
};
}

191
modules/michael.nix Normal file
View File

@@ -0,0 +1,191 @@
{
inputs,
den,
...
}: {
den.aspects.michael.includes = [
den.aspects.nixos-system
den.aspects.core
den.aspects.openssh
den.aspects.fail2ban
den.aspects.tailscale
];
den.aspects.michael.nixos = {
config,
pkgs,
lib,
modulesPath,
...
}: {
imports = [
(modulesPath + "/installer/scan/not-detected.nix")
(modulesPath + "/profiles/qemu-guest.nix")
./_hosts/michael/disk-config.nix
./_hosts/michael/hardware-configuration.nix
inputs.disko.nixosModules.default
];
networking.hostName = "michael";
sops.secrets = {
michael-gitea-litestream = {
sopsFile = ../secrets/michael-gitea-litestream;
format = "binary";
owner = "gitea";
group = "gitea";
};
michael-gitea-restic-password = {
sopsFile = ../secrets/michael-gitea-restic-password;
format = "binary";
owner = "gitea";
group = "gitea";
};
michael-gitea-restic-env = {
sopsFile = ../secrets/michael-gitea-restic-env;
format = "binary";
owner = "gitea";
group = "gitea";
};
};
networking.firewall.allowedTCPPorts = [80 443];
services.redis.servers.gitea = {
enable = true;
port = 6380;
bind = "127.0.0.1";
settings = {
maxmemory = "64mb";
maxmemory-policy = "allkeys-lru";
};
};
services.gitea = {
enable = true;
database = {
type = "sqlite3";
path = "/var/lib/gitea/data/gitea.db";
};
settings = {
server = {
ROOT_URL = "https://git.schmatzler.com/";
DOMAIN = "git.schmatzler.com";
HTTP_ADDR = "127.0.0.1";
HTTP_PORT = 3000;
LANDING_PAGE = "explore";
};
service.DISABLE_REGISTRATION = true;
security.INSTALL_LOCK = true;
cache = {
ADAPTER = "redis";
HOST = "redis://127.0.0.1:6380/0?pool_size=100&idle_timeout=180s";
ITEM_TTL = "16h";
};
"cache.last_commit" = {
ITEM_TTL = "8760h";
COMMITS_COUNT = 100;
};
session = {
PROVIDER = "redis";
PROVIDER_CONFIG = "redis://127.0.0.1:6380/1?pool_size=100&idle_timeout=180s";
COOKIE_SECURE = true;
SAME_SITE = "strict";
};
api.ENABLE_SWAGGER = false;
};
};
services.litestream = {
enable = true;
environmentFile = config.sops.secrets.michael-gitea-litestream.path;
settings = {
dbs = [
{
path = "/var/lib/gitea/data/gitea.db";
replicas = [
{
type = "s3";
bucket = "michael-gitea-litestream";
path = "gitea";
endpoint = "s3.eu-central-003.backblazeb2.com";
}
];
}
];
};
};
systemd.services.litestream = {
serviceConfig = {
User = lib.mkForce "gitea";
Group = lib.mkForce "gitea";
};
};
services.caddy = {
enable = true;
virtualHosts."git.schmatzler.com".extraConfig = ''
header {
Strict-Transport-Security "max-age=31536000; includeSubDomains"
X-Content-Type-Options "nosniff"
X-Frame-Options "DENY"
Referrer-Policy "strict-origin-when-cross-origin"
}
reverse_proxy localhost:3000
'';
};
services.restic.backups.gitea = {
repository = "s3:s3.eu-central-003.backblazeb2.com/michael-gitea-repositories";
paths = ["/var/lib/gitea"];
exclude = [
"/var/lib/gitea/log"
"/var/lib/gitea/data/gitea.db"
"/var/lib/gitea/data/gitea.db-shm"
"/var/lib/gitea/data/gitea.db-wal"
];
passwordFile = config.sops.secrets.michael-gitea-restic-password.path;
environmentFile = config.sops.secrets.michael-gitea-restic-env.path;
pruneOpts = [
"--keep-daily 7"
"--keep-weekly 4"
"--keep-monthly 6"
];
timerConfig = {
OnCalendar = "daily";
Persistent = true;
RandomizedDelaySec = "1h";
};
};
systemd.services.restic-backups-gitea = {
wants = ["restic-init-gitea.service"];
after = ["restic-init-gitea.service"];
serviceConfig = {
User = lib.mkForce "gitea";
Group = lib.mkForce "gitea";
};
};
systemd.services.restic-init-gitea = {
description = "Initialize Restic repository for Gitea backups";
wantedBy = ["multi-user.target"];
after = ["network-online.target"];
wants = ["network-online.target"];
path = [pkgs.restic];
serviceConfig = {
Type = "oneshot";
User = "gitea";
Group = "gitea";
RemainAfterExit = true;
EnvironmentFile = config.sops.secrets.michael-gitea-restic-env.path;
};
script = ''
export RESTIC_PASSWORD=$(cat ${config.sops.secrets.michael-gitea-restic-password.path})
restic -r s3:s3.eu-central-003.backblazeb2.com/michael-gitea-repositories snapshots &>/dev/null || \
restic -r s3:s3.eu-central-003.backblazeb2.com/michael-gitea-repositories init
'';
};
};
}

8
modules/neovim.nix Normal file
View File

@@ -0,0 +1,8 @@
{inputs, ...}: {
den.aspects.neovim.homeManager = {pkgs, ...}: {
imports = [
inputs.nixvim.homeModules.nixvim
./_neovim/default.nix
];
};
}

67
modules/network.nix Normal file
View File

@@ -0,0 +1,67 @@
{...}: {
den.aspects.openssh.nixos = {
services.openssh = {
enable = true;
settings = {
PermitRootLogin = "no";
PasswordAuthentication = false;
};
};
};
den.aspects.fail2ban.nixos = {
services.fail2ban = {
enable = true;
maxretry = 5;
bantime = "10m";
bantime-increment = {
enable = true;
multipliers = "1 2 4 8 16 32 64";
maxtime = "168h";
overalljails = true;
};
jails = {
sshd = {
settings = {
enabled = true;
port = "ssh";
filter = "sshd";
maxretry = 3;
};
};
gitea = {
settings = {
enabled = true;
filter = "gitea";
logpath = "/var/lib/gitea/log/gitea.log";
maxretry = 10;
findtime = 3600;
bantime = 900;
action = "iptables-allports";
};
};
};
};
environment.etc."fail2ban/filter.d/gitea.local".text = ''
[Definition]
failregex = .*(Failed authentication attempt|invalid credentials|Attempted access of unknown user).* from <HOST>
ignoreregex =
'';
};
den.aspects.tailscale.nixos = {
services.tailscale = {
enable = true;
openFirewall = true;
permitCertUid = "caddy";
useRoutingFeatures = "server";
};
};
den.aspects.tailscale.darwin = {
services.tailscale = {
enable = true;
};
};
}

87
modules/nixos-system.nix Normal file
View File

@@ -0,0 +1,87 @@
{inputs, ...}: {
den.aspects.nixos-system.nixos = {pkgs, ...}: {
imports = [inputs.home-manager.nixosModules.home-manager];
security.sudo.enable = true;
security.sudo.extraRules = [
{
users = ["cschmatzler"];
commands = [
{
command = "/run/current-system/sw/bin/nix-env";
options = ["NOPASSWD"];
}
{
command = "/nix/store/*/bin/switch-to-configuration";
options = ["NOPASSWD"];
}
{
command = "/nix/store/*/bin/activate";
options = ["NOPASSWD"];
}
{
command = "/nix/store/*/bin/activate-rs";
options = ["NOPASSWD"];
}
{
command = "/nix/store/*/bin/wait-activate";
options = ["NOPASSWD"];
}
];
}
];
time.timeZone = "UTC";
nix = {
settings.trusted-users = ["cschmatzler"];
gc.dates = "weekly";
nixPath = ["nixos-config=/home/cschmatzler/.local/share/src/nixos-config:/etc/nixos"];
};
boot = {
loader = {
systemd-boot = {
enable = true;
configurationLimit = 42;
};
efi.canTouchEfiVariables = true;
};
initrd.availableKernelModules = [
"xhci_pci"
"ahci"
"nvme"
"usbhid"
"usb_storage"
"sd_mod"
];
kernelPackages = pkgs.linuxPackages_latest;
};
users.users = {
cschmatzler = {
isNormalUser = true;
home = "/home/cschmatzler";
extraGroups = [
"wheel"
"sudo"
"network"
"systemd-journal"
];
shell = pkgs.nushell;
openssh.authorizedKeys.keys = [
"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIHfRZQ+7ejD3YHbyMTrV0gN1Gc0DxtGgl5CVZSupo5ws"
"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIL/I+/2QT47raegzMIyhwMEPKarJP/+Ox9ewA4ZFJwk/"
];
};
root = {
openssh.authorizedKeys.keys = [
"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIHfRZQ+7ejD3YHbyMTrV0gN1Gc0DxtGgl5CVZSupo5ws"
"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIL/I+/2QT47raegzMIyhwMEPKarJP/+Ox9ewA4ZFJwk/"
];
};
};
home-manager.useGlobalPkgs = true;
};
}

29
modules/overlays.nix Normal file
View File

@@ -0,0 +1,29 @@
{inputs, ...}: let
overlays = [
# himalaya
(final: prev: {
himalaya = inputs.himalaya.packages.${prev.stdenv.hostPlatform.system}.default;
})
# jj-ryu (uses build-rust-package helper)
(final: prev: {
jj-ryu =
import ./_lib/build-rust-package.nix {
inherit inputs prev;
input = inputs.jj-ryu;
};
})
# jj-starship (passes through upstream overlay)
inputs.jj-starship.overlays.default
# zjstatus
(final: prev: {
zjstatus = inputs.zjstatus.packages.${prev.stdenv.hostPlatform.system}.default;
})
# tuicr
(final: prev: {
tuicr = inputs.tuicr.defaultPackage.${prev.stdenv.hostPlatform.system};
})
];
in {
den.default.nixos.nixpkgs.overlays = overlays;
den.default.darwin.nixpkgs.overlays = overlays;
}

View File

@@ -1,257 +0,0 @@
{
config,
lib,
pkgs,
...
}:
with lib; let
cfg = config.my.pgbackrest;
in {
options.my.pgbackrest = {
enable = mkEnableOption "pgBackRest PostgreSQL backup";
stanza =
mkOption {
type = types.str;
default = "main";
description = "Name of the pgBackRest stanza";
};
secretFile =
mkOption {
type = types.path;
description = "Path to the environment file containing S3 credentials and cipher passphrase";
};
s3 =
mkOption {
type =
types.submodule {
options = {
endpoint =
mkOption {
type = types.str;
default = "s3.eu-central-003.backblazeb2.com";
description = "S3 endpoint URL";
};
bucket =
mkOption {
type = types.str;
description = "S3 bucket name";
};
region =
mkOption {
type = types.str;
default = "eu-central-003";
description = "S3 region";
};
path =
mkOption {
type = types.str;
default = "/backups";
description = "Path within the S3 bucket";
};
};
};
default = {};
description = "S3 storage configuration";
};
retention =
mkOption {
type =
types.submodule {
options = {
full =
mkOption {
type = types.int;
default = 7;
description = "Number of full backups to retain";
};
diff =
mkOption {
type = types.int;
default = 7;
description = "Number of differential backups to retain";
};
};
};
default = {};
description = "Backup retention configuration";
};
compression =
mkOption {
type =
types.submodule {
options = {
type =
mkOption {
type = types.str;
default = "zst";
description = "Compression algorithm (none, gz, lz4, zst)";
};
level =
mkOption {
type = types.int;
default = 3;
description = "Compression level";
};
};
};
default = {};
description = "Compression configuration";
};
processMax =
mkOption {
type = types.int;
default = 2;
description = "Maximum number of processes for parallel operations";
};
schedule =
mkOption {
type =
types.submodule {
options = {
full =
mkOption {
type = types.str;
default = "daily";
description = "OnCalendar expression for full backups";
};
diff =
mkOption {
type = types.str;
default = "hourly";
description = "OnCalendar expression for differential backups";
};
};
};
default = {};
description = "Backup schedule configuration";
};
};
config =
mkIf cfg.enable (let
archivePushScript =
pkgs.writeShellScript "pgbackrest-archive-push" ''
set -a
source ${cfg.secretFile}
set +a
exec ${pkgs.pgbackrest}/bin/pgbackrest --stanza=${cfg.stanza} archive-push "$1"
'';
in {
environment.systemPackages = [
pkgs.pgbackrest
(pkgs.writeShellScriptBin "pgbackrest-wrapper" ''
set -a
source ${cfg.secretFile}
set +a
exec ${pkgs.pgbackrest}/bin/pgbackrest "$@"
'')
];
services.postgresql.settings = {
archive_mode = "on";
archive_command = "${archivePushScript} %p";
};
environment.etc."pgbackrest/pgbackrest.conf".text = ''
[global]
repo1-type=s3
repo1-s3-endpoint=${cfg.s3.endpoint}
repo1-s3-bucket=${cfg.s3.bucket}
repo1-s3-region=${cfg.s3.region}
repo1-path=${cfg.s3.path}
repo1-retention-full=${toString cfg.retention.full}
repo1-retention-diff=${toString cfg.retention.diff}
repo1-cipher-type=aes-256-cbc
compress-type=${cfg.compression.type}
compress-level=${toString cfg.compression.level}
process-max=${toString cfg.processMax}
log-level-console=info
log-level-file=detail
log-path=/var/log/pgbackrest
spool-path=/var/spool/pgbackrest
[${cfg.stanza}]
pg1-path=/var/lib/postgresql/${config.services.postgresql.package.psqlSchema}
pg1-user=postgres
'';
systemd.services.pgbackrest-stanza-create = {
description = "pgBackRest Stanza Create";
after = ["postgresql.service"];
requires = ["postgresql.service"];
path = [pkgs.pgbackrest];
serviceConfig = {
Type = "oneshot";
User = "postgres";
EnvironmentFile = cfg.secretFile;
RemainAfterExit = true;
};
script = ''
pgbackrest --stanza=${cfg.stanza} stanza-create || true
'';
};
systemd.services.pgbackrest-backup = {
description = "pgBackRest Full Backup";
after = ["postgresql.service" "pgbackrest-stanza-create.service"];
requires = ["postgresql.service"];
wants = ["pgbackrest-stanza-create.service"];
path = [pkgs.pgbackrest];
serviceConfig = {
Type = "oneshot";
User = "postgres";
EnvironmentFile = cfg.secretFile;
};
script = ''
pgbackrest --stanza=${cfg.stanza} backup --type=full
'';
};
systemd.timers.pgbackrest-backup = {
wantedBy = ["timers.target"];
timerConfig = {
OnCalendar = cfg.schedule.full;
Persistent = true;
RandomizedDelaySec = "1h";
};
};
systemd.services.pgbackrest-backup-diff = {
description = "pgBackRest Differential Backup";
after = ["postgresql.service" "pgbackrest-stanza-create.service"];
requires = ["postgresql.service"];
wants = ["pgbackrest-stanza-create.service"];
path = [pkgs.pgbackrest];
serviceConfig = {
Type = "oneshot";
User = "postgres";
EnvironmentFile = cfg.secretFile;
};
script = ''
pgbackrest --stanza=${cfg.stanza} backup --type=diff
'';
};
systemd.timers.pgbackrest-backup-diff = {
wantedBy = ["timers.target"];
timerConfig = {
OnCalendar = cfg.schedule.diff;
Persistent = true;
RandomizedDelaySec = "5m";
};
};
systemd.tmpfiles.rules = [
"d /var/lib/pgbackrest 0750 postgres postgres -"
"d /var/log/pgbackrest 0750 postgres postgres -"
"d /var/spool/pgbackrest 0750 postgres postgres -"
];
});
}

15
modules/secrets.nix Normal file
View File

@@ -0,0 +1,15 @@
{inputs, ...}: {
# Import sops-nix modules into den.default per-class
den.default.nixos.imports = [inputs.sops-nix.nixosModules.sops];
den.default.darwin.imports = [inputs.sops-nix.darwinModules.sops];
# Configure NixOS SOPS defaults
den.default.nixos.sops.age.sshKeyPaths = ["/etc/ssh/ssh_host_ed25519_key"];
# Configure Darwin SOPS defaults
den.default.darwin = {
sops.age.keyFile = "/Users/cschmatzler/.config/sops/age/keys.txt";
sops.age.sshKeyPaths = [];
sops.gnupg.sshKeyPaths = [];
};
}

285
modules/shell.nix Normal file
View File

@@ -0,0 +1,285 @@
{...}: {
den.aspects.shell.homeManager = {
lib,
pkgs,
...
}: {
programs.nushell = {
enable = true;
settings = {
show_banner = false;
completions = {
algorithm = "fuzzy";
case_sensitive = false;
};
history = {
file_format = "sqlite";
};
};
environmentVariables = {
COLORTERM = "truecolor";
COLORFGBG = "15;0";
TERM_BACKGROUND = "light";
EDITOR = "nvim";
};
extraEnv =
''
$env.LS_COLORS = (${pkgs.vivid}/bin/vivid generate catppuccin-latte)
''
+ lib.optionalString pkgs.stdenv.isDarwin ''
# Nushell on Darwin doesn't source /etc/zprofile or path_helper,
# so nix-managed paths must be added explicitly.
$env.PATH = ($env.PATH | split row (char esep) | prepend "/run/current-system/sw/bin" | prepend $"($env.HOME)/.nix-profile/bin")
'';
extraConfig = ''
# --- Catppuccin Latte Theme ---
let theme = {
rosewater: "#dc8a78"
flamingo: "#dd7878"
pink: "#ea76cb"
mauve: "#8839ef"
red: "#d20f39"
maroon: "#e64553"
peach: "#fe640b"
yellow: "#df8e1d"
green: "#40a02b"
teal: "#179299"
sky: "#04a5e5"
sapphire: "#209fb5"
blue: "#1e66f5"
lavender: "#7287fd"
text: "#4c4f69"
subtext1: "#5c5f77"
subtext0: "#6c6f85"
overlay2: "#7c7f93"
overlay1: "#8c8fa1"
overlay0: "#9ca0b0"
surface2: "#acb0be"
surface1: "#bcc0cc"
surface0: "#ccd0da"
base: "#eff1f5"
mantle: "#e6e9ef"
crust: "#dce0e8"
}
let scheme = {
recognized_command: $theme.blue
unrecognized_command: $theme.text
constant: $theme.peach
punctuation: $theme.overlay2
operator: $theme.sky
string: $theme.green
virtual_text: $theme.surface2
variable: { fg: $theme.flamingo attr: i }
filepath: $theme.yellow
}
$env.config.color_config = {
separator: { fg: $theme.surface2 attr: b }
leading_trailing_space_bg: { fg: $theme.lavender attr: u }
header: { fg: $theme.text attr: b }
row_index: $scheme.virtual_text
record: $theme.text
list: $theme.text
hints: $scheme.virtual_text
search_result: { fg: $theme.base bg: $theme.yellow }
shape_closure: $theme.teal
closure: $theme.teal
shape_flag: { fg: $theme.maroon attr: i }
shape_matching_brackets: { attr: u }
shape_garbage: $theme.red
shape_keyword: $theme.mauve
shape_match_pattern: $theme.green
shape_signature: $theme.teal
shape_table: $scheme.punctuation
cell-path: $scheme.punctuation
shape_list: $scheme.punctuation
shape_record: $scheme.punctuation
shape_vardecl: $scheme.variable
shape_variable: $scheme.variable
empty: { attr: n }
filesize: {||
if $in < 1kb {
$theme.teal
} else if $in < 10kb {
$theme.green
} else if $in < 100kb {
$theme.yellow
} else if $in < 10mb {
$theme.peach
} else if $in < 100mb {
$theme.maroon
} else if $in < 1gb {
$theme.red
} else {
$theme.mauve
}
}
duration: {||
if $in < 1day {
$theme.teal
} else if $in < 1wk {
$theme.green
} else if $in < 4wk {
$theme.yellow
} else if $in < 12wk {
$theme.peach
} else if $in < 24wk {
$theme.maroon
} else if $in < 52wk {
$theme.red
} else {
$theme.mauve
}
}
datetime: {|| (date now) - $in |
if $in < 1day {
$theme.teal
} else if $in < 1wk {
$theme.green
} else if $in < 4wk {
$theme.yellow
} else if $in < 12wk {
$theme.peach
} else if $in < 24wk {
$theme.maroon
} else if $in < 52wk {
$theme.red
} else {
$theme.mauve
}
}
shape_external: $scheme.unrecognized_command
shape_internalcall: $scheme.recognized_command
shape_external_resolved: $scheme.recognized_command
shape_block: $scheme.recognized_command
block: $scheme.recognized_command
shape_custom: $theme.pink
custom: $theme.pink
background: $theme.base
foreground: $theme.text
cursor: { bg: $theme.rosewater fg: $theme.base }
shape_range: $scheme.operator
range: $scheme.operator
shape_pipe: $scheme.operator
shape_operator: $scheme.operator
shape_redirection: $scheme.operator
glob: $scheme.filepath
shape_directory: $scheme.filepath
shape_filepath: $scheme.filepath
shape_glob_interpolation: $scheme.filepath
shape_globpattern: $scheme.filepath
shape_int: $scheme.constant
int: $scheme.constant
bool: $scheme.constant
float: $scheme.constant
nothing: $scheme.constant
binary: $scheme.constant
shape_nothing: $scheme.constant
shape_bool: $scheme.constant
shape_float: $scheme.constant
shape_binary: $scheme.constant
shape_datetime: $scheme.constant
shape_literal: $scheme.constant
string: $scheme.string
shape_string: $scheme.string
shape_string_interpolation: $theme.flamingo
shape_raw_string: $scheme.string
shape_externalarg: $scheme.string
}
$env.config.highlight_resolved_externals = true
$env.config.explore = {
status_bar_background: { fg: $theme.text, bg: $theme.mantle },
command_bar_text: { fg: $theme.text },
highlight: { fg: $theme.base, bg: $theme.yellow },
status: {
error: $theme.red,
warn: $theme.yellow,
info: $theme.blue,
},
selected_cell: { bg: $theme.blue fg: $theme.base },
}
# --- Custom Commands ---
def --env open_project [] {
let base = ($env.HOME | path join "Projects")
let choice = (
${pkgs.fd}/bin/fd -t d -d 1 -a . ($base | path join "Personal") ($base | path join "Work")
| lines
| each {|p| $p | str replace $"($base)/" "" }
| str join "\n"
| ${pkgs.fzf}/bin/fzf --prompt "project > "
)
if ($choice | str trim | is-not-empty) {
cd ($base | path join ($choice | str trim))
}
}
# --- Keybinding: Ctrl+O for open_project ---
$env.config.keybindings = ($env.config.keybindings | append [
{
name: open_project
modifier: control
keycode: char_o
mode: [emacs vi_insert vi_normal]
event: {
send: executehostcommand
cmd: "open_project"
}
}
])
'';
};
programs.zsh = {
enable = true;
};
programs.starship = {
enable = true;
enableNushellIntegration = true;
settings = {
format = "$directory\${custom.scm}$hostname$line_break$character";
buf = {
disabled = true;
};
character = {
error_symbol = "[󰘧](bold red)";
success_symbol = "[󰘧](bold green)";
};
directory = {
truncate_to_repo = false;
};
git_branch = {
disabled = true;
symbol = " ";
truncation_length = 18;
};
git_status = {
disabled = true;
};
git_commit = {
disabled = true;
};
git_state = {
disabled = true;
};
custom.scm = {
when = "jj-starship detect";
shell = ["jj-starship" "--strip-bookmark-prefix" "cschmatzler/" "--truncate-name" "20" "--bookmarks-display-limit" "1"];
format = "$output ";
};
lua = {
symbol = " ";
};
package = {
disabled = true;
};
};
};
};
}

27
modules/ssh-client.nix Normal file
View File

@@ -0,0 +1,27 @@
{...}: {
den.aspects.ssh-client.homeManager = {
config,
lib,
pkgs,
...
}: {
programs.ssh = {
enable = true;
enableDefaultConfig = false;
includes = [
(lib.mkIf pkgs.stdenv.hostPlatform.isLinux "/home/${config.home.username}/.ssh/config_external")
(lib.mkIf pkgs.stdenv.hostPlatform.isDarwin "/Users/${config.home.username}/.ssh/config_external")
];
matchBlocks = {
"*" = {};
"github.com" = {
identitiesOnly = true;
identityFile = [
(lib.mkIf pkgs.stdenv.hostPlatform.isLinux "/home/${config.home.username}/.ssh/id_ed25519")
(lib.mkIf pkgs.stdenv.hostPlatform.isDarwin "/Users/${config.home.username}/.ssh/id_ed25519")
];
};
};
};
};
}

89
modules/tahani.nix Normal file
View File

@@ -0,0 +1,89 @@
{den, ...}: {
den.aspects.tahani.includes = [
den.aspects.nixos-system
den.aspects.core
den.aspects.openssh
den.aspects.tailscale
den.aspects.terminal
den.aspects.email
den.aspects.atuin
den.aspects.dev-tools
den.aspects.neovim
den.aspects.ai-tools
den.aspects.zellij
den.aspects.zk
];
den.aspects.tahani.nixos = {...}: {
imports = [
./_hosts/tahani/adguardhome.nix
./_hosts/tahani/cache.nix
./_hosts/tahani/networking.nix
./_hosts/tahani/paperless.nix
];
networking.hostName = "tahani";
sops.secrets = {
tahani-paperless-password = {
sopsFile = ../secrets/tahani-paperless-password;
format = "binary";
};
tahani-email-password = {
sopsFile = ../secrets/tahani-email-password;
format = "binary";
owner = "cschmatzler";
};
};
virtualisation.docker.enable = true;
users.users.cschmatzler.extraGroups = ["docker"];
swapDevices = [
{
device = "/swapfile";
size = 16 * 1024;
}
];
};
den.aspects.tahani.homeManager = {
pkgs,
inputs',
...
}: let
opencode = inputs'.llm-agents.packages.opencode;
in {
programs.git.settings.user.email = "christoph@schmatzler.com";
# Auto-start zellij in nushell on tahani (headless server)
programs.nushell.extraConfig = ''
if 'ZELLIJ' not-in ($env | columns) {
zellij
}
'';
# Inbox-triage systemd service
systemd.user.services.opencode-inbox-triage = {
Unit = {
Description = "OpenCode inbox triage";
};
Service = {
Type = "oneshot";
ExecStart = "${opencode}/bin/opencode run --command inbox-triage";
Environment = "PATH=${pkgs.himalaya}/bin:${opencode}/bin:${pkgs.coreutils}/bin";
};
};
systemd.user.timers.opencode-inbox-triage = {
Unit = {
Description = "Run OpenCode inbox triage every 10 minutes";
};
Timer = {
OnCalendar = "*:0/10";
Persistent = true;
};
Install = {
WantedBy = ["timers.target"];
};
};
};
}

129
modules/terminal.nix Normal file
View File

@@ -0,0 +1,129 @@
{...}: {
den.aspects.terminal.homeManager = {pkgs, ...}: {
xdg.configFile."ghostty/config".text = ''
command = ${pkgs.nushell}/bin/nu
theme = Catppuccin Latte
window-padding-x = 12
window-padding-y = 3
window-padding-balance = true
font-family = TX-02
font-size = 16.5
cursor-style = block
mouse-hide-while-typing = true
mouse-scroll-multiplier = 1.25
shell-integration = none
shell-integration-features = no-cursor
clipboard-read = allow
clipboard-write = allow
'';
programs.bat = {
enable = true;
config = {
theme = "Catppuccin Latte";
pager = "ov";
};
themes = {
"Catppuccin Latte" = {
src =
pkgs.fetchFromGitHub {
owner = "catppuccin";
repo = "bat";
rev = "6810349b28055dce54076712fc05fc68da4b8ec0";
sha256 = "lJapSgRVENTrbmpVyn+UQabC9fpV1G1e+CdlJ090uvg=";
};
file = "themes/Catppuccin Latte.tmTheme";
};
};
};
programs.fzf = {
enable = true;
};
home.sessionVariables = {
FZF_DEFAULT_OPTS = ''
--bind=alt-k:up,alt-j:down
--expect=tab,enter
--layout=reverse
--delimiter='\t'
--with-nth=1
--preview-window='border-rounded' --prompt=' ' --marker=' ' --pointer=' '
--separator='' --scrollbar='' --layout='reverse'
--color=bg+:#CCD0DA,bg:#EFF1F5,spinner:#DC8A78,hl:#D20F39
--color=fg:#4C4F69,header:#D20F39,info:#8839EF,pointer:#DC8A78
--color=marker:#7287FD,fg+:#4C4F69,prompt:#8839EF,hl+:#D20F39
--color=selected-bg:#BCC0CC
--color=border:#9CA0B0,label:#4C4F69
'';
};
programs.ripgrep = {
enable = true;
arguments = [
"--max-columns=150"
"--max-columns-preview"
"--hidden"
"--smart-case"
"--colors=column:none"
"--colors=column:fg:4"
"--colors=column:style:underline"
"--colors=line:none"
"--colors=line:fg:4"
"--colors=match:none"
"--colors=match:bg:0"
"--colors=match:fg:6"
"--colors=path:none"
"--colors=path:fg:14"
"--colors=path:style:bold"
];
};
programs.zoxide = {
enable = true;
enableNushellIntegration = true;
};
programs.yazi = {
enable = true;
enableNushellIntegration = true;
shellWrapperName = "y";
settings = {
manager = {
show_hidden = true;
sort_by = "natural";
sort_dir_first = true;
};
};
theme = {
tabs = {
sep_inner = {
open = "";
close = "";
};
sep_outer = {
open = "";
close = "";
};
};
indicator = {
padding = {
open = "";
close = "";
};
};
status = {
sep_left = {
open = "";
close = "";
};
sep_right = {
open = "";
close = "";
};
};
};
};
};
}

27
modules/user.nix Normal file
View File

@@ -0,0 +1,27 @@
{den, ...}: {
den.aspects.cschmatzler.includes = [
den.provides.primary-user
den.aspects.shell
den.aspects.ssh-client
];
den.aspects.cschmatzler.homeManager = {
lib,
pkgs,
inputs',
...
}: {
programs.home-manager.enable = true;
home.packages = pkgs.callPackage ./_lib/packages.nix {inputs = inputs';};
home.activation =
lib.mkIf pkgs.stdenv.isDarwin {
"setWallpaper" =
lib.hm.dag.entryAfter ["revealHomeLibraryDirectory"] ''
echo "[+] Setting wallpaper"
${import ./_lib/wallpaper.nix {inherit pkgs;}}/bin/set-wallpaper-script
'';
};
};
}

54
modules/zellij.nix Normal file
View File

@@ -0,0 +1,54 @@
{...}: {
den.aspects.zellij.homeManager = {pkgs, ...}: {
programs.zellij = {
enable = true;
settings = {
theme = "catppuccin-latte";
default_layout = "default";
default_shell = "${pkgs.nushell}/bin/nu";
pane_frames = false;
show_startup_tips = false;
show_release_notes = false;
};
};
xdg.configFile."zellij/layouts/default.kdl".text = ''
layout {
default_tab_template {
pane split_direction="vertical" {
pane
}
pane size=1 borderless=true {
plugin location="file:${pkgs.zjstatus}/bin/zjstatus.wasm" {
hide_frame_for_single_pane "true"
format_left "{mode}#[fg=#1e66f5,bg=#eff1f5,bold] {session}#[bg=#eff1f5] {tabs}"
format_right "{datetime}"
format_space "#[bg=#eff1f5]"
mode_normal "#[fg=#eff1f5,bg=#1e66f5] "
mode_locked "#[fg=#eff1f5,bg=#fe640b] L "
mode_tab "#[fg=#eff1f5,bg=#40a02b] T "
mode_pane "#[fg=#eff1f5,bg=#8839ef] P "
mode_session "#[fg=#eff1f5,bg=#04a5e5] S "
mode_resize "#[fg=#eff1f5,bg=#df8e1d] R "
mode_move "#[fg=#eff1f5,bg=#ea76cb] M "
mode_search "#[fg=#eff1f5,bg=#d20f39] S "
tab_normal "#[fg=#acb0be,bg=#eff1f5] {index} {name} {fullscreen_indicator}{sync_indicator}{floating_indicator}"
tab_active "#[fg=#eff1f5,bg=#1e66f5,bold,underline] {index} {name} {fullscreen_indicator}{sync_indicator}{floating_indicator}"
tab_fullscreen_indicator " "
tab_sync_indicator " "
tab_floating_indicator "󰉈 "
datetime "#[fg=#4c4f69,bg=#eff1f5] {format} "
datetime_format "%A, %d %b %Y %H:%M"
datetime_timezone "Europe/Berlin"
}
}
}
}
'';
};
}

11
modules/zk.nix Normal file
View File

@@ -0,0 +1,11 @@
{...}: {
den.aspects.zk.homeManager = {...}: {
programs.zk = {
enable = true;
settings = {};
};
home.sessionVariables = {
ZK_NOTEBOOK_DIR = "$HOME/Projects/Personal/Zettelkasten";
};
};
}