Kieran's opinionated (and probably slightly dumb) nix config
1{
2 lib,
3 pkgs,
4 config,
5 inputs,
6 ...
7}:
8let
9 tangled-setup = pkgs.writeShellScriptBin "tangled-setup" ''
10 # Configuration
11 default_plc_id="did:plc:krxbvxvis5skq7jj6eot23ul"
12 default_github_username="taciturnaxolotl"
13 default_knot_host="knot.dunkirk.sh"
14
15 # Verify git repository
16 if ! ${pkgs.git}/bin/git rev-parse --is-inside-work-tree &>/dev/null; then
17 ${pkgs.gum}/bin/gum style --foreground 196 "Not a git repository"
18 exit 1
19 fi
20
21 repo_name=$(basename "$(${pkgs.git}/bin/git rev-parse --show-toplevel)")
22 ${pkgs.gum}/bin/gum style --bold --foreground 212 "Configuring tangled remotes for: $repo_name"
23 echo
24
25 # Check current remotes
26 origin_url=$(${pkgs.git}/bin/git remote get-url origin 2>/dev/null)
27 github_url=$(${pkgs.git}/bin/git remote get-url github 2>/dev/null)
28 origin_is_knot=false
29 github_username="$default_github_username"
30
31 # Extract GitHub username from existing origin if it's GitHub
32 if [[ "$origin_url" == *"github.com"* ]]; then
33 github_username=$(echo "$origin_url" | ${pkgs.gnused}/bin/sed -E 's/.*github\.com[:/]([^/]+)\/.*$/\1/')
34 fi
35
36 # Check if origin points to knot
37 if [[ "$origin_url" == *"$default_knot_host"* ]]; then
38 origin_is_knot=true
39 ${pkgs.gum}/bin/gum style --foreground 35 "✓ Origin → knot ($origin_url)"
40 elif [[ -n "$origin_url" ]]; then
41 ${pkgs.gum}/bin/gum style --foreground 214 "! Origin → $origin_url (not knot)"
42 else
43 ${pkgs.gum}/bin/gum style --foreground 214 "! Origin not configured"
44 fi
45
46 # Check github remote
47 if [[ -n "$github_url" ]]; then
48 ${pkgs.gum}/bin/gum style --foreground 35 "✓ GitHub → $github_url"
49 else
50 ${pkgs.gum}/bin/gum style --foreground 214 "! GitHub remote not configured"
51 fi
52
53 echo
54
55 # Configure origin remote if needed
56 if [[ "$origin_is_knot" = false ]]; then
57 should_migrate=true
58 if [[ -n "$origin_url" ]]; then
59 ${pkgs.gum}/bin/gum confirm "Migrate origin from $origin_url to knot?" || should_migrate=false
60 fi
61
62 if [[ "$should_migrate" = true ]]; then
63 plc_id=$(${pkgs.gum}/bin/gum input --placeholder "$default_plc_id" --prompt "PLC ID: " --value "$default_plc_id")
64 plc_id=''${plc_id:-$default_plc_id}
65
66 if ${pkgs.git}/bin/git remote get-url origin &>/dev/null; then
67 ${pkgs.git}/bin/git remote remove origin
68 fi
69 ${pkgs.git}/bin/git remote add origin "git@$default_knot_host:''${plc_id}/''${repo_name}"
70 ${pkgs.gum}/bin/gum style --foreground 35 "✓ Configured origin → git@$default_knot_host:''${plc_id}/''${repo_name}"
71 fi
72 fi
73
74 # Configure github remote if needed
75 if [[ -z "$github_url" ]]; then
76 username=$(${pkgs.gum}/bin/gum input --placeholder "$github_username" --prompt "GitHub username: " --value "$github_username")
77 username=''${username:-$github_username}
78
79 ${pkgs.git}/bin/git remote add github "git@github.com:''${username}/''${repo_name}.git"
80 ${pkgs.gum}/bin/gum style --foreground 35 "✓ Configured github → git@github.com:''${username}/''${repo_name}.git"
81 fi
82
83 echo
84
85 # Configure default push remote
86 current_remote=$(${pkgs.git}/bin/git config --get branch.main.remote 2>/dev/null)
87 if [[ -z "$current_remote" ]]; then
88 if ${pkgs.gum}/bin/gum confirm "Set origin (knot) as default push remote?"; then
89 ${pkgs.git}/bin/git config branch.main.remote origin
90 ${pkgs.gum}/bin/gum style --foreground 35 "✓ Default push remote → origin"
91 fi
92 elif [[ "$current_remote" != "origin" ]]; then
93 ${pkgs.gum}/bin/gum style --foreground 117 "Current default: $current_remote"
94 if ${pkgs.gum}/bin/gum confirm "Change default push remote to origin (knot)?"; then
95 ${pkgs.git}/bin/git config branch.main.remote origin
96 ${pkgs.gum}/bin/gum style --foreground 35 "✓ Default push remote → origin"
97 fi
98 else
99 ${pkgs.gum}/bin/gum style --foreground 35 "✓ Default push remote is origin"
100 fi
101 '';
102
103 assh = pkgs.writeShellScriptBin "assh" ''
104 # SSH auto-reconnect
105 host=$1
106 port=$2
107
108 if [[ -z "$host" || -z "$port" ]]; then
109 ${pkgs.gum}/bin/gum style --foreground 196 "Usage: assh <host> <port>"
110 exit 1
111 fi
112
113 ${pkgs.gum}/bin/gum style --foreground 212 "Connecting to $host:$port (auto-reconnect enabled)..."
114
115 while true; do
116 ${pkgs.openssh}/bin/ssh -p "$port" -o "BatchMode yes" "$host" || {
117 ${pkgs.gum}/bin/gum style --foreground 214 "Connection lost. Reconnecting in 1s..."
118 sleep 1
119 }
120 done
121 '';
122
123 hackatime-summary = pkgs.writeShellScriptBin "hackatime-summary" ''
124 # Hackatime summary
125 user_id=""
126 use_waka=false
127
128 # Parse arguments
129 while [[ $# -gt 0 ]]; do
130 case "$1" in
131 --waka)
132 use_waka=true
133 shift
134 ;;
135 *)
136 user_id="$1"
137 shift
138 ;;
139 esac
140 done
141
142 if [[ -z "$user_id" ]]; then
143 user_id=$(${pkgs.gum}/bin/gum input --placeholder "Enter user ID" --prompt "User ID: ")
144 if [[ -z "$user_id" ]]; then
145 ${pkgs.gum}/bin/gum style --foreground 196 "No user ID provided"
146 exit 1
147 fi
148 fi
149
150 if [[ "$use_waka" = true ]]; then
151 host="waka.hackclub.com"
152 else
153 host="hackatime.hackclub.com"
154 fi
155
156 ${pkgs.gum}/bin/gum spin --spinner dot --title "Fetching summary from $host for $user_id..." -- \
157 ${pkgs.curl}/bin/curl -s -X 'GET' \
158 "https://$host/api/summary?user=''${user_id}&interval=month" \
159 -H 'accept: application/json' \
160 -H 'Authorization: Bearer 2ce9e698-8a16-46f0-b49a-ac121bcfd608' \
161 > /tmp/hackatime-$$.json
162
163 ${pkgs.gum}/bin/gum style --bold --foreground 212 "Summary for $user_id"
164 echo
165
166 # Extract and display total time
167 total_seconds=$(${pkgs.jq}/bin/jq -r '
168 if (.categories | length) > 0 then
169 (.categories | map(.total) | add)
170 elif (.projects | length) > 0 then
171 (.projects | map(.total) | add)
172 else
173 0
174 end
175 ' /tmp/hackatime-$$.json)
176
177 if [[ "$total_seconds" -gt 0 ]]; then
178 hours=$((total_seconds / 3600))
179 minutes=$(((total_seconds % 3600) / 60))
180 seconds=$((total_seconds % 60))
181 ${pkgs.gum}/bin/gum style --foreground 35 "Total time: ''${hours}h ''${minutes}m ''${seconds}s"
182 else
183 ${pkgs.gum}/bin/gum style --foreground 214 "No activity recorded"
184 fi
185
186 echo
187
188 # Top projects
189 ${pkgs.gum}/bin/gum style --bold "Top Projects:"
190 ${pkgs.jq}/bin/jq -r '
191 if (.projects | length) > 0 then
192 .projects | sort_by(-.total) | .[0:10] | .[] |
193 " \(.key): \((.total / 3600 | floor))h \(((.total % 3600) / 60) | floor)m"
194 else
195 " No projects"
196 end
197 ' /tmp/hackatime-$$.json
198
199 echo
200
201 # Top languages
202 ${pkgs.gum}/bin/gum style --bold "Top Languages:"
203 ${pkgs.jq}/bin/jq -r '
204 if (.languages | length) > 0 then
205 .languages | sort_by(-.total) | .[0:10] | .[] |
206 " \(.key): \((.total / 3600 | floor))h \(((.total % 3600) / 60) | floor)m"
207 else
208 " No languages"
209 end
210 ' /tmp/hackatime-$$.json
211
212 rm -f /tmp/hackatime-$$.json
213 '';
214
215 now = pkgs.writeShellScriptBin "now" ''
216 # Post AtProto status updates
217 message=""
218 prompt_message=true
219
220 # Parse arguments
221 while [[ $# -gt 0 ]]; do
222 case "$1" in
223 -m|--message)
224 message="$2"
225 prompt_message=false
226 shift 2
227 ;;
228 *)
229 ${pkgs.gum}/bin/gum style --foreground 196 "Usage: now [-m|--message \"your message\"]"
230 exit 1
231 ;;
232 esac
233 done
234
235 # Load account information from agenix secrets
236 if [[ -f "/run/agenix/bluesky" ]]; then
237 source "/run/agenix/bluesky"
238 else
239 ${pkgs.gum}/bin/gum style --foreground 196 "Error: Bluesky credentials file not found at /run/agenix/bluesky"
240 exit 1
241 fi
242
243 # Prompt for message if none provided
244 if [[ "$prompt_message" = true ]]; then
245 message=$(${pkgs.gum}/bin/gum input --placeholder "What's happening?" --prompt "$ACCOUNT1 is: ")
246 if [[ -z "$message" ]]; then
247 ${pkgs.gum}/bin/gum style --foreground 214 "No message provided. Aborting."
248 exit 1
249 fi
250 fi
251
252 ${pkgs.gum}/bin/gum spin --spinner dot --title "Posting to Bluesky..." -- /bin/bash <<EOF
253 # Generate JWT for ACCOUNT1
254 account1_response=\$(${pkgs.curl}/bin/curl -s -X POST \
255 -H "Content-Type: application/json" \
256 -d '{
257 "identifier": "'$ACCOUNT1'",
258 "password": "'$ACCOUNT1_PASSWORD'"
259 }' \
260 "https://bsky.social/xrpc/com.atproto.server.createSession")
261
262 account1_jwt=\$(echo "\$account1_response" | ${pkgs.jq}/bin/jq -r '.accessJwt')
263
264 if [[ -z "\$account1_jwt" || "\$account1_jwt" == "null" ]]; then
265 echo "Failed to authenticate account $ACCOUNT1" >&2
266 echo "Response: \$account1_response" >&2
267 exit 1
268 fi
269
270 # Generate JWT for ACCOUNT2
271 account2_response=\$(${pkgs.curl}/bin/curl -s -X POST \
272 -H "Content-Type: application/json" \
273 -d '{
274 "identifier": "'$ACCOUNT2'",
275 "password": "'$ACCOUNT2_PASSWORD'"
276 }' \
277 "https://bsky.social/xrpc/com.atproto.server.createSession")
278
279 account2_jwt=\$(echo "\$account2_response" | ${pkgs.jq}/bin/jq -r '.accessJwt')
280
281 if [[ -z "\$account2_jwt" || "\$account2_jwt" == "null" ]]; then
282 echo "Failed to authenticate account $ACCOUNT2" >&2
283 echo "Response: \$account2_response" >&2
284 exit 1
285 fi
286
287 # Post to ACCOUNT1 as a.status.updates
288 account1_post_response=\$(${pkgs.curl}/bin/curl -s -X POST \
289 -H "Content-Type: application/json" \
290 -H "Authorization: Bearer \$account1_jwt" \
291 -d '{
292 "collection": "a.status.update",
293 "repo": "'$ACCOUNT1'",
294 "record": {
295 "\$type": "a.status.update",
296 "text": "'"$message"'",
297 "createdAt": "'\$(date -u +"%Y-%m-%dT%H:%M:%SZ")'"
298 }
299 }' \
300 "https://bsky.social/xrpc/com.atproto.repo.createRecord")
301
302 if [[ \$(echo "\$account1_post_response" | ${pkgs.jq}/bin/jq -r 'has("error")') == "true" ]]; then
303 echo "Error posting to $ACCOUNT1:" >&2
304 echo "\$account1_post_response" | ${pkgs.jq}/bin/jq >&2
305 exit 1
306 fi
307
308 # Post to ACCOUNT2 as normal post
309 account2_post_response=\$(${pkgs.curl}/bin/curl -s -X POST \
310 -H "Content-Type: application/json" \
311 -H "Authorization: Bearer \$account2_jwt" \
312 -d '{
313 "collection": "app.bsky.feed.post",
314 "repo": "'$ACCOUNT2'",
315 "record": {
316 "\$type": "app.bsky.feed.post",
317 "text": "'"$message"'",
318 "createdAt": "'\$(date -u +"%Y-%m-%dT%H:%M:%SZ")'"
319 }
320 }' \
321 "https://bsky.social/xrpc/com.atproto.repo.createRecord")
322
323 if [[ \$(echo "\$account2_post_response" | ${pkgs.jq}/bin/jq -r 'has("error")') == "true" ]]; then
324 echo "Error posting to $ACCOUNT2:" >&2
325 echo "\$account2_post_response" | ${pkgs.jq}/bin/jq >&2
326 exit 1
327 fi
328EOF
329
330 if [[ $? -eq 0 ]]; then
331 ${pkgs.gum}/bin/gum style --foreground 35 "✓ Posted successfully!"
332 else
333 ${pkgs.gum}/bin/gum style --foreground 196 "✗ Failed to post"
334 exit 1
335 fi
336 '';
337
338 ghostty-setup = pkgs.writeShellScriptBin "ghostty-setup" ''
339 # Copy Ghostty terminfo to remote host
340 target="$1"
341
342 if [[ -z "$target" ]]; then
343 target=$(${pkgs.gum}/bin/gum input --placeholder "user@host" --prompt "Remote host: ")
344 if [[ -z "$target" ]]; then
345 ${pkgs.gum}/bin/gum style --foreground 196 "No target provided"
346 exit 1
347 fi
348 fi
349
350 ${pkgs.gum}/bin/gum style --bold --foreground 212 "Setting up Ghostty on $target"
351 echo
352
353 ${pkgs.gum}/bin/gum spin --spinner dot --title "Copying SSH key to $target..." -- \
354 ${pkgs.openssh}/bin/ssh-copy-id "$target" 2>&1
355
356 if [[ $? -ne 0 ]]; then
357 ${pkgs.gum}/bin/gum style --foreground 196 "✗ SSH key copy failed"
358 exit 2
359 fi
360
361 ${pkgs.gum}/bin/gum style --foreground 35 "✓ SSH key copied"
362
363 ${pkgs.gum}/bin/gum spin --spinner dot --title "Installing xterm-ghostty terminfo on $target..." -- \
364 bash -c "${pkgs.ncurses}/bin/infocmp -x xterm-ghostty | ${pkgs.openssh}/bin/ssh '$target' 'tic -x -'" 2>&1
365
366 if [[ $? -ne 0 ]]; then
367 ${pkgs.gum}/bin/gum style --foreground 196 "✗ Terminfo transfer failed"
368 exit 3
369 fi
370
371 ${pkgs.gum}/bin/gum style --foreground 35 "✓ Terminfo installed"
372 echo
373 ${pkgs.gum}/bin/gum style --foreground 35 --bold "Done! Ghostty is ready on $target"
374 '';
375in
376{
377 options.atelier.shell.enable = lib.mkEnableOption "Custom shell config";
378 config = lib.mkIf config.atelier.shell.enable {
379 programs.oh-my-posh = {
380 enable = true;
381 enableZshIntegration = true;
382 settings = {
383 upgrade = {
384 notice = false;
385 interval = "2w";
386 auto = false;
387 };
388 version = 2;
389 final_space = true;
390 console_title_template = "{{ .Shell }} in {{ .Folder }}";
391 blocks = [
392 {
393 type = "prompt";
394 alignment = "left";
395 newline = true;
396 segments = [
397 {
398 type = "session";
399 background = "transparent";
400 foreground = "yellow";
401 template = "{{ if .SSHSession }}{{.HostName}} {{ end }}";
402 }
403 {
404 type = "path";
405 style = "plain";
406 background = "transparent";
407 foreground = "blue";
408 template = "{{ .Path }} ";
409 properties = {
410 style = "full";
411 };
412 }
413 {
414 type = "git";
415 style = "plain";
416 foreground = "p:grey";
417 background = "transparent";
418 template = "{{if not .Detached}}{{ .HEAD }}{{else}}@{{ printf \"%.7s\" .Commit.Sha }}{{end}}{{ if .Staging.Changed }} ({{ .Staging.String }}){{ end }}{{ if .Working.Changed }}*{{ end }} <cyan>{{ if .BranchStatus }}{{ .BranchStatus }}{{ end }}</>";
419 properties = {
420 branch_icon = "";
421 branch_identical_icon = "";
422 branch_gone_icon = "";
423 branch_ahead_icon = "⇡";
424 branch_behind_icon = "⇣";
425 commit_icon = "@";
426 fetch_status = true;
427 };
428 }
429 ];
430 }
431 {
432 type = "rprompt";
433 overflow = "hidden";
434 segments = [
435 {
436 type = "executiontime";
437 style = "plain";
438 foreground = "yellow";
439 background = "transparent";
440 template = "{{ .FormattedMs }}";
441 properties = {
442 threshold = 3000;
443 };
444 }
445 {
446 type = "nix-shell";
447 style = "plain";
448 foreground = "red";
449 background = "transparent";
450 template = ''{{if ne .Type "unknown" }} {{ .Type }}{{ end }}'';
451 }
452 ];
453 }
454 {
455 type = "prompt";
456 alignment = "left";
457 newline = true;
458 segments = [
459 {
460 type = "text";
461 style = "plain";
462 foreground_templates = [
463 "{{if gt .Code 0}}red{{end}}"
464 "{{if eq .Code 0}}magenta{{end}}"
465 ];
466 background = "transparent";
467 template = "❯";
468 }
469 ];
470 }
471 ];
472 transient_prompt = {
473 foreground_templates = [
474 "{{if gt .Code 0}}red{{end}}"
475 "{{if eq .Code 0}}magenta{{end}}"
476 ];
477 background = "transparent";
478 template = "❯ ";
479 };
480 secondary_prompt = {
481 foreground = "p:gray";
482 background = "transparent";
483 template = "❯❯ ";
484 };
485 palette = {
486 grey = "#6c6c6c";
487 };
488 };
489 };
490
491 programs.zsh = {
492 enable = true;
493 enableCompletion = true;
494 syntaxHighlighting.enable = true;
495
496 shellAliases = {
497 cat = "bat";
498 ls = "eza";
499 ll = "eza -l";
500 la = "eza -la";
501 gc = "git commit";
502 gp = "git push";
503 rr = "rm -Rf";
504 ghrpc = "gh repo create -c";
505 goops = "git commit --amend --no-edit && git push --force-with-lease";
506 vi = "nvim";
507 vim = "nvim";
508 };
509 initContent = ''
510 zstyle ':completion:*' matcher-list 'm:{a-z}={A-Za-z}'
511 zstyle ':completion:*' list-colors "''${(s.:.)LS_COLORS}"
512 zstyle ':completion:*' menu no
513 zstyle ':fzf-tab:complete:cd:*' fzf-preview 'ls --color $realpath'
514 zstyle ':fzf-tab:complete:__zoxide_z:*' fzf-preview 'ls --color $realpath'
515
516 eval "$(terminal-wakatime init)"
517 '';
518 history = {
519 size = 10000;
520 path = "${config.xdg.dataHome}/zsh/history";
521 ignoreDups = true;
522 ignoreAllDups = true;
523 ignoreSpace = true;
524 expireDuplicatesFirst = true;
525 share = true;
526 extended = true;
527 append = true;
528 };
529
530 oh-my-zsh = {
531 enable = true;
532 plugins = [
533 "git"
534 "sudo"
535 "docker"
536 "git"
537 "command-not-found"
538 "colored-man-pages"
539 ];
540 };
541
542 plugins = [
543 {
544 # will source zsh-autosuggestions.plugin.zsh
545 name = "zsh-autosuggestions";
546 src = pkgs.fetchFromGitHub {
547 owner = "zsh-users";
548 repo = "zsh-autosuggestions";
549 rev = "v0.7.0";
550 sha256 = "sha256-KLUYpUu4DHRumQZ3w59m9aTW6TBKMCXl2UcKi4uMd7w=";
551 };
552 }
553 {
554 # will source zsh-sytax-highlighting
555 name = "zsh-sytax-highlighting";
556 src = pkgs.fetchFromGitHub {
557 owner = "zsh-users";
558 repo = "zsh-syntax-highlighting";
559 rev = "0.8.0";
560 sha256 = "sha256-iJdWopZwHpSyYl5/FQXEW7gl/SrKaYDEtTH9cGP7iPo=";
561 };
562 }
563 {
564 # fzf tab completion
565 name = "fzf-tab";
566 src = pkgs.fetchFromGitHub {
567 owner = "aloxaf";
568 repo = "fzf-tab";
569 rev = "v1.1.2";
570 sha256 = "sha256-Qv8zAiMtrr67CbLRrFjGaPzFZcOiMVEFLg1Z+N6VMhg=";
571 };
572 }
573 ];
574 };
575
576 programs.zoxide = {
577 enable = true;
578 enableZshIntegration = true;
579 };
580 programs.fzf = {
581 enable = true;
582 enableZshIntegration = true;
583 colors = {
584 bg = lib.mkForce "";
585 };
586 };
587 programs.atuin = {
588 enable = true;
589 settings = {
590 auto_sync = true;
591 sync_frequency = "5m";
592 sync_address = "https://api.atuin.sh";
593 search_mode = "fuzzy";
594 update_check = false;
595 style = "auto";
596 sync.records = true;
597 dotfiles.enabled = false;
598 };
599 };
600 programs.yazi = {
601 enable = true;
602 enableZshIntegration = true;
603 };
604
605 home.packages = with pkgs; [
606 tangled-setup
607 assh
608 hackatime-summary
609 now
610 ghostty-setup
611 pkgs.unstable.wakatime-cli
612 inputs.terminal-wakatime.packages.${pkgs.stdenv.hostPlatform.system}.default
613 unzip
614 dog
615 dust
616 wget
617 curl
618 jq
619 fd
620 eza
621 bat
622 ripgrep
623 ripgrep-all
624 neofetch
625 glow
626 ];
627
628 atelier.shell.git.enable = lib.mkDefault true;
629 };
630}