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 extracted_github_username=""
173
174 # Check if current directory is a git repository
175 if ! git rev-parse --is-inside-work-tree &>/dev/null; then
176 echo "Not a git repository"
177 return 1
178 fi
179
180 # Get the repository name from the current directory
181 local repo_name=$(basename "$(git rev-parse --show-toplevel)")
182
183 # Check if origin remote exists and points to ember
184 local origin_url=$(git remote get-url origin 2>/dev/null)
185 local origin_ember=false
186
187 if [[ -n "$origin_url" ]]; then
188 # Try to extract GitHub username if origin is a GitHub URL
189 if [[ "$origin_url" == *"github.com"* ]]; then
190 extracted_github_username=$(echo "$origin_url" | sed -E 's/.*github\.com[:/]([^/]+)\/.*$/\1/')
191 # Override the default username with the extracted one
192 default_github_username=$extracted_github_username
193 fi
194
195 if [[ "$origin_url" == *"ember"* ]]; then
196 origin_ember=true
197 echo "✅ Origin remote exists and points to ember"
198 else
199 echo "⚠️ Origin remote exists but doesn't point to ember"
200 fi
201 else
202 echo "⚠️ Origin remote doesn't exist"
203 fi
204
205 # Check if github remote exists
206 local github_exists=false
207 if git remote get-url github &>/dev/null; then
208 github_exists=true
209 echo "✅ GitHub remote exists"
210 else
211 echo "⚠️ GitHub remote doesn't exist"
212 fi
213
214 # Fix remotes if needed
215 if [[ "$origin_ember" = false || "$github_exists" = false ]]; then
216 echo "Setting up remotes..."
217
218 # Prompt for PLC identifier if needed
219 local plc_id=""
220 if [[ "$origin_ember" = false ]]; then
221 echo -n "Enter your PLC identifier [default: $default_plc_id]: "
222 read plc_input
223 plc_id=''${plc_input:-$default_plc_id}
224 fi
225
226 # Prompt for GitHub username with default from origin if available
227 local github_username=""
228 if [[ "$github_exists" = false ]]; then
229 echo -n "Enter your GitHub username [default: $default_github_username]: "
230 read github_input
231 github_username=''${github_input:-$default_github_username}
232 fi
233
234 # Set up origin remote if needed
235 if [[ "$origin_ember" = false && -n "$plc_id" ]]; then
236 if git remote get-url origin &>/dev/null; then
237 git remote remove origin
238 fi
239 git remote add origin "git@ember:''${plc_id}/''${repo_name}"
240 echo "✅ Set up origin remote: git@ember:''${plc_id}/''${repo_name}"
241 fi
242
243 # Set up GitHub remote if needed
244 if [[ "$github_exists" = false && -n "$github_username" ]]; then
245 git remote add github "git@github.com:''${github_username}/''${repo_name}.git"
246 echo "✅ Set up GitHub remote: git@github.com:''${github_username}/''${repo_name}.git"
247 fi
248 else
249 echo "Remotes are correctly configured"
250 fi
251 }
252
253 # Post AtProto status updates
254 now() {
255 local message=""
256 local prompt_message=true
257 local account1_name=""
258 local account2_name=""
259 local account1_jwt=""
260 local account2_jwt=""
261
262 # Load account information from agenix secrets
263 if [[ -f "/run/agenix/bluesky" ]]; then
264 source "/run/agenix/bluesky"
265 else
266 echo "Error: Bluesky credentials file not found at /run/agenix/bluesky"
267 return 1
268 fi
269
270 # Parse arguments
271 while [[ $# -gt 0 ]]; do
272 case "$1" in
273 -m|--message)
274 message="$2"
275 prompt_message=false
276 shift 2
277 ;;
278 *)
279 echo "Usage: now [-m|--message \"your message\"]"
280 return 1
281 ;;
282 esac
283 done
284
285 # Prompt for message if none provided
286 if [[ "$prompt_message" = true ]]; then
287 echo -n "$ACCOUNT1 is: "
288 read message
289
290 if [[ -z "$message" ]]; then
291 echo "No message provided. Aborting."
292 return 1
293 fi
294 fi
295
296 # Generate JWT for ACCOUNT1
297 local account1_response=$(curl -s -X POST \
298 -H "Content-Type: application/json" \
299 -d '{
300 "identifier": "'$ACCOUNT1'",
301 "password": "'$ACCOUNT1_PASSWORD'"
302 }' \
303 "https://bsky.social/xrpc/com.atproto.server.createSession")
304
305 account1_jwt=$(echo "$account1_response" | jq -r '.accessJwt')
306
307 if [[ -z "$account1_jwt" || "$account1_jwt" == "null" ]]; then
308 echo "Failed to authenticate account $ACCOUNT1"
309 echo "Response: $account1_response"
310 return 1
311 fi
312
313 # Generate JWT for ACCOUNT2
314 local account2_response=$(curl -s -X POST \
315 -H "Content-Type: application/json" \
316 -d '{
317 "identifier": "'$ACCOUNT2'",
318 "password": "'$ACCOUNT2_PASSWORD'"
319 }' \
320 "https://bsky.social/xrpc/com.atproto.server.createSession")
321
322 account2_jwt=$(echo "$account2_response" | jq -r '.accessJwt')
323
324 if [[ -z "$account2_jwt" || "$account2_jwt" == "null" ]]; then
325 echo "Failed to authenticate account $ACCOUNT2"
326 echo "Response: $account2_response"
327 return 1
328 fi
329
330 # Post to ACCOUNT1 as a.status.updates
331 local account1_post_response=$(curl -s -X POST \
332 -H "Content-Type: application/json" \
333 -H "Authorization: Bearer $account1_jwt" \
334 -d '{
335 "collection": "a.status.update",
336 "repo": "'$ACCOUNT1'",
337 "record": {
338 "$type": "a.status.update",
339 "text": "'"$message"'",
340 "createdAt": "'$(date -u +"%Y-%m-%dT%H:%M:%SZ")'"
341 }
342 }' \
343 "https://bsky.social/xrpc/com.atproto.repo.createRecord")
344
345 if [[ $(echo "$account1_post_response" | jq -r 'has("error")') == "true" ]]; then
346 echo "Error posting to $ACCOUNT1:"
347 echo "$account1_post_response" | jq
348 return 1
349 fi
350
351 # Post to ACCOUNT2 as normal post
352 local account2_post_response=$(curl -s -X POST \
353 -H "Content-Type: application/json" \
354 -H "Authorization: Bearer $account2_jwt" \
355 -d '{
356 "collection": "app.bsky.feed.post",
357 "repo": "'$ACCOUNT2'",
358 "record": {
359 "$type": "app.bsky.feed.post",
360 "text": "'"$message"'",
361 "createdAt": "'$(date -u +"%Y-%m-%dT%H:%M:%SZ")'"
362 }
363 }' \
364 "https://bsky.social/xrpc/com.atproto.repo.createRecord")
365
366 if [[ $(echo "$account2_post_response" | jq -r 'has("error")') == "true" ]]; then
367 echo "Error posting to $ACCOUNT2:"
368 echo "$account2_post_response" | jq
369 return 1
370 fi
371
372 echo "done"
373 }
374
375 ghostty_setup() {
376 local target="$1"
377
378 if [[ -z "$target" ]]; then
379 echo "Usage: ghostty_setup <user@host>"
380 return 1
381 fi
382
383 # Copy SSH key
384 echo "Copying SSH key to $target..."
385 ssh-copy-id "$target" || { echo "ssh-copy-id failed"; return 2; }
386
387 # Pipe infocmp output to tic on remote host
388 echo "Sending xterm-ghostty terminfo to $target..."
389 infocmp -x xterm-ghostty | ssh "$target" 'tic -x -' || { echo "Terminfo transfer failed"; return 3; }
390
391 echo "Done."
392 }
393
394 zstyle ':completion:*' matcher-list 'm:{a-z}={A-Za-z}'
395 zstyle ':completion:*' list-colors "''${(s.:.)LS_COLORS}"
396 zstyle ':completion:*' menu no
397 zstyle ':fzf-tab:complete:cd:*' fzf-preview 'ls --color $realpath'
398 zstyle ':fzf-tab:complete:__zoxide_z:*' fzf-preview 'ls --color $realpath'
399
400 eval "$(terminal-wakatime init)"
401 '';
402 history = {
403 size = 10000;
404 path = "${config.xdg.dataHome}/zsh/history";
405 ignoreDups = true;
406 ignoreAllDups = true;
407 ignoreSpace = true;
408 expireDuplicatesFirst = true;
409 share = true;
410 extended = true;
411 append = true;
412 };
413
414 oh-my-zsh = {
415 enable = true;
416 plugins = [
417 "git"
418 "sudo"
419 "docker"
420 "git"
421 "command-not-found"
422 "colored-man-pages"
423 ];
424 };
425
426 plugins = [
427 {
428 # will source zsh-autosuggestions.plugin.zsh
429 name = "zsh-autosuggestions";
430 src = pkgs.fetchFromGitHub {
431 owner = "zsh-users";
432 repo = "zsh-autosuggestions";
433 rev = "v0.7.0";
434 sha256 = "sha256-KLUYpUu4DHRumQZ3w59m9aTW6TBKMCXl2UcKi4uMd7w=";
435 };
436 }
437 {
438 # will source zsh-sytax-highlighting
439 name = "zsh-sytax-highlighting";
440 src = pkgs.fetchFromGitHub {
441 owner = "zsh-users";
442 repo = "zsh-syntax-highlighting";
443 rev = "0.8.0";
444 sha256 = "sha256-iJdWopZwHpSyYl5/FQXEW7gl/SrKaYDEtTH9cGP7iPo=";
445 };
446 }
447 {
448 # fzf tab completion
449 name = "fzf-tab";
450 src = pkgs.fetchFromGitHub {
451 owner = "aloxaf";
452 repo = "fzf-tab";
453 rev = "v1.1.2";
454 sha256 = "sha256-Qv8zAiMtrr67CbLRrFjGaPzFZcOiMVEFLg1Z+N6VMhg=";
455 };
456 }
457 ];
458 };
459
460 programs.zoxide = {
461 enable = true;
462 enableZshIntegration = true;
463 };
464 programs.fzf = {
465 enable = true;
466 enableZshIntegration = true;
467 colors = {
468 bg = lib.mkForce "";
469 };
470 };
471 programs.atuin = {
472 enable = true;
473 settings = {
474 auto_sync = true;
475 sync_frequency = "5m";
476 sync_address = "https://api.atuin.sh";
477 search_mode = "fuzzy";
478 update_check = false;
479 style = "auto";
480 sync.records = true;
481 dotfiles.enabled = false;
482 };
483 };
484 programs.yazi = {
485 enable = true;
486 enableZshIntegration = true;
487 };
488
489 home.packages = with pkgs; [
490 pkgs.unstable.wakatime-cli
491 inputs.terminal-wakatime.packages.${pkgs.system}.default
492 unzip
493 dog
494 dust
495 wget
496 curl
497 jq
498 fd
499 eza
500 bat
501 ripgrep
502 ripgrep-all
503 neofetch
504 glow
505 ];
506
507 atelier.shell.git.enable = lib.mkDefault true;
508 };
509}