Kieran's opinionated (and probably slightly dumb) nix config
1{
2 lib,
3 pkgs,
4 config,
5 inputs,
6 ...
7}:
8{
9 options.atelier.shell.enable = lib.mkEnableOption "Custom shell config";
10 config = lib.mkIf config.atelier.shell.enable {
11 programs.oh-my-posh = {
12 enable = true;
13 enableZshIntegration = true;
14 settings = {
15 upgrade = {
16 notice = false;
17 interval = "2w";
18 auto = false;
19 };
20 version = 2;
21 final_space = true;
22 console_title_template = "{{ .Shell }} in {{ .Folder }}";
23 blocks = [
24 {
25 type = "prompt";
26 alignment = "left";
27 newline = true;
28 segments = [
29 {
30 type = "session";
31 background = "transparent";
32 foreground = "yellow";
33 template = "{{ if .SSHSession }}{{.HostName}} {{ end }}";
34 }
35 {
36 type = "path";
37 style = "plain";
38 background = "transparent";
39 foreground = "blue";
40 template = "{{ .Path }} ";
41 properties = {
42 style = "full";
43 };
44 }
45 {
46 type = "git";
47 style = "plain";
48 foreground = "p:grey";
49 background = "transparent";
50 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 }}</>";
51 properties = {
52 branch_icon = "";
53 branch_identical_icon = "";
54 branch_gone_icon = "";
55 branch_ahead_icon = "⇡";
56 branch_behind_icon = "⇣";
57 commit_icon = "@";
58 fetch_status = true;
59 };
60 }
61 ];
62 }
63 {
64 type = "rprompt";
65 overflow = "hidden";
66 segments = [
67 {
68 type = "executiontime";
69 style = "plain";
70 foreground = "yellow";
71 background = "transparent";
72 template = "{{ .FormattedMs }}";
73 properties = {
74 threshold = 3000;
75 };
76 }
77 {
78 type = "nix-shell";
79 style = "plain";
80 foreground = "red";
81 background = "transparent";
82 template = ''{{if ne .Type "unknown" }} {{ .Type }}{{ end }}'';
83 }
84 ];
85 }
86 {
87 type = "prompt";
88 alignment = "left";
89 newline = true;
90 segments = [
91 {
92 type = "text";
93 style = "plain";
94 foreground_templates = [
95 "{{if gt .Code 0}}red{{end}}"
96 "{{if eq .Code 0}}magenta{{end}}"
97 ];
98 background = "transparent";
99 template = "❯";
100 }
101 ];
102 }
103 ];
104 transient_prompt = {
105 foreground_templates = [
106 "{{if gt .Code 0}}red{{end}}"
107 "{{if eq .Code 0}}magenta{{end}}"
108 ];
109 background = "transparent";
110 template = "❯ ";
111 };
112 secondary_prompt = {
113 foreground = "p:gray";
114 background = "transparent";
115 template = "❯❯ ";
116 };
117 palette = {
118 grey = "#6c6c6c";
119 };
120 };
121 };
122
123 programs.zsh = {
124 enable = true;
125 enableCompletion = true;
126 syntaxHighlighting.enable = true;
127
128 shellAliases = {
129 cat = "bat";
130 ls = "eza";
131 ll = "eza -l";
132 la = "eza -la";
133 gc = "git commit";
134 gp = "git push";
135 rr = "rm -Rf";
136 ghrpc = "gh repo create -c";
137 goops = "git commit --amend --no-edit && git push --force-with-lease";
138 vi = "nvim";
139 vim = "nvim";
140 };
141 initContent = ''
142 #ssh auto reconnect
143 assh() {
144 local host=$1
145 local port=$2
146 while true; do
147 ssh -p $port -o "BatchMode yes" $host || sleep 1
148 done
149 }
150 # hackatime summary
151 summary() {
152 local user_id=$1
153 curl -X 'GET' \
154 "https://waka.hackclub.com/api/summary?user=''${user_id}&interval=month" \
155 -H 'accept: application/json' \
156 -H 'Authorization: Bearer 2ce9e698-8a16-46f0-b49a-ac121bcfd608' | jq '. + {
157 "total_categories_sum": (.categories | map(.total) | add),
158 "total_categories_human_readable": (
159 (.categories | map(.total) | add) as $total_seconds |
160 "\($total_seconds / 3600 | floor)h \(($total_seconds % 3600) / 60 | floor)m \($total_seconds % 60)s"
161 ),
162 "projectsKeys": (
163 .projects | sort_by(-.total) | map(.key)
164 )
165 }'
166 }
167
168 tangled() {
169 # Configuration variables - set these to your defaults
170 local default_plc_id="did:plc:krxbvxvis5skq7jj6eot23ul"
171 local default_github_username="taciturnaxolotl"
172 local default_knot_host="knot.dunkirk.sh"
173 local extracted_github_username=""
174
175 # Check if current directory is a git repository
176 if ! git rev-parse --is-inside-work-tree &>/dev/null; then
177 echo "Not a git repository"
178 return 1
179 fi
180
181 # Get the repository name from the current directory
182 local repo_name=$(basename "$(git rev-parse --show-toplevel)")
183
184 # Check if origin remote exists and points to knot
185 local origin_url=$(git remote get-url origin 2>/dev/null)
186 local origin_knot=false
187
188 if [[ -n "$origin_url" ]]; then
189 # Try to extract GitHub username if origin is a GitHub URL
190 if [[ "$origin_url" == *"github.com"* ]]; then
191 extracted_github_username=$(echo "$origin_url" | sed -E 's/.*github\.com[:/]([^/]+)\/.*$/\1/')
192 # Override the default username with the extracted one
193 default_github_username=$extracted_github_username
194 fi
195
196 if [[ "$origin_url" == *"$default_knot_host"* || "$origin_url" == *"knot.dunkirk.sh"* ]]; then
197 origin_knot=true
198 echo "✅ Origin remote exists and points to knot"
199 else
200 echo "⚠️ Origin remote exists but doesn't point to knot"
201 fi
202 else
203 echo "⚠️ Origin remote doesn't exist"
204 fi
205
206 # Check if github remote exists
207 local github_exists=false
208 if git remote get-url github &>/dev/null; then
209 github_exists=true
210 echo "✅ GitHub remote exists"
211 else
212 echo "⚠️ GitHub remote doesn't exist"
213 fi
214
215 # Fix remotes if needed
216 if [[ "$origin_knot" = false || "$github_exists" = false ]]; then
217 # Prompt for PLC identifier if needed
218 local plc_id=""
219 local should_fix_origin=false
220
221 if [[ "$origin_knot" = false ]]; then
222 if [[ -n "$origin_url" ]]; then
223 echo -n "Migrate origin from $origin_url to knot.dunkirk.sh? [Y/n]: "
224 read fix_input
225 if [[ -z "$fix_input" || "$fix_input" =~ ^[Yy]$ ]]; then
226 should_fix_origin=true
227 fi
228 else
229 should_fix_origin=true
230 fi
231
232 if [[ "$should_fix_origin" = true ]]; then
233 echo -n "Enter your PLC identifier [default: $default_plc_id]: "
234 read plc_input
235 plc_id=''${plc_input:-$default_plc_id}
236 fi
237 fi
238
239 # Prompt for GitHub username with default from origin if available
240 local github_username=""
241 if [[ "$github_exists" = false ]]; then
242 echo -n "Enter your GitHub username [default: $default_github_username]: "
243 read github_input
244 github_username=''${github_input:-$default_github_username}
245 fi
246
247 # Set up origin remote if needed
248 if [[ "$should_fix_origin" = true && -n "$plc_id" ]]; then
249 if git remote get-url origin &>/dev/null; then
250 git remote remove origin
251 fi
252 git remote add origin "git@$default_knot_host:''${plc_id}/''${repo_name}"
253 echo "✅ Set up origin remote: git@$default_knot_host:''${plc_id}/''${repo_name}"
254 fi
255
256 # Set up GitHub remote if needed
257 if [[ "$github_exists" = false && -n "$github_username" ]]; then
258 git remote add github "git@github.com:''${github_username}/''${repo_name}.git"
259 echo "✅ Set up GitHub remote: git@github.com:''${github_username}/''${repo_name}.git"
260 fi
261 else
262 echo "Remotes are correctly configured"
263 fi
264 }
265
266 # Post AtProto status updates
267 now() {
268 local message=""
269 local prompt_message=true
270 local account1_name=""
271 local account2_name=""
272 local account1_jwt=""
273 local account2_jwt=""
274
275 # Load account information from agenix secrets
276 if [[ -f "/run/agenix/bluesky" ]]; then
277 source "/run/agenix/bluesky"
278 else
279 echo "Error: Bluesky credentials file not found at /run/agenix/bluesky"
280 return 1
281 fi
282
283 # Parse arguments
284 while [[ $# -gt 0 ]]; do
285 case "$1" in
286 -m|--message)
287 message="$2"
288 prompt_message=false
289 shift 2
290 ;;
291 *)
292 echo "Usage: now [-m|--message \"your message\"]"
293 return 1
294 ;;
295 esac
296 done
297
298 # Prompt for message if none provided
299 if [[ "$prompt_message" = true ]]; then
300 echo -n "$ACCOUNT1 is: "
301 read message
302
303 if [[ -z "$message" ]]; then
304 echo "No message provided. Aborting."
305 return 1
306 fi
307 fi
308
309 # Generate JWT for ACCOUNT1
310 local account1_response=$(curl -s -X POST \
311 -H "Content-Type: application/json" \
312 -d '{
313 "identifier": "'$ACCOUNT1'",
314 "password": "'$ACCOUNT1_PASSWORD'"
315 }' \
316 "https://bsky.social/xrpc/com.atproto.server.createSession")
317
318 account1_jwt=$(echo "$account1_response" | jq -r '.accessJwt')
319
320 if [[ -z "$account1_jwt" || "$account1_jwt" == "null" ]]; then
321 echo "Failed to authenticate account $ACCOUNT1"
322 echo "Response: $account1_response"
323 return 1
324 fi
325
326 # Generate JWT for ACCOUNT2
327 local account2_response=$(curl -s -X POST \
328 -H "Content-Type: application/json" \
329 -d '{
330 "identifier": "'$ACCOUNT2'",
331 "password": "'$ACCOUNT2_PASSWORD'"
332 }' \
333 "https://bsky.social/xrpc/com.atproto.server.createSession")
334
335 account2_jwt=$(echo "$account2_response" | jq -r '.accessJwt')
336
337 if [[ -z "$account2_jwt" || "$account2_jwt" == "null" ]]; then
338 echo "Failed to authenticate account $ACCOUNT2"
339 echo "Response: $account2_response"
340 return 1
341 fi
342
343 # Post to ACCOUNT1 as a.status.updates
344 local account1_post_response=$(curl -s -X POST \
345 -H "Content-Type: application/json" \
346 -H "Authorization: Bearer $account1_jwt" \
347 -d '{
348 "collection": "a.status.update",
349 "repo": "'$ACCOUNT1'",
350 "record": {
351 "$type": "a.status.update",
352 "text": "'"$message"'",
353 "createdAt": "'$(date -u +"%Y-%m-%dT%H:%M:%SZ")'"
354 }
355 }' \
356 "https://bsky.social/xrpc/com.atproto.repo.createRecord")
357
358 if [[ $(echo "$account1_post_response" | jq -r 'has("error")') == "true" ]]; then
359 echo "Error posting to $ACCOUNT1:"
360 echo "$account1_post_response" | jq
361 return 1
362 fi
363
364 # Post to ACCOUNT2 as normal post
365 local account2_post_response=$(curl -s -X POST \
366 -H "Content-Type: application/json" \
367 -H "Authorization: Bearer $account2_jwt" \
368 -d '{
369 "collection": "app.bsky.feed.post",
370 "repo": "'$ACCOUNT2'",
371 "record": {
372 "$type": "app.bsky.feed.post",
373 "text": "'"$message"'",
374 "createdAt": "'$(date -u +"%Y-%m-%dT%H:%M:%SZ")'"
375 }
376 }' \
377 "https://bsky.social/xrpc/com.atproto.repo.createRecord")
378
379 if [[ $(echo "$account2_post_response" | jq -r 'has("error")') == "true" ]]; then
380 echo "Error posting to $ACCOUNT2:"
381 echo "$account2_post_response" | jq
382 return 1
383 fi
384
385 echo "done"
386 }
387
388 ghostty_setup() {
389 local target="$1"
390
391 if [[ -z "$target" ]]; then
392 echo "Usage: ghostty_setup <user@host>"
393 return 1
394 fi
395
396 # Copy SSH key
397 echo "Copying SSH key to $target..."
398 ssh-copy-id "$target" || { echo "ssh-copy-id failed"; return 2; }
399
400 # Pipe infocmp output to tic on remote host
401 echo "Sending xterm-ghostty terminfo to $target..."
402 infocmp -x xterm-ghostty | ssh "$target" 'tic -x -' || { echo "Terminfo transfer failed"; return 3; }
403
404 echo "Done."
405 }
406
407 zstyle ':completion:*' matcher-list 'm:{a-z}={A-Za-z}'
408 zstyle ':completion:*' list-colors "''${(s.:.)LS_COLORS}"
409 zstyle ':completion:*' menu no
410 zstyle ':fzf-tab:complete:cd:*' fzf-preview 'ls --color $realpath'
411 zstyle ':fzf-tab:complete:__zoxide_z:*' fzf-preview 'ls --color $realpath'
412
413 eval "$(terminal-wakatime init)"
414 '';
415 history = {
416 size = 10000;
417 path = "${config.xdg.dataHome}/zsh/history";
418 ignoreDups = true;
419 ignoreAllDups = true;
420 ignoreSpace = true;
421 expireDuplicatesFirst = true;
422 share = true;
423 extended = true;
424 append = true;
425 };
426
427 oh-my-zsh = {
428 enable = true;
429 plugins = [
430 "git"
431 "sudo"
432 "docker"
433 "git"
434 "command-not-found"
435 "colored-man-pages"
436 ];
437 };
438
439 plugins = [
440 {
441 # will source zsh-autosuggestions.plugin.zsh
442 name = "zsh-autosuggestions";
443 src = pkgs.fetchFromGitHub {
444 owner = "zsh-users";
445 repo = "zsh-autosuggestions";
446 rev = "v0.7.0";
447 sha256 = "sha256-KLUYpUu4DHRumQZ3w59m9aTW6TBKMCXl2UcKi4uMd7w=";
448 };
449 }
450 {
451 # will source zsh-sytax-highlighting
452 name = "zsh-sytax-highlighting";
453 src = pkgs.fetchFromGitHub {
454 owner = "zsh-users";
455 repo = "zsh-syntax-highlighting";
456 rev = "0.8.0";
457 sha256 = "sha256-iJdWopZwHpSyYl5/FQXEW7gl/SrKaYDEtTH9cGP7iPo=";
458 };
459 }
460 {
461 # fzf tab completion
462 name = "fzf-tab";
463 src = pkgs.fetchFromGitHub {
464 owner = "aloxaf";
465 repo = "fzf-tab";
466 rev = "v1.1.2";
467 sha256 = "sha256-Qv8zAiMtrr67CbLRrFjGaPzFZcOiMVEFLg1Z+N6VMhg=";
468 };
469 }
470 ];
471 };
472
473 programs.zoxide = {
474 enable = true;
475 enableZshIntegration = true;
476 };
477 programs.fzf = {
478 enable = true;
479 enableZshIntegration = true;
480 colors = {
481 bg = lib.mkForce "";
482 };
483 };
484 programs.atuin = {
485 enable = true;
486 settings = {
487 auto_sync = true;
488 sync_frequency = "5m";
489 sync_address = "https://api.atuin.sh";
490 search_mode = "fuzzy";
491 update_check = false;
492 style = "auto";
493 sync.records = true;
494 dotfiles.enabled = false;
495 };
496 };
497 programs.yazi = {
498 enable = true;
499 enableZshIntegration = true;
500 };
501
502 home.packages = with pkgs; [
503 pkgs.unstable.wakatime-cli
504 inputs.terminal-wakatime.packages.${pkgs.system}.default
505 unzip
506 dog
507 dust
508 wget
509 curl
510 jq
511 fd
512 eza
513 bat
514 ripgrep
515 ripgrep-all
516 neofetch
517 glow
518 ];
519
520 atelier.shell.git.enable = lib.mkDefault true;
521 };
522}