@jaspermayone.com's dotfiles
1# Shell configuration
2{
3 config,
4 lib,
5 pkgs,
6 hostname,
7 inputs,
8 ...
9}:
10
11let
12 # Strings CLI for uploading to pastebin
13 strings-cli = pkgs.writeShellScriptBin "strings" ''
14 #!/usr/bin/env bash
15 # strings - CLI for strings pastebin
16 # Usage: strings <file> or cat file | strings
17
18 set -e
19
20 STRINGS_HOST="''${STRINGS_HOST:-https://str.hogwarts.dev}"
21 STRINGS_USER="''${STRINGS_USER:-}"
22 STRINGS_PASS="''${STRINGS_PASS:-}"
23
24 # Try to load credentials from file if not set
25 if [[ -f "$HOME/.config/strings/credentials" ]]; then
26 source "$HOME/.config/strings/credentials"
27 fi
28
29 if [ -z "$STRINGS_USER" ] || [ -z "$STRINGS_PASS" ]; then
30 echo "Error: STRINGS_USER and STRINGS_PASS environment variables must be set" >&2
31 echo "Or create ~/.config/strings/credentials with:" >&2
32 echo " STRINGS_USER=youruser" >&2
33 echo " STRINGS_PASS=yourpass" >&2
34 exit 1
35 fi
36
37 # Determine filename and content
38 FILENAME=""
39 SLUG=""
40
41 while [[ $# -gt 0 ]]; do
42 case $1 in
43 -s|--slug)
44 SLUG="$2"
45 shift 2
46 ;;
47 -h|--host)
48 STRINGS_HOST="$2"
49 shift 2
50 ;;
51 *)
52 if [ -z "$FILENAME" ]; then
53 FILENAME="$1"
54 fi
55 shift
56 ;;
57 esac
58 done
59
60 if [ -n "$FILENAME" ]; then
61 if [ ! -f "$FILENAME" ]; then
62 echo "Error: File not found: $FILENAME" >&2
63 exit 1
64 fi
65 BASENAME=$(basename "$FILENAME")
66 CONTENT=$(cat "$FILENAME")
67 else
68 CONTENT=$(cat)
69 BASENAME=""
70 fi
71
72 # Build headers
73 HEADERS=(-H "Content-Type: text/plain")
74 [ -n "$BASENAME" ] && HEADERS+=(-H "X-Filename: $BASENAME")
75 [ -n "$SLUG" ] && HEADERS+=(-H "X-Slug: $SLUG")
76
77 # Make request
78 RESPONSE=$(${pkgs.curl}/bin/curl -s -X POST "$STRINGS_HOST/api/paste" \
79 -u "$STRINGS_USER:$STRINGS_PASS" \
80 "''${HEADERS[@]}" \
81 --data-binary "$CONTENT")
82
83 # Parse response
84 URL=$(echo "$RESPONSE" | ${pkgs.gnugrep}/bin/grep -o '"url":"[^"]*"' | cut -d'"' -f4)
85
86 if [ -n "$URL" ]; then
87 echo "$URL"
88
89 # Copy to clipboard if available
90 if command -v pbcopy &> /dev/null; then
91 echo -n "$URL" | pbcopy
92 elif command -v xclip &> /dev/null; then
93 echo -n "$URL" | xclip -selection clipboard
94 elif command -v wl-copy &> /dev/null; then
95 echo -n "$URL" | wl-copy
96 fi
97 else
98 echo "Error: $RESPONSE" >&2
99 exit 1
100 fi
101 '';
102
103 # Tangled setup script for configuring git remotes
104 tangled-setup = pkgs.writeShellScriptBin "tangled-setup" ''
105 # Configuration
106 default_plc_id="did:plc:krxbvxvis5skq7jj6eot23ul"
107 default_github_username="jaspermayone"
108 default_knot_host="knot.jaspermayone.com"
109
110 # Verify git repository
111 if ! ${pkgs.git}/bin/git rev-parse --is-inside-work-tree &>/dev/null; then
112 ${pkgs.gum}/bin/gum style --foreground 196 "Not a git repository"
113 exit 1
114 fi
115
116 repo_name=$(basename "$(${pkgs.git}/bin/git rev-parse --show-toplevel)")
117 ${pkgs.gum}/bin/gum style --bold --foreground 212 "Configuring tangled remotes for: $repo_name"
118 echo
119
120 # Check current remotes
121 origin_url=$(${pkgs.git}/bin/git remote get-url origin 2>/dev/null)
122 github_url=$(${pkgs.git}/bin/git remote get-url github 2>/dev/null)
123 origin_is_knot=false
124 github_username="$default_github_username"
125
126 # Extract GitHub username from existing origin if it's GitHub
127 if [[ "$origin_url" == *"github.com"* ]]; then
128 github_username=$(echo "$origin_url" | ${pkgs.gnused}/bin/sed -E 's/.*github\.com[:/]([^/]+)\/.*$/\1/')
129 fi
130
131 # Check if origin points to knot
132 if [[ "$origin_url" == *"$default_knot_host"* ]] || [[ "$origin_url" == *"tangled"* ]]; then
133 origin_is_knot=true
134 ${pkgs.gum}/bin/gum style --foreground 35 "✓ Origin → knot ($origin_url)"
135 elif [[ -n "$origin_url" ]]; then
136 ${pkgs.gum}/bin/gum style --foreground 214 "! Origin → $origin_url (not knot)"
137 else
138 ${pkgs.gum}/bin/gum style --foreground 214 "! Origin not configured"
139 fi
140
141 # Check github remote
142 if [[ -n "$github_url" ]]; then
143 ${pkgs.gum}/bin/gum style --foreground 35 "✓ GitHub → $github_url"
144 else
145 ${pkgs.gum}/bin/gum style --foreground 214 "! GitHub remote not configured"
146 fi
147
148 echo
149
150 # Configure origin remote if needed
151 if [[ "$origin_is_knot" = false ]]; then
152 should_migrate=true
153 if [[ -n "$origin_url" ]]; then
154 ${pkgs.gum}/bin/gum confirm "Migrate origin from $origin_url to knot?" || should_migrate=false
155 fi
156
157 if [[ "$should_migrate" = true ]]; then
158 plc_id=$(${pkgs.gum}/bin/gum input --placeholder "$default_plc_id" --prompt "PLC ID: " --value "$default_plc_id")
159 plc_id=''${plc_id:-$default_plc_id}
160
161 if ${pkgs.git}/bin/git remote get-url origin &>/dev/null; then
162 ${pkgs.git}/bin/git remote remove origin
163 fi
164 ${pkgs.git}/bin/git remote add origin "git@$default_knot_host:''${plc_id}/''${repo_name}"
165 ${pkgs.gum}/bin/gum style --foreground 35 "✓ Configured origin → git@$default_knot_host:''${plc_id}/''${repo_name}"
166 fi
167 fi
168
169 # Configure github remote if needed
170 if [[ -z "$github_url" ]]; then
171 username=$(${pkgs.gum}/bin/gum input --placeholder "$github_username" --prompt "GitHub username: " --value "$github_username")
172 username=''${username:-$github_username}
173
174 ${pkgs.git}/bin/git remote add github "git@github.com:''${username}/''${repo_name}.git"
175 ${pkgs.gum}/bin/gum style --foreground 35 "✓ Configured github → git@github.com:''${username}/''${repo_name}.git"
176 fi
177
178 echo
179
180 # Configure default push remote
181 current_remote=$(${pkgs.git}/bin/git config --get branch.main.remote 2>/dev/null)
182 if [[ -z "$current_remote" ]]; then
183 if ${pkgs.gum}/bin/gum confirm "Set origin (knot) as default push remote?"; then
184 ${pkgs.git}/bin/git config branch.main.remote origin
185 ${pkgs.gum}/bin/gum style --foreground 35 "✓ Default push remote → origin"
186 fi
187 elif [[ "$current_remote" != "origin" ]]; then
188 ${pkgs.gum}/bin/gum style --foreground 117 "Current default: $current_remote"
189 if ${pkgs.gum}/bin/gum confirm "Change default push remote to origin (knot)?"; then
190 ${pkgs.git}/bin/git config branch.main.remote origin
191 ${pkgs.gum}/bin/gum style --foreground 35 "✓ Default push remote → origin"
192 fi
193 else
194 ${pkgs.gum}/bin/gum style --foreground 35 "✓ Default push remote is origin"
195 fi
196 '';
197
198in
199{
200 # Starship prompt
201 programs.starship = {
202 enable = true;
203 enableZshIntegration = true;
204 settings = {
205 format = "$username$hostname$localip$directory$git_branch$git_commit$git_state$git_metrics$git_status$nix_shell$mise$bun$nodejs$python$ruby$rust$java$swift$direnv$shell$cmd_duration$line_break$character";
206
207 # Hostname - always visible
208 hostname = {
209 ssh_only = false;
210 format = "[$hostname]($style) ";
211 style = "dimmed white";
212 ssh_symbol = " ";
213 };
214
215 # Local IP - show on SSH
216 localip = {
217 ssh_only = true;
218 format = "[@$localipv4]($style) ";
219 style = "dimmed white";
220 disabled = false;
221 };
222
223 # Directory
224 directory = {
225 style = "cyan";
226 truncation_length = 3;
227 truncation_symbol = "…/";
228 truncate_to_repo = true;
229 read_only = " ";
230 };
231
232 # Git branch
233 git_branch = {
234 format = "[$symbol$branch(:$remote_branch)]($style) ";
235 symbol = " ";
236 style = "purple";
237 };
238
239 # Git commit - show hash when detached
240 git_commit = {
241 format = "[$hash$tag]($style) ";
242 style = "dimmed white";
243 only_detached = true;
244 tag_disabled = false;
245 tag_symbol = " ";
246 };
247
248 # Git state - show rebase/merge/etc
249 git_state = {
250 format = "[$state( $progress_current/$progress_total)]($style) ";
251 style = "yellow";
252 };
253
254 # Git metrics - show +/- lines
255 git_metrics = {
256 format = "([+$added]($added_style))([-$deleted]($deleted_style) )";
257 added_style = "green";
258 deleted_style = "red";
259 disabled = false;
260 };
261
262 # Git status - detailed symbols
263 git_status = {
264 format = "([$all_status$ahead_behind]($style) )";
265 style = "dimmed red";
266 conflicted = "=";
267 ahead = "⇡$count";
268 behind = "⇣$count";
269 diverged = "⇕⇡$ahead_count⇣$behind_count";
270 up_to_date = "";
271 untracked = "?$count";
272 stashed = "";
273 modified = "!$count";
274 staged = "+$count";
275 renamed = "»$count";
276 deleted = "✘$count";
277 };
278
279 # Bun
280 bun = {
281 format = "[$symbol($version)]($style) ";
282 symbol = " ";
283 style = "dimmed white";
284 };
285
286 # Direnv
287 direnv = {
288 format = "[$symbol$loaded/$allowed]($style) ";
289 symbol = "direnv ";
290 style = "dimmed white";
291 disabled = false;
292 };
293
294 # Command duration
295 cmd_duration = {
296 min_time = 2000;
297 format = "[$duration]($style) ";
298 style = "dimmed yellow";
299 show_notifications = false;
300 };
301
302 # Character
303 character = {
304 success_symbol = "[❯](cyan)";
305 error_symbol = "[❯](red)";
306 };
307
308 # Username - show when root or SSH
309 username = {
310 style_root = "bold red";
311 style_user = "dimmed white";
312 format = "[$user]($style)@";
313 show_always = false;
314 disabled = false;
315 };
316
317 # Nix shell
318 nix_shell = {
319 format = "[$symbol$state( \\($name\\))]($style) ";
320 symbol = " ";
321 style = "dimmed blue";
322 impure_msg = "impure";
323 pure_msg = "pure";
324 disabled = false;
325 heuristic = true;
326 };
327
328 # Mise (version manager)
329 mise = {
330 format = "[$symbol$health]($style) ";
331 symbol = "mise ";
332 style = "dimmed white";
333 disabled = false;
334 };
335
336 # Node.js
337 nodejs = {
338 format = "[$symbol($version)]($style) ";
339 symbol = " ";
340 style = "dimmed green";
341 not_capable_style = "red";
342 };
343
344 # Python
345 python = {
346 format = "[$symbol$pyenv_prefix($version )(\\($virtualenv\\))]($style) ";
347 symbol = " ";
348 style = "dimmed yellow";
349 pyenv_version_name = false;
350 };
351
352 # Ruby
353 ruby = {
354 format = "[$symbol($version)]($style) ";
355 symbol = " ";
356 style = "dimmed red";
357 };
358
359 # Rust
360 rust = {
361 format = "[$symbol($version)]($style) ";
362 symbol = " ";
363 style = "dimmed red";
364 };
365
366 # Java
367 java = {
368 format = "[$symbol($version)]($style) ";
369 symbol = " ";
370 style = "dimmed red";
371 };
372
373 # Swift
374 swift = {
375 format = "[$symbol($version)]($style) ";
376 symbol = " ";
377 style = "dimmed white";
378 };
379
380 # Shell indicator
381 shell = {
382 format = "[$indicator]($style) ";
383 style = "white bold";
384 zsh_indicator = "";
385 bash_indicator = "bsh";
386 fish_indicator = "fish";
387 disabled = true; # enable if you switch shells often
388 };
389 };
390 };
391
392 programs.zsh = {
393 enable = true;
394 enableCompletion = true;
395 autosuggestion.enable = true;
396 syntaxHighlighting.enable = true;
397
398 shellAliases = {
399 # Navigation
400 ll = "eza -l";
401 la = "eza -la";
402 l = "eza";
403 ls = "eza";
404 ".." = "cd ..";
405 "..." = "cd ../..";
406
407 # Safety
408 rm = "rm -i";
409 cp = "cp -i";
410 mv = "mv -i";
411 rr = "rm -Rf";
412
413 # Enhanced commands
414 cd = "z";
415 cat = "bat --paging=never";
416
417 # Quick commands
418 e = "exit";
419 c = "clear";
420
421 # Directory shortcuts
422 dev = "z ~/dev";
423 proj = "z ~/dev/projects";
424 scripts = "z ~/dev/scripts";
425
426 # Git shortcuts
427 g = "git";
428 gs = "git status";
429 gd = "git diff";
430 ga = "git add";
431 gc = "git commit";
432 gp = "git push";
433 gl = "git pull";
434 s = "git status";
435 push = "git push";
436 pull = "git pull";
437 goops = "git commit --amend --no-edit && git push --force-with-lease";
438
439 # Python
440 pip = "pip3";
441 python = "python3";
442
443 cl = "claude --allow-dangerously-skip-permissions";
444 clo = "ollama launch claude";
445
446 # Project shortcuts
447 dns = "z dev/dns && source .env && source .venv/bin/activate";
448
449 # Zsh config
450 zshedit = "code ~/.zshrc";
451 zshreload = "source ~/.zshrc";
452
453 # GPG key sync
454 keysync = "gpg --keyserver pgp.mit.edu --send-keys 00E643C21FAC965FFB28D3B714D0D45A1DADAAFA && gpg --keyserver keyserver.ubuntu.com --send-keys 00E643C21FAC965FFB28D3B714D0D45A1DADAAFA && gpg --keyserver keys.openpgp.org --send-keys 00E643C21FAC965FFB28D3B714D0D45A1DADAAFA && gpg --export me@jaspermayone.com | curl -T - https://keys.openpgp.org";
455 gpgend = "gpg --keyserver hkps://keys.openpgp.org --send-keys 14D0D45A1DADAAFA";
456
457 path = "echo -e \${PATH//:/\\n}";
458
459 # Vim
460 vi = "vim";
461
462 afk = "/System/Library/CoreServices/Menu\ Extras/User.menu/Contents/Resources/CGSession -suspend";
463 reload = "exec \${SHELL} -l";
464 };
465
466 initContent = ''
467 # ============================================================================
468 # HOMEBREW
469 # ============================================================================
470 if [[ -f /opt/homebrew/bin/brew ]]; then
471 eval "$(/opt/homebrew/bin/brew shellenv)"
472 fi
473
474 # ============================================================================
475 # ZINIT PLUGIN MANAGER
476 # ============================================================================
477 if [[ ! -f $HOME/.local/share/zinit/zinit.git/zinit.zsh ]]; then
478 print -P "%F{33} %F{220}Installing %F{33}ZDHARMA-CONTINUUM%F{220} Initiative Plugin Manager (%F{33}zdharma-continuum/zinit%F{220})…%f"
479 command mkdir -p "$HOME/.local/share/zinit" && command chmod g-rwX "$HOME/.local/share/zinit"
480 command git clone https://github.com/zdharma-continuum/zinit "$HOME/.local/share/zinit/zinit.git" && \
481 print -P "%F{33} %F{34}Installation successful.%f%b" || \
482 print -P "%F{160} The clone has failed.%f%b"
483 fi
484
485 source "$HOME/.local/share/zinit/zinit.git/zinit.zsh"
486 autoload -Uz _zinit
487 (( ''${+_comps} )) && _comps[zinit]=_zinit
488
489 # ============================================================================
490 # ZINIT PLUGINS
491 # ============================================================================
492 zinit light zsh-users/zsh-completions
493 zinit light zsh-users/zsh-autosuggestions
494
495 # Defer fzf-tab
496 zinit ice wait"0a" lucid
497 zinit light Aloxaf/fzf-tab
498
499 # Syntax highlighting loads in background
500 zinit ice wait"1" lucid
501 zinit light zsh-users/zsh-syntax-highlighting
502
503 # ============================================================================
504 # OH-MY-ZSH SNIPPETS (deferred)
505 # ============================================================================
506 zinit ice wait"0b" lucid
507 zinit snippet OMZP::git-commit
508
509 zinit ice wait"0b" lucid
510 zinit snippet OMZP::sudo
511
512 zinit ice wait"0b" lucid
513 zinit snippet OMZP::command-not-found
514
515 zinit ice wait"0b" lucid
516 zinit snippet OMZP::iterm2
517
518 # ============================================================================
519 # COMPLETIONS
520 # ============================================================================
521 autoload -Uz compinit
522 if [[ -n ''${HOME}/.zcompdump(#qN.mh+24) ]]; then
523 compinit
524 else
525 compinit -C
526 fi
527 zinit cdreplay -q
528
529 # ============================================================================
530 # KEY BINDINGS
531 # ============================================================================
532 bindkey '^f' autosuggest-accept
533 bindkey '^p' history-search-backward
534 bindkey '^n' history-search-forward
535
536 # ============================================================================
537 # HISTORY CONFIGURATION
538 # ============================================================================
539 HISTFILE=~/.zsh_history
540 HISTSIZE=999999999999
541 SAVEHIST=999999999999
542 HISTDUP=erase
543 setopt appendhistory
544 setopt extendedhistory
545 setopt hist_ignore_space
546 setopt hist_ignore_all_dups
547 setopt hist_save_no_dups
548 setopt hist_ignore_dups
549 setopt hist_find_no_dups
550
551 # ============================================================================
552 # COMPLETION STYLING
553 # ============================================================================
554 zstyle ':completion:*' matcher-list 'm:{a-z}={A-Za-z}'
555 zstyle ':completion:*' list-colors "''${(s.:.)LS_COLORS}"
556 zstyle ':completion:*' menu no
557 zstyle ':fzf-tab:complete:cd:*' fzf-preview 'ls --color $realpath'
558 zstyle ':fzf-tab:complete:__zoxide_z:*' fzf-preview 'ls --color $realpath'
559
560 # ============================================================================
561 # PATH EXPORTS
562 # ============================================================================
563 # Remus-specific: Run path_helper to setup PATH
564 if [[ $(hostname -s) == "remus" ]]; then
565 eval "$(/usr/libexec/path_helper -s)"
566 fi
567
568 # Note: flyctl, bun, pnpm are now managed by nix
569 export PATH="$HOME/bin:$PATH"
570 export PATH="$HOME/.local/bin:$PATH"
571 export PATH="/Users/jsp/.lmstudio/bin:$PATH"
572 export PATH="/Users/jsp/.codeium/windsurf/bin:$PATH"
573 export PATH="/opt/homebrew/opt/mysql@8.0/bin:$PATH"
574 export PATH="/Users/jsp/.antigravity/antigravity/bin:$PATH"
575 export PATH="$HOME/go/bin:$PATH"
576 export PATH="$HOME/toolchains/gcc-arm-none-eabi-10.3-2021.10/bin:$PATH"
577
578 # ============================================================================
579 # ENVIRONMENT VARIABLES
580 # ============================================================================
581 export PICO_SDK_PATH="$HOME/dev/pico-sdk"
582 export COMPOSE_BAKE=true
583 export VISUAL="code --wait"
584 export EDITOR="code --wait"
585 export LDFLAGS="-L/opt/homebrew/opt/postgresql@17/lib"
586 export CPPFLAGS="-I/opt/homebrew/opt/postgresql@17/include"
587 export ENABLE_BACKGROUND_TASKS=1
588
589 # Fix GPG issues with Homebrew install
590 export GPG_TTY=$(tty)
591
592 # Claude Code GitHub token (for MCP server)
593 if [[ -f "$HOME/.config/claude/github-token" ]]; then
594 export GITHUB_PERSONAL_ACCESS_TOKEN=$(cat "$HOME/.config/claude/github-token")
595 fi
596
597 # ============================================================================
598 # SHELL INTEGRATIONS
599 # ============================================================================
600 # Note: zoxide, fzf, atuin are initialized by home-manager programs.*
601
602 # Mise activation
603 eval "$(mise activate zsh)"
604
605 # Wut activation (workspace manager)
606 if command -v wut &> /dev/null; then
607 eval "$(wut init)"
608 fi
609 '';
610 };
611
612 # Common CLI tools
613 home.packages = with pkgs; [
614 # Custom scripts
615 tangled-setup
616 strings-cli
617
618 # File management
619 tree
620 fd
621 ripgrep
622 bat
623 eza
624 unzip
625
626 # System monitoring
627 htop
628 btop
629
630 # Networking
631 curl
632 wget
633 httpie
634
635 # JSON/YAML
636 jq
637 yq
638
639 # Misc
640 fzf
641 tmux
642 watch
643 gum # Required for tangled-setup script
644
645 # Encryption
646 age # Modern encryption tool
647
648 # Dev tools
649 mise # Version manager (formerly rtx)
650 flyctl # Fly.io CLI
651 unstable.bun # JavaScript runtime
652 nodePackages.pnpm # Package manager
653 zmx-binary # Session persistence for terminal processes
654
655 # Deployment
656 inputs.deploy-rs.packages.${pkgs.stdenv.hostPlatform.system}.default
657 ];
658
659 # Fuzzy finder integration
660 programs.fzf = {
661 enable = true;
662 enableZshIntegration = true;
663 colors = {
664 bg = lib.mkForce "";
665 };
666 };
667
668 # Better cat
669 programs.bat = {
670 enable = true;
671 config = {
672 theme = "TwoDark";
673 };
674 };
675
676 # Zoxide (better cd)
677 programs.zoxide = {
678 enable = true;
679 enableZshIntegration = true;
680 };
681
682 # Atuin (shell history) with self-hosted sync server
683 programs.atuin = {
684 enable = true;
685 enableZshIntegration = true;
686 flags = [
687 "--disable-up-arrow"
688 ];
689 settings = {
690 auto_sync = true;
691 sync_frequency = "5m";
692 sync_address = "https://atuin.hogwarts.dev";
693 key_path = "~/.local/share/atuin/key";
694 search_mode = "fuzzy";
695 update_check = false;
696 style = "auto";
697 inline_height = 30;
698 };
699 };
700
701}