Kieran's opinionated (and probably slightly dumb) nix config
at main 20 kB view raw
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 = "text"; 405 style = "plain"; 406 background = "transparent"; 407 foreground = "green"; 408 template = "{{ if .Env.ZMX_SESSION }}[{{ .Env.ZMX_SESSION }}] {{ end }}"; 409 } 410 { 411 type = "path"; 412 style = "plain"; 413 background = "transparent"; 414 foreground = "blue"; 415 template = "{{ .Path }} "; 416 properties = { 417 style = "full"; 418 }; 419 } 420 { 421 type = "git"; 422 style = "plain"; 423 foreground = "p:grey"; 424 background = "transparent"; 425 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 }}</>"; 426 properties = { 427 branch_icon = ""; 428 branch_identical_icon = ""; 429 branch_gone_icon = ""; 430 branch_ahead_icon = ""; 431 branch_behind_icon = ""; 432 commit_icon = "@"; 433 fetch_status = true; 434 }; 435 } 436 ]; 437 } 438 { 439 type = "rprompt"; 440 overflow = "hidden"; 441 segments = [ 442 { 443 type = "executiontime"; 444 style = "plain"; 445 foreground = "yellow"; 446 background = "transparent"; 447 template = "{{ .FormattedMs }}"; 448 properties = { 449 threshold = 3000; 450 }; 451 } 452 { 453 type = "nix-shell"; 454 style = "plain"; 455 foreground = "red"; 456 background = "transparent"; 457 template = ''{{if ne .Type "unknown" }} {{ .Type }}{{ end }}''; 458 } 459 ]; 460 } 461 { 462 type = "prompt"; 463 alignment = "left"; 464 newline = true; 465 segments = [ 466 { 467 type = "text"; 468 style = "plain"; 469 foreground_templates = [ 470 "{{if gt .Code 0}}red{{end}}" 471 "{{if eq .Code 0}}{{if .Env.SSH_CONNECTION}}cyan{{else}}magenta{{end}}{{end}}" 472 ]; 473 background = "transparent"; 474 template = ""; 475 } 476 ]; 477 } 478 ]; 479 transient_prompt = { 480 foreground_templates = [ 481 "{{if gt .Code 0}}red{{end}}" 482 "{{if eq .Code 0}}{{if .Env.SSH_CONNECTION}}cyan{{else}}magenta{{end}}{{end}}" 483 ]; 484 background = "transparent"; 485 template = " "; 486 }; 487 secondary_prompt = { 488 foreground = "p:gray"; 489 background = "transparent"; 490 template = " "; 491 }; 492 palette = { 493 grey = "#6c6c6c"; 494 }; 495 }; 496 }; 497 498 programs.zsh = { 499 enable = true; 500 enableCompletion = true; 501 syntaxHighlighting.enable = true; 502 503 shellAliases = { 504 cat = "bat"; 505 ls = "eza"; 506 ll = "eza -l"; 507 la = "eza -la"; 508 gc = "git commit"; 509 gp = "git push"; 510 rr = "rm -Rf"; 511 ghrpc = "gh repo create -c"; 512 goops = "git commit --amend --no-edit && git push --force-with-lease"; 513 vi = "nvim"; 514 vim = "nvim"; 515 }; 516 initContent = '' 517 zstyle ':completion:*' matcher-list 'm:{a-z}={A-Za-z}' 518 zstyle ':completion:*' list-colors "''${(s.:.)LS_COLORS}" 519 zstyle ':completion:*' menu no 520 zstyle ':fzf-tab:complete:cd:*' fzf-preview 'ls --color $realpath' 521 zstyle ':fzf-tab:complete:__zoxide_z:*' fzf-preview 'ls --color $realpath' 522 523 eval "$(terminal-wakatime init)" 524 ''; 525 history = { 526 size = 10000; 527 path = "${config.xdg.dataHome}/zsh/history"; 528 ignoreDups = true; 529 ignoreAllDups = true; 530 ignoreSpace = true; 531 expireDuplicatesFirst = true; 532 share = true; 533 extended = true; 534 append = true; 535 }; 536 537 oh-my-zsh = { 538 enable = true; 539 plugins = [ 540 "git" 541 "sudo" 542 "docker" 543 "git" 544 "command-not-found" 545 "colored-man-pages" 546 ]; 547 }; 548 549 plugins = [ 550 { 551 # will source zsh-autosuggestions.plugin.zsh 552 name = "zsh-autosuggestions"; 553 src = pkgs.fetchFromGitHub { 554 owner = "zsh-users"; 555 repo = "zsh-autosuggestions"; 556 rev = "v0.7.0"; 557 sha256 = "sha256-KLUYpUu4DHRumQZ3w59m9aTW6TBKMCXl2UcKi4uMd7w="; 558 }; 559 } 560 { 561 # will source zsh-sytax-highlighting 562 name = "zsh-sytax-highlighting"; 563 src = pkgs.fetchFromGitHub { 564 owner = "zsh-users"; 565 repo = "zsh-syntax-highlighting"; 566 rev = "0.8.0"; 567 sha256 = "sha256-iJdWopZwHpSyYl5/FQXEW7gl/SrKaYDEtTH9cGP7iPo="; 568 }; 569 } 570 { 571 # fzf tab completion 572 name = "fzf-tab"; 573 src = pkgs.fetchFromGitHub { 574 owner = "aloxaf"; 575 repo = "fzf-tab"; 576 rev = "v1.1.2"; 577 sha256 = "sha256-Qv8zAiMtrr67CbLRrFjGaPzFZcOiMVEFLg1Z+N6VMhg="; 578 }; 579 } 580 ]; 581 }; 582 583 programs.zoxide = { 584 enable = true; 585 enableZshIntegration = true; 586 }; 587 programs.fzf = { 588 enable = true; 589 enableZshIntegration = true; 590 colors = { 591 bg = lib.mkForce ""; 592 }; 593 }; 594 programs.atuin = { 595 enable = true; 596 settings = { 597 auto_sync = true; 598 sync_frequency = "5m"; 599 sync_address = "https://api.atuin.sh"; 600 search_mode = "fuzzy"; 601 update_check = false; 602 style = "auto"; 603 sync.records = true; 604 dotfiles.enabled = false; 605 }; 606 }; 607 programs.yazi = { 608 enable = true; 609 enableZshIntegration = true; 610 }; 611 612 home.packages = with pkgs; [ 613 tangled-setup 614 assh 615 hackatime-summary 616 now 617 ghostty-setup 618 pkgs.unstable.wakatime-cli 619 inputs.terminal-wakatime.packages.${pkgs.stdenv.hostPlatform.system}.default 620 unzip 621 dog 622 dust 623 wget 624 curl 625 jq 626 fd 627 eza 628 bat 629 ripgrep 630 ripgrep-all 631 neofetch 632 glow 633 ]; 634 635 atelier.shell.git.enable = lib.mkDefault true; 636 }; 637}