1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
1073
1074
1075
1076
1077
1078
1079
1080
1081
1082
1083
1084
1085
1086
1087
1088
1089
1090
1091
1092
1093
1094
1095
1096
1097
1098
1099
1100
1101
1102
1103
1104
1105
1106
1107
1108
1109
1110
1111
1112
1113
1114
1115
1116
1117
1118
1119
1120
1121
1122
1123
1124
1125
1126
1127
1128
1129
1130
1131
1132
1133
1134
1135
1136
1137
1138
1139
1140
1141
1142
1143
1144
1145
1146
1147
1148
1149
1150
1151
1152
1153
1154
1155
1156
1157
1158
1159
1160
1161
1162
1163
1164
1165
1166
1167
1168
1169
1170
1171
1172
1173
1174
1175
1176
1177
1178
1179
1180
1181
1182
1183
1184
1185
1186
1187
1188
1189
1190
1191
1192
1193
1194
1195
1196
1197
1198
1199
1200
1201
1202
1203
1204
1205
1206
1207
1208
1209
1210
1211
1212
1213
1214
1215
1216
1217
1218
1219
1220
1221
1222
1223
1224
1225
1226
1227
1228
1229
1230
1231
1232
1233
1234
1235
1236
1237
1238
1239
1240
1241
1242
1243
1244
1245
1246
1247
1248
1249
1250
1251
1252
1253
1254
1255
1256
1257
1258
1259
1260
1261
1262
1263
1264
1265
1266
1267
1268
1269
1270
1271
1272
1273
1274
1275
1276
1277
1278
1279
1280
1281
1282
1283
1284
1285
1286
1287
1288
1289
1290
1291
1292
1293
1294
1295
1296
1297
1298
1299
1300
1301
1302
1303
1304
1305
1306
1307
1308
1309
1310
1311
1312
1313
1314
1315
1316
1317
1318
1319
1320
1321
1322
1323
1324
1325
1326
1327
1328
1329
1330
1331
1332
1333
1334
1335
1336
1337
1338
1339
1340
1341
1342
1343
1344
1345
1346
1347
1348
1349
1350
1351
1352
1353
1354
1355
1356
1357
1358
1359
1360
1361
1362
1363
1364
1365
1366
1367
1368
1369
1370
1371
1372
1373
1374
1375
1376
1377
1378
1379
1380
1381
1382
1383
1384
1385
1386
1387
1388
1389
1390
1391
1392
1393
1394
1395
1396
1397
1398
1399
1400
1401
1402
1403
1404
1405
1406
1407
1408
1409
1410
1411
1412
1413
1414
1415
1416
1417
1418
1419
1420
1421
1422
1423
1424
1425
1426
1427
1428
1429
1430
1431
1432
1433
1434
1435
1436
1437
1438
1439
1440
1441
1442
1443
1444
1445
1446
1447
1448
1449
1450
1451
1452
1453
1454
1455
1456
1457
1458
1459
1460
1461
1462
1463
1464
1465
1466
1467
1468
1469
1470
1471
1472
1473
1474
1475
1476
1477
1478
1479
1480
1481
1482
1483
1484
1485
1486
1487
1488
1489
1490
1491
1492
1493
1494
1495
1496
1497
1498
1499
1500
1501
1502
1503
1504
1505
1506
1507
1508
1509
1510
1511
1512
1513
1514
1515
1516
1517
1518
1519
1520
1521
1522
1523
1524
1525
1526
1527
1528
1529
1530
1531
1532
1533
1534
1535
1536
1537
1538
1539
1540
1541
1542
1543
1544
1545
1546
1547
1548
1549
1550
1551
1552
1553
1554
1555
1556
1557
1558
1559
1560
1561
1562
1563
1564
1565
1566
1567
1568
1569
1570
1571
1572
1573
1574
1575
1576
1577
1578
1579
1580
1581
1582
1583
1584
1585
1586
1587
1588
1589
1590
1591
1592
1593
1594
1595
1596
1597
1598
1599
1600
1601
1602
1603
1604
1605
1606
1607
1608
1609
1610
1611
1612
1613
1614
1615
1616
1617
1618
1619
1620
1621
1622
1623
|
# Dotfiles Management System
if [[ -d "$HOME/.cfg" && -d "$HOME/.cfg/refs" ]]; then
# Core git wrapper - .cfg is bare repo, work-tree points to .cfg itself
_config() {
git --git-dir="$HOME/.cfg" --work-tree="$HOME/.cfg" "$@"
}
# Detect OS
case "$(uname -s)" in
Linux) CFG_OS="linux" ;;
Darwin) CFG_OS="macos" ;;
MINGW*|MSYS*|CYGWIN*) CFG_OS="windows" ;;
*) CFG_OS="other" ;;
esac
# Map system path to repository path
_repo_path() {
local f="$1"
# If it's an absolute path that's not in HOME, handle it specially
if [[ "$f" == /* && "$f" != "$HOME/"* ]]; then
echo "$CFG_OS/${f#/}"
return
fi
# Check for paths that should go to the repository root
case "$f" in
common/*|linux/*|macos/*|windows/*|profile/*|README.md)
echo "$f"
return
;;
"$HOME/"*)
f="${f#$HOME/}"
;;
esac
# Default: put under OS-specific home
echo "$CFG_OS/home/$f"
}
_sys_path() {
local repo_path="$1"
local os_path_pattern="$CFG_OS/"
# Handle OS-specific files that are not in the home subdirectory
if [[ "$repo_path" == "$os_path_pattern"* && "$repo_path" != */home/* ]]; then
echo "/${repo_path#$os_path_pattern}"
return
fi
case "$repo_path" in
common/scripts/*)
echo "$HOME/.scripts/${repo_path#common/scripts/}"
;;
common/config/*)
case "$CFG_OS" in
linux)
local base="${XDG_CONFIG_HOME:-$HOME/.config}"
echo "$base/${repo_path#common/config/}"
;;
macos)
echo "$HOME/Library/Application Support/${repo_path#common/config/}"
;;
windows)
# Windows Bash (Git Bash, MSYS, WSL) respects LOCALAPPDATA
echo "$LOCALAPPDATA\\${repo_path#common/config/}"
;;
*)
echo "$HOME/.config/${repo_path#common/config/}"
;;
esac
;;
common/assets/*|profile/*|README.md)
echo "$HOME/.cfg/$repo_path"
;;
common/*)
echo "$HOME/.cfg/$repo_path"
;;
*/home/*)
echo "$HOME/${repo_path#*/home/}"
;;
*)
echo "$HOME/.cfg/$repo_path"
;;
esac
}
# Prompts for sudo if needed and runs the command
_sudo_prompt() {
if [[ $EUID -eq 0 ]]; then
"$@"
else
if command -v sudo >/dev/null; then
sudo "$@"
elif command -v doas >/dev/null; then
doas "$@"
elif command -v pkexec >/dev/null; then
pkexec "$@"
else
echo "Error: No privilege escalation tool found."
return 1
fi
fi
}
# Main config command
config() {
local cmd="$1"; shift
local target_dir=""
# Parse optional --target flag for add
if [[ "$cmd" == "add" ]]; then
while [[ "$1" == --* ]]; do
case "$1" in
--target|-t)
target_dir="$2"
shift 2
;;
*)
echo "Unknown option: $1"
return 1
;;
esac
done
fi
case "$cmd" in
add)
local file_path
local git_opts=()
local files=()
# Parse arguments
while [[ $# -gt 0 ]]; do
case "$1" in
--target|-t)
target_dir="$2"
shift 2
;;
-*) # Any other flags are git flags
git_opts+=("$1")
shift
;;
*) # Anything else is a file
files+=("$1")
shift
;;
esac
done
# Process each file
for file_path in "${files[@]}"; do
local repo_path
if [[ -n "$target_dir" ]]; then
local rel_path
if [[ "$file_path" == /* ]]; then
rel_path="$(basename "$file_path")"
else
rel_path="$file_path"
fi
repo_path="$target_dir/$rel_path"
else
repo_path="$(_repo_path "$file_path")"
fi
local full_repo_path="$HOME/.cfg/$repo_path"
mkdir -p "$(dirname "$full_repo_path")"
cp -a "$file_path" "$full_repo_path"
# Only git flags + repo_path go to git
_config add "${git_opts[@]}" "$repo_path"
echo "Added: $file_path -> $repo_path"
done
;;
rm)
local rm_opts=""
local file_path_list=()
for arg in "$@"; do
if [[ "$arg" == "-"* ]]; then
rm_opts+=" $arg"
else
file_path_list+=("$arg")
fi
done
for file_path in "${file_path_list[@]}"; do
local repo_path="$(_repo_path "$file_path")"
if [[ "$rm_opts" == *"-r"* ]]; then
_config rm --cached -r "$repo_path"
else
_config rm --cached "$repo_path"
fi
eval "rm $rm_opts \"$file_path\""
echo "Removed: $file_path"
done
;;
sync)
local direction="${1:-to-repo}"; shift
_config ls-files | while read -r repo_file; do
local sys_file="$(_sys_path "$repo_file")"
local full_repo_path="$HOME/.cfg/$repo_file"
if [[ "$direction" == "to-repo" ]]; then
if [[ -e "$sys_file" && -n "$(diff "$full_repo_path" "$sys_file" 2>/dev/null || echo "diff")" ]]; then
cp -a "$sys_file" "$full_repo_path"
echo "Synced to repo: $sys_file"
fi
elif [[ "$direction" == "from-repo" ]]; then
if [[ -e "$full_repo_path" && -n "$(diff "$full_repo_path" "$sys_file" 2>/dev/null || echo "diff")" ]]; then
local dest_dir="$(dirname "$sys_file")"
if [[ "$sys_file" == /* && "$sys_file" != "$HOME/"* ]]; then
_sudo_prompt mkdir -p "$dest_dir"
_sudo_prompt cp -a "$full_repo_path" "$sys_file"
else
mkdir -p "$dest_dir"
cp -a "$full_repo_path" "$sys_file"
fi
echo "Synced from repo: $sys_file"
fi
fi
done
;;
status)
# Check for missing files and auto-sync existing ones
local auto_synced=()
local missing_files=()
while read -r repo_file; do
local sys_file="$(_sys_path "$repo_file")"
local full_repo_path="$HOME/.cfg/$repo_file"
if [[ ! -e "$full_repo_path" ]]; then
missing_files+=("$repo_file")
elif [[ -e "$sys_file" ]]; then
if ! diff -q "$full_repo_path" "$sys_file" >/dev/null 2>&1; then
cp -fa "$sys_file" "$full_repo_path"
auto_synced+=("$repo_file")
fi
fi
done < <(_config ls-files)
# Report missing files
if [[ ${#missing_files[@]} -gt 0 ]]; then
echo "=== Missing Files (consider removing from git) ==="
for repo_file in "${missing_files[@]}"; do
echo "missing: $repo_file"
done
echo
fi
# Report auto-synced files
if [[ ${#auto_synced[@]} -gt 0 ]]; then
echo "=== Auto-synced Files ==="
for repo_file in "${auto_synced[@]}"; do
echo "synced: $(_sys_path "$repo_file") -> $repo_file"
done
echo
fi
_config status
;;
deploy|checkout)
echo "Deploying dotfiles from .cfg..."
_config ls-files | while read -r repo_file; do
local full_repo_path="$HOME/.cfg/$repo_file"
local sys_file="$(_sys_path "$repo_file")"
# Only continue if the source exists
if [[ -e "$full_repo_path" && -n "$sys_file" ]]; then
local dest_dir
dest_dir="$(dirname "$sys_file")"
# Create destination if needed
if [[ "$sys_file" == /* && "$sys_file" != "$HOME/"* ]]; then
_sudo_prompt mkdir -p "$dest_dir"
_sudo_prompt cp -a "$full_repo_path" "$sys_file"
else
mkdir -p "$dest_dir"
cp -a "$full_repo_path" "$sys_file"
fi
echo "Deployed: $repo_file -> $sys_file"
fi
done
;;
backup)
local timestamp=$(date +%Y%m%d%H%M%S)
local backup_dir="$HOME/.dotfiles_backup/$timestamp"
echo "Backing up existing dotfiles to $backup_dir..."
_config ls-files | while read -r repo_file; do
local sys_file="$(_sys_path "$repo_file")"
if [[ -e "$sys_file" ]]; then
local dest_dir_full="$backup_dir/$(dirname "$repo_file")"
mkdir -p "$dest_dir_full"
cp -a "$sys_file" "$backup_dir/$repo_file"
fi
done
echo "Backup complete. To restore, copy files from $backup_dir to their original locations."
;;
*)
_config "$cmd" "$@"
;;
esac
}
fi
# Make SUDO_ASKPASS agnostic: pick the first available askpass binary.
# You can predefine SUDO_ASKPASS env var to force a particular path.
: "${SUDO_ASKPASS:=""}"
# list of common askpass binaries (order: preferred -> fallback)
_askpass_candidates=(
"$SUDO_ASKPASS" # user-specified (if absolute path)
"/usr/lib/ssh/x11-ssh-askpass"
"/usr/libexec/openssh/ssh-askpass"
"/usr/lib/ssh/ssh-askpass"
"/usr/bin/ssh-askpass"
"/usr/bin/ssh-askpass-gtk"
"/usr/bin/ssh-askpass-gnome"
"/usr/bin/ssh-askpass-qt"
"/usr/bin/ksshaskpass"
"/usr/bin/zenity" # use zenity --entry as wrapper (see below)
"/usr/bin/mate-ssh-askpass"
"/usr/bin/xdg-open" # last-resort GUI helper (not ideal)
)
find_askpass() {
for p in "${_askpass_candidates[@]}"; do
[ -z "$p" ] && continue
# if user gave a path in SUDO_ASKPASS we accept it only if it's executable
if [ -n "$SUDO_ASKPASS" ] && [ "$p" = "$SUDO_ASKPASS" ]; then
[ -x "$p" ] && { printf '%s\n' "$p"; return 0; }
continue
fi
# if candidate is an absolute path, test directly
if [ "${p#/}" != "$p" ]; then
[ -x "$p" ] && { printf '%s\n' "$p"; return 0; }
continue
fi
# otherwise try to resolve via PATH
if command -v "$p" >/dev/null 2>&1; then
# For zenity, we will use a small wrapper (see below)
printf '%s\n' "$(command -v "$p")"
return 0
fi
done
return 1
}
# If zenity is chosen, use a thin wrapper script so sudo -A can call it like an askpass binary.
# This wrapper will be created in $XDG_RUNTIME_DIR or /tmp (non-persistent).
create_zenity_wrapper() {
local wrapper
wrapper="${XDG_RUNTIME_DIR:-/tmp}/.sudo_askpass_zenity.sh"
cat >"$wrapper" <<'EOF'
#!/bin/sh
# simple zenity askpass wrapper for sudo
# prints password to stdout so sudo -A works
zenity --entry --title="Authentication" --text="Elevated privileges are required" --hide-text 2>/dev/null || exit 1
EOF
chmod 700 "$wrapper"
printf '%s\n' "$wrapper"
}
# Set askpass
if [ -z "$SUDO_ASKPASS" ]; then
candidate="$(find_askpass || true)"
if [ -n "$candidate" ]; then
if command -v zenity >/dev/null 2>&1 && [ "$(command -v zenity)" = "$candidate" ]; then
# create the wrapper and export it
wrapper="$(create_zenity_wrapper)"
export SUDO_ASKPASS="$wrapper"
else
export SUDO_ASKPASS="$candidate"
fi
else
# optional: leave unset or set to empty to avoid mistakes
unset SUDO_ASKPASS
fi
fi
# debug: (uncomment to print what was chosen)
# printf 'SUDO_ASKPASS -> %s\n' "${SUDO_ASKPASS:-<none>}"
# Git
# No arguments: `git status`
# With arguments: acts like `git`
g() {
if [ $# -gt 0 ]; then
git "$@" # If arguments are provided, pass them to git
else
git status # Otherwise, show git status
fi
}
# Complete g like git
compdef g=git
# Git alias commands
ga() { g add "$@"; } # ga: Add files to the staging area
gaw() { g add -A && g diff --cached -w | g apply --cached -R; } # gaw: Add all changes to the staging area and unstage whitespace changes
grm() { g rm "$@"; }
gb() { g branch "$@"; } # gb: List branches
gbl() { g branch -l "$@"; } # gbl: List local branches
gbD() { g branch -D "$@"; } # gbD: Delete a branch
gbu() { g branch -u "$@"; } # gbu: Set upstream branch
ge() { g clone "$@"; }
gc() { g commit "$@"; } # gc: Commit changes
gcm() { g commit -m "$@"; } # gcm: Commit with a message
gca() { g commit -a "$@"; } # gca: Commit all changes
gcaa() { g commit -a --amend "$@"; } # gcaa: Amend the last commit
gcam() { g commit -a -m "$@"; } # gcam: Commit all changes with a message
gce() { g commit -e "$@"; } # gce: Commit with message and allow editing
gcfu() { g commit --fixup "$@"; } # gcfu: Commit fixes in the context of the previous commit
gco() { g checkout "$@"; } # gco: Checkout a branch or file
gcob() { g checkout -b "$@"; } # gcob: Checkout a new branch
gcoB() { g checkout -B "$@"; } # gcoB: Checkout a new branch, even if it exists
gcp() { g cherry-pick "$@"; } # gcp: Cherry-pick a commit
gcpc() { g cherry-pick --continue "$@"; } # gcpc: Continue cherry-picking after resolving conflicts
gd() { g diff "$@"; } # gd: Show changes
#gd^() { g diff HEAD^ HEAD "$@"; } # gd^: Show changes between HEAD^ and HEAD
gds() { g diff --staged "$@"; } # gds: Show staged changes
gl() { g lg "$@"; } # gl: Show a customized log
glg() { g log --graph --decorate --all "$@"; } # glg: Show a customized log with graph
gls() { # Query `glog` with regex query.
query="$1"
shift
glog --pickaxe-regex "-S$query" "$@"
}
gdc() { g diff --cached "$@"; } # gdc: Show changes between the working directory and the index
gu() { g pull "$@"} # gu: Pull
gp() { g push "$@"} # gp: Push
gpom() { g push origin main "$@"; } # gpom: Push changes to origin main
gr() { g remote "$@"; } # gr: Show remote
gra() { g rebase --abort "$@"; } # gra: Abort a rebase
grb() { g rebase --committer-date-is-author-date "$@"; } # grb: Rebase with the author date preserved
grbom() { grb --onto master "$@"; } # grbom: Rebase onto master
grbasi() { g rebase --autosquash --interactive "$@"; } # grbasi: Interactive rebase with autosquash
grc() { g rebase --continue "$@"; } # grc: Continue a rebase
grs() { g restore --staged "$@"; } # grs: Restore changes staged for the next commit
grv() { g remote -v "$@"; } # grv: Show remote URLs after each name
grh() { g reset --hard "$@"; } # grh: Reset the repository and the working directory
grH() { g reset HEAD "$@"; } # grH: Reset the index but not the working directory
#grH^() { g reset HEAD^ "$@"; } # grH^: Reset the index and working directory to the state of the HEAD's first parent
gs() { g status -sb "$@"; } # gs: Show the status of the working directory and the index
gsd() { g stash drop "$@"; } # gsd: Drop a stash
gsl() { g stash list --date=relative "$@"; } # gsl: List all stashes
gsp() { g stash pop "$@"; } # gsp: Apply and remove a single stash
gss() { g stash show "$@"; } # gss: Show changes recorded in the stash as a diff
gst() { g status "$@"; } # gst: Show the status of the working directory and the index
gsu() { g standup "$@"; } # gsu: Customized standup command
gforgotrecursive() { g submodule update --init --recursive --remote "$@"; } # gforgotrecursive: Update submodules recursively
gfp() { g commit --amend --no-edit && g push --force-with-lease "$@"; } # gfp: Amending the last commit and force-pushing
# Temporarily unset GIT_WORK_TREE
function git_without_work_tree() {
# Only proceed if a git command is being run
if [ "$1" = "git" ]; then
shift
# Check if the current directory is inside a Git work tree
if git rev-parse --is-inside-work-tree &>/dev/null; then
# If inside a work tree, temporarily unset GIT_WORK_TREE
GIT_WORK_TREE_OLD="$GIT_WORK_TREE"
unset GIT_WORK_TREE
git "$@"
export GIT_WORK_TREE="$GIT_WORK_TREE_OLD"
else
# If not inside a work tree, call git command directly
git "$@"
fi
else
# If it's not a git command, just execute it normally
command "$@"
fi
}
# Set alias conditionally
#alias git='git_without_work_tree git'
# Set bare dotfiles repository git environment variables dynamically
function set_git_env_vars() {
# Do nothing unless ~/.cfg exists and is a bare git repo
[[ -d "$HOME/.cfg" ]] || return
git --git-dir="$HOME/.cfg" rev-parse --is-bare-repository &>/dev/null || return
# Skip if last command was a package manager
if [[ "${(%)${(z)history[1]}}" =~ ^(pacman|yay|apt|dnf|brew|npm|pip|gem|go|cargo) ]]; then
return
fi
# Only set env vars if not already inside another Git repo
if ! git rev-parse --is-inside-work-tree &>/dev/null; then
export GIT_DIR="$HOME/.cfg"
export GIT_WORK_TREE="$(realpath ~)"
else
unset GIT_DIR
unset GIT_WORK_TREE
fi
}
# Hook and initial call
function chpwd() { set_git_env_vars }
set_git_env_vars
# Git Subtrees
function gsp() {
# Config file for subtrees
#
# Format:
# <prefix>;<remote address>;<remote branch>
# # Lines starting with '#' will be ignored
GIT_SUBTREE_FILE="$PWD/.gitsubtrees"
if [ ! -f "$GIT_SUBTREE_FILE" ]; then
echo "Nothing to do - file <$GIT_SUBTREE_FILE> does not exist."
return
fi
if ! command -v config &> /dev/null; then
echo "Error: 'config' command not found. Make sure it's available in your PATH."
return
fi
OLD_IFS=$IFS
IFS=$'\n'
for LINE in $(cat "$GIT_SUBTREE_FILE"); do
# Skip lines starting with '#'.
if [[ $LINE = \#* ]]; then
continue
fi
# Parse the current line.
PREFIX=$(echo "$LINE" | cut -d';' -f 1)
REMOTE=$(echo "$LINE" | cut -d';' -f 2)
BRANCH=$(echo "$LINE" | cut -d';' -f 3)
# Pull from the remote.
echo "Executing: git subtree pull --prefix=$PREFIX $REMOTE $BRANCH"
if git subtree pull --prefix="$PREFIX" "$REMOTE" "$BRANCH"; then
echo "Subtree pull successful for $PREFIX."
else
echo "Error: Subtree pull failed for $PREFIX."
fi
done
IFS=$OLD_IFS
}
# Print previous command into a file
getlast () {
fc -nl $((HISTCMD - 1))
}
# Copy the current command to a file
copy_command_to_file() {
# Only write the last command if BUFFER is not empty
if [[ -n "$BUFFER" ]]; then
echo "$BUFFER" > ~/command_log.txt # Overwrite with the latest command
else
# If the buffer is empty, remove the previous log file
command rm -f ~/command_log.txt # Optionally remove the log if no command is present
fi
}
# Display the latest command from the log in the user input
display_latest_command() {
if [[ -f ~/command_log.txt ]]; then
# Read the last command from the log
local last_command
last_command=$(< ~/command_log.txt)
# Only display if the last command is not empty
if [[ -n "$last_command" ]]; then
BUFFER="$last_command" # Set the BUFFER to the last command
CURSOR=${#BUFFER} # Set the cursor to the end of the command
fi
fi
zle reset-prompt # Refresh the prompt
}
# Go up a directory
go_up() {
copy_command_to_file # Copy the current command to a file
BUFFER="" # Clear the current command line
cd .. || return # Change directory and return if it fails
display_latest_command # Display the latest command in the user input
}
# Initialize a variable to store the previous directory
previous_dir=""
# Function to change directories
go_into() {
copy_command_to_file # Copy the current command to a file
# Use fzf or another tool to choose the directory
local dir
dir=$( (ls -d */; echo "Go Last directory:") | fzf --height 40% --reverse --tac) # Include previous directory as an option
if [[ -n "$dir" ]]; then
# Check if the user selected the previous directory
if [[ "$dir" == Previous:* ]]; then
cd - || return # Change to the previous directory
else
cd "${dir%/}" || return # Change directory if a selection is made (remove trailing slash)
fi
# Save the current directory to previous_dir
previous_dir=$(pwd) # Update previous_dir to current directory after changing
BUFFER="" # Clear the current command line
display_latest_command # Display the last command if available
fi
}
# Register functions as ZLE widgets
zle -N go_up
zle -N go_into
# XDG_GAMES_DIR:
# Path to user-dirs config
USER_DIRS_FILE="$HOME/.config/user-dirs.dirs"
if [ -f "$USER_DIRS_FILE" ]; then
# Extract directory names from user-dirs config
_dirs=(
${(f)"$(grep '^XDG_.*_DIR=' "$USER_DIRS_FILE" \
| cut -d= -f2 \
| tr -d '"' \
| sed "s|^\$HOME/||")"}
)
_lowercase_count=0
_total_count=0
for d in "${_dirs[@]}"; do
[ -n "$d" ] || continue
_total_count=$(( _total_count + 1 ))
case "$d" in
[a-z0-9]*)
_lowercase_count=$(( _lowercase_count + 1 ))
;;
esac
done
# Require majority lowercase (≥70%)
if [ "$_total_count" -gt 0 ]; then
_percent=$(( 100 * _lowercase_count / _total_count ))
if [ "$_percent" -ge 70 ]; then
# Ensure the lowercase games directory exists
if [ ! -d "$HOME/games" ]; then
mkdir -p "$HOME/games"
fi
# Create symbolic link if it doesn't already exist
if [ ! -L "$HOME/Games" ] && [ ! -d "$HOME/Games" ]; then
ln -s "$HOME/games" "$HOME/Games"
fi
export XDG_GAMES_DIR="$HOME/games"
fi
fi
unset _dirs _lowercase_count _total_count _percent
fi
# Enter directory and list contents
function cd-clear-ls() {
if [ -n "$1" ]; then
builtin cd "$@" 2>/dev/null || { echo "cd: no such file or directory: $1"; return 1; }
else
builtin cd ~ || return 1
fi
echo -e "\033[H\033[J" # Clear screen but keep scroll buffer
if [ "$PWD" != "$HOME" ] && git rev-parse --is-inside-work-tree &>/dev/null; then
ls -a
else
ls
fi
}
# cd using "up n" as a command up as many directories, example "up 3"
up() {
# default parameter to 1 if non provided
declare -i d=${@:-1}
# ensure given parameter is non-negative. Print error and return if it is
(( $d < 0 )) && (>&2 echo "up: Error: negative value provided") && return 1;
# remove last d directories from pwd, append "/" in case result is empty
cd "$(pwd | sed -E 's;(/[^/]*){0,'$d'}$;;')/";
}
# cd into $XDG_CONFIG_HOME/$1 directory
c() {
local root=${XDG_CONFIG_HOME:-~/.config}
local dname="$root/$1"
if [[ ! -d "$dname" ]]; then
return
fi
cd "$dname"
}
# Make and cd into directory and any parent directories
mkcd () {
if [[ -z "$1" ]]; then
echo "Usage: mkcd <dir>" 1>&2
return 1
fi
mkdir -p "$1"
cd "$1"
}
bak() {
if [[ -e "$1" ]]; then
echo "Found: $1"
mv "${1%.*}"{,.bak}
elif [[ -e "$1.bak" ]]; then
echo "Found: $1.bak"
mv "$1"{.bak,}
fi
}
back() {
for file in "$@"; do
cp -r "$file" "$file".bak
done
}
# tre is a shorthand for tree
tre() {
tree -aC -I \
'.git|.hg|.svn|.tmux|.backup|.vim-backup|.swap|.vim-swap|.undo|.vim-undo|*.bak|tags' \
--dirsfirst "$@" \
| less
}
# switch from/to project/package dir
pkg() {
if [ "$#" -eq 2 ]; then
ln -s "$(readlink -f $1)" "$(readlink -f $2)"/._pkg
ln -s "$(readlink -f $2)" "$(readlink -f $1)"/._pkg
else
cd "$(readlink -f ./._pkg)"
fi
}
# Prepare C/C++ project for Language Server Protoco
lsp-prep() {
(cd build && cmake .. -DCMAKE_EXPORT_COMPILE_COMMANDS=ON) \
&& ln -sf build/compile_commands.json
}
reposize() {
url=`echo $1 \
| perl -pe 's#(?:https?://github.com/)([\w\d.-]+\/[\w\d.-]+).*#\1#g' \
| perl -pe 's#git\@github.com:([\w\d.-]+\/[\w\d.-]+)\.git#\1#g'
`
printf "https://github.com/$url => "
curl -s https://api.github.com/repos/$url \
| jq '.size' \
| numfmt --to=iec --from-unit=1024
}
# Launch a program in a terminal without getting any output,
# and detach the process from terminal
# (can then close the terminal without terminating process)
-echo() {
"$@" &> /dev/null & disown
}
# Reload shell
function reload() {
local compdump_files="$ZDOTDIR/.zcompdump*"
if ls $compdump_files &> /dev/null; then
rm -f $compdump_files
fi
exec $SHELL -l
}
#pom() {
# local -r HOURS=${1:?}
# local -r MINUTES=${2:-0}
# local -r POMODORO_DURATION=${3:-25}
#
# bc <<< "(($HOURS * 60) + $MINUTES) / $POMODORO_DURATION"
#}
#mnt() {
# local FILE="/mnt/external"
# if [ ! -z $2 ]; then
# FILE=$2
# fi
#
# if [ ! -z $1 ]; then
# sudo mount "$1" "$FILE" -o rw
# echo "Device in read/write mounted in $FILE"
# fi
#
# if [ $# = 0 ]; then
# echo "You need to provide the device (/dev/sd*) - use lsblk"
# fi
#}
#
#umnt() {
# local DIRECTORY="/mnt"
# if [ ! -z $1 ]; then
# DIRECTORY=$1
# fi
# MOUNTED=$(grep $DIRECTORY /proc/mounts | cut -f2 -d" " | sort -r)
# cd "/mnt"
# sudo umount $MOUNTED
# echo "$MOUNTED unmounted"
#}
mntmtp() {
local DIRECTORY="$HOME/mnt"
if [ ! -z $2 ]; then
local DIRECTORY=$2
fi
if [ ! -d $DIRECTORY ]; then
mkdir $DIRECTORY
fi
if [ ! -z $1 ]; then
simple-mtpfs --device "$1" "$DIRECTORY"
echo "MTPFS device in read/write mounted in $DIRECTORY"
fi
if [ $# = 0 ]; then
echo "You need to provide the device number - use simple-mtpfs -l"
fi
}
umntmtp() {
local DIRECTORY="$HOME/mnt"
if ; then
DIRECTORY=$1
fi
cd $HOME
umount $DIRECTORY
echo "$DIRECTORY with mtp filesystem unmounted"
}
duckduckgo() {
lynx -vikeys -accept_all_cookies "https://lite.duckduckgo.com/lite/?q=$@"
}
wikipedia() {
lynx -vikeys -accept_all_cookies "https://en.wikipedia.org/wiki?search=$@"
}
#function filesize() {
# # Check if 'du' supports the -b option, which provides sizes in bytes.
# if du -b /dev/null > /dev/null 2>&1; then
# local arg=-sbh; # If supported, use -sbh options for 'du'.
# else
# local arg=-sh; # If not supported, use -sh options for 'du'.
# fi
#
# # Check if no arguments are provided.
# if [ "$#" -eq 0 ]; then
# # Calculate and display sizes for all files and directories in cwd.
# du $arg ./*
# else
# # Calculate and display sizes for the specified files and directories.
# du $arg -- "$@"
# fi
#}
#
fgl() {
git log --graph --color=always \
--format="%C(auto)%h%d %s %C(black)%C(bold)%cr" "$@" |
fzf --ansi --no-sort --reverse --tiebreak=index --bind=ctrl-s:toggle-sort \
--bind "ctrl-m:execute:
(grep -o '[a-f0-9]\{7\}' | head -1 |
xargs -I % sh -c 'git show --color=always % | less -R') << 'FZF-EOF'
{}
FZF-EOF"
}
fgb() {
local branches branch
branches=$(git --no-pager branch -vv) &&
branch=$(echo "$branches" | fzf +m) &&
git checkout $(echo "$branch" | awk '{print $1}' | sed "s/.* //")
}
# +--------+
# | Pacman |
# +--------+
# TODO can improve that with a bind to switch to what was installed
fpac() {
pacman -Slq | fzf --multi --reverse --preview 'pacman -Si {1}' | xargs -ro sudo pacman -S
}
fyay() {
yay -Slq | fzf --multi --reverse --preview 'yay -Si {1}' | xargs -ro yay -S
}
# +------+
# | tmux |
# +------+
fmux() {
prj=$(find $XDG_CONFIG_HOME/tmuxp/ -execdir bash -c 'basename "${0%.*}"' {} ';' | sort | uniq | nl | fzf | cut -f 2)
echo $prj
[ -n "$prj" ] && tmuxp load $prj
}
# ftmuxp - propose every possible tmuxp session
ftmuxp() {
if [[ -n $TMUX ]]; then
return
fi
# get the IDs
ID="$(ls $XDG_CONFIG_HOME/tmuxp | sed -e 's/\.yml$//')"
if [[ -z "$ID" ]]; then
tmux new-session
fi
create_new_session="Create New Session"
ID="${create_new_session}\n$ID"
ID="$(echo $ID | fzf | cut -d: -f1)"
if [[ "$ID" = "${create_new_session}" ]]; then
tmux new-session
elif [[ -n "$ID" ]]; then
# Change name of urxvt tab to session name
printf '\033]777;tabbedx;set_tab_name;%s\007' "$ID"
tmuxp load "$ID"
fi
}
# ftmux - help you choose tmux sessions
ftmux() {
if [[ ! -n $TMUX ]]; then
# get the IDs
ID="`tmux list-sessions`"
if [[ -z "$ID" ]]; then
tmux new-session
fi
create_new_session="Create New Session"
ID="$ID\n${create_new_session}:"
ID="`echo $ID | fzf | cut -d: -f1`"
if [[ "$ID" = "${create_new_session}" ]]; then
tmux new-session
elif [[ -n "$ID" ]]; then
printf '\033]777;tabbedx;set_tab_name;%s\007' "$ID"
tmux attach-session -t "$ID"
else
: # Start terminal normally
fi
fi
}
# +-------+
# | Other |
# +-------+
# List install files for dotfiles
fdot() {
file=$(find "$DOTFILES/install" -exec basename {} ';' | sort | uniq | nl | fzf | cut -f 2)
[ -n "$file" ] && "$EDITOR" "$DOTFILES/install/$file"
}
# List projects
fwork() {
result=$(find ~/workspace/* -type d -prune -exec basename {} ';' | sort | uniq | nl | fzf | cut -f 2)
[ -n "$result" ] && cd ~/workspace/$result
}
# Open pdf with Zathura
fpdf() {
result=$(find -type f -name '*.pdf' | fzf --bind "ctrl-r:reload(find -type f -name '*.pdf')" --preview "pdftotext {} - | less")
[ -n "$result" ] && nohup zathura "$result" &> /dev/null & disown
}
# Open epubs with Zathura
fepub() {
result=$(find -type f -name '*.epub' | fzf --bind "ctrl-r:reload(find -type f -name '*.epub')")
[ -n "$result" ] && nohup zathura "$result" &> /dev/null & disown
}
# Search and find directories in the dir stack
fpop() {
# Only work with alias d defined as:
# alias d='dirs -v'
# for index ({1..9}) alias "$index"="cd +${index}"; unset index
d | fzf --height="20%" | cut -f 1 | source /dev/stdin
}
#ip() {
# emulate -LR zsh
#
# if [[ $1 == 'get' ]]; then
# res=$(curl -s ipinfo.io/ip)
# echo -n $res | xsel --clipboard
# echo "copied $res to clipboard"
# # only run ip if it exists
# elif (( $+commands[ip] )); then
# command ip $*
# fi
#}
ssh-create() {
if [ ! -z "$1" ]; then
ssh-keygen -f $HOME/.ssh/$1 -t rsa -N '' -C "$1"
chmod 700 $HOME/.ssh/$1*
fi
}
guest() {
local guest="$1"
shift
local port
if [[ "$#" -ge 2 && "${@: -1}" =~ ^[0-9]+$ ]]; then
port="${@: -1}"
set -- "${@:1:$(($#-1))}"
fi
if [[ -z "$guest" || "$#" -lt 1 ]]; then
echo "Send file(s) or directories to remote machine"
echo "Usage: guest <guest-alias> <file-or-directory>... [port]"
return 1
fi
# Auto-detect port
if [[ -z "$port" ]]; then
if nc -z localhost 22220 2>/dev/null; then
port=22220
elif nc -z localhost 22 2>/dev/null; then
port=22
else
echo "No known SSH port (22220 or 22) is open. Specify a port manually."
return 1
fi
fi
for src in "$@"; do
src="${src/#\~/$HOME}"
if [[ ! -e "$src" ]]; then
echo "Error: '$src' does not exist."
continue
fi
local abs_path dest_dir rel_dir rsync_src rsync_dest
abs_path=$(realpath "$src")
rel_dir="${abs_path#$HOME/}"
dest_dir=$(dirname "$rel_dir")
# Ensure target dir exists remotely
ssh -p "$port" "$guest" "mkdir -p ~/$dest_dir"
if [[ -d "$src" ]]; then
# Add trailing slash to copy contents instead of nesting the dir
rsync_src="${src%/}/"
rsync_dest="~/$rel_dir/"
else
rsync_src="$src"
rsync_dest="~/$dest_dir/"
fi
echo "Sending '$src' to '$guest:$rsync_dest'..."
rsync -avz -e "ssh -p $port" "$rsync_src" "$guest:$rsync_dest"
done
}
historystat() {
history 0 | awk '{print $2}' | sort | uniq -c | sort -n -r | head
}
promptspeed() {
for i in $(seq 1 10); do /usr/bin/time zsh -i -c exit; done
}
matrix() {
local lines=$(tput lines)
cols=$(tput cols)
# Check if tmux is available
if command -v tmux > /dev/null; then
# Save the current status setting
local status_setting=$(tmux show -g -w -v status)
# Turn off tmux status
tmux set -g status off
else
echo "tmux is not available. Exiting."
return 1
fi
# Function to restore terminal state
restore_terminal() {
# Clear the screen
clear
# Bring back tmux status to its original setting
if command -v tmux > /dev/null; then
tmux set -g status "$status_setting"
fi
}
trap 'restore_terminal' INT
awkscript='
{
letters="abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789@#$%^&*()"
lines=$1
random_col=$3
c=$4
letter=substr(letters,c,1)
cols[random_col]=0;
for (col in cols) {
line=cols[col];
cols[col]=cols[col]+1;
printf "\033[%s;%sH\033[2;32m%s", line, col, letter;
printf "\033[%s;%sH\033[1;37m%s\033[0;0H", cols[col], col, letter;
if (cols[col] >= lines) {
cols[col]=0;
}
}
}
'
echo -e "\e[1;40m"
clear
while :; do
echo $lines $cols $(( $RANDOM % $cols)) $(( $RANDOM % 72 ))
sleep 0.05
done | awk "$awkscript"
# Restore terminal state
restore_terminal
}
## Reload shell
function reload() {
local compdump_files="$ZDOTDIR/.zcompdump*"
if ls $compdump_files &> /dev/null; then
rm -f $compdump_files
fi
exec $SHELL -l
}
## Generate a secure password
function passgen() {
LC_ALL=C tr -dc ${1:-"[:graph:]"} < /dev/urandom | head -c ${2:-20}
}
## Encode/Decode string using base64
function b64e() {
echo "$@" | base64
}
function b64d() {
echo "$@" | base64 -D
}
# Search through all man pages
function fman() {
man -k . | fzf -q "$1" --prompt='man> ' --preview $'echo {} | tr -d \'()\' | awk \'{printf "%s ", $2} {print $1}\' | xargs -r man' | tr -d '()' | awk '{printf "%s ", $2} {print $1}' | xargs -r man
}
# Back up a file. Usage "backupthis <filename>"
backupthis() {
cp -riv $1 ${1}-$(date +%Y%m%d%H%M).backup;
}
# Spawn a clone of current terminal
putstate () {
declare +x >~/environment.tmp
declare -x >>~/environment.tmp
echo cd "$PWD" >>~/environment.tmp
}
getstate () {
. ~/environment.tmp
}
# Tmux layout
openSession () {
tmux split-window -h -t
tmux split-window -v -t
tmux resize-pane -U 5
}
# archive compress
compress() {
if [[ -n "$1" ]]; then
local file=$1
shift
case "$file" in
*.tar ) tar cf "$file" "$*" ;;
*.tar.bz2 ) tar cjf "$file" "$*" ;;
*.tar.gz ) tar czf "$file" "$*" ;;
*.tgz ) tar czf "$file" "$*" ;;
*.zip ) zip "$file" "$*" ;;
*.rar ) rar "$file" "$*" ;;
* ) tar zcvf "$file.tar.gz" "$*" ;;
esac
else
echo 'usage: compress <foo.tar.gz> ./foo ./bar'
fi
}
extract() {
if [[ -f "$1" ]] ; then
local filename
filename=$(basename "$1")
local foldername="${filename%%.*}"
local fullpath
fullpath=$(perl -e 'use Cwd "abs_path";print abs_path(shift)' "$1")
local didfolderexist=false
if [[ -d "$foldername" ]]; then
didfolderexist=true
read -p "$foldername already exists, do you want to overwrite it? (y/n) " -n 1
echo
if [[ "$REPLY" =~ ^[Nn]$ ]]; then
return
fi
fi
mkdir -p "$foldername" && cd "$foldername" || return
case "$1" in
*.tar.bz2) tar xjf "$fullpath" ;;
*.tar.gz) tar xzf "$fullpath" ;;
*.tar.xz) tar Jxf "$fullpath" ;;
*.tar.Z) tar xzf "$fullpath" ;;
*.tar) tar xf "$fullpath" ;;
*.taz) tar xzf "$fullpath" ;;
*.tb2) tar xjf "$fullpath" ;;
*.tbz) tar xjf "$fullpath" ;;
*.tbz2) tar xjf "$fullpath" ;;
*.tgz) tar xzf "$fullpath" ;;
*.txz) tar Jxf "$fullpath" ;;
*.rar) unrar x -o+ "$fullpath" >/dev/null ;;
*.zip) unzip -o "$fullpath" ;;
*)
echo "'$1' cannot be extracted via extract()" \
&& cd .. \
&& ! "$didfolderexist" \
&& rm -r "$foldername"
;;
esac
else
echo "'$1' is not a valid file"
fi
}
ports() {
local result
result=$(sudo netstat -tulpn | grep LISTEN)
echo "$result" | fzf
}
trash() {
case "$1" in
--list)
ls -A1 ~/.local/share/Trash/files/
;;
--empty)
ls -A1 ~/.local/share/Trash/files/ && \rm -rfv ~/.local/share/Trash/files/*
;;
--restore)
gio trash --restore "$(gio trash --list | fzf | cut -f 1)"
;;
--delete)
trash_files=$(ls -A ~/.local/share/Trash/files/ | fzf --multi); echo $trash_files | xargs -I {} rm -rf ~/.local/share/Trash/files/{}
;;
*)
gio trash "$@"
;;
esac
}
what() {
type "$1"
echo "$PATH"
}
shutdown() {
if [ "$#" -eq 0 ]; then
sudo /sbin/shutdown -h now
else
sudo /sbin/shutdown -h "$@"
fi
}
windowManagerName () {
local window=$(
xprop -root -notype
)
local identifier=$(
echo "${window}" |
awk '$1=="_NET_SUPPORTING_WM_CHECK:"{print $5}'
)
local attributes=$(
xprop -id "${identifier}" -notype -f _NET_WM_NAME 8t
)
local name=$(
echo "${attributes}" |
grep "_NET_WM_NAME = " |
cut --delimiter=' ' --fields=3 |
cut --delimiter='"' --fields=2
)
echo "${name}"
}
logout() {
local wm
wm="$(windowManagerName)"
if [[ -n "$wm" ]]; then
echo "Logging out by killing window manager: $wm"
pkill "$wm"
else
echo "No window manager detected!" >&2
fi
}
# Gentoo
emg() {
if [[ -z "$1" ]]; then
echo "Usage: emg [USE_FLAGS] package [package...]"
return 1
fi
if [[ "$1" =~ ^[^-].* ]]; then
local use_flags="$1"
shift
sudo USE="$use_flags" emerge -av "$@"
else
sudo emerge -av "$@"
fi
}
# Remove command from history
forget () { # Accepts one history line number as argument or search term
if [[ -z "$1" ]]; then
echo "Usage: hist <history_number> | hist -s <search_term>"
return 1
fi
if [[ "$1" == "-s" ]]; then
if [[ -z "$2" ]]; then
echo "Usage: hist -s <search_term>"
return 1
fi
local search_term="$2"
if [[ "$OSTYPE" == "linux-gnu"* ]]; then
LC_ALL=C sed -i "/${search_term}/d" "$HISTFILE" # GNU sed
else
LC_ALL=C sed -i '' "/${search_term}/d" "$HISTFILE" # BSD/macOS sed
fi
fc -R "$HISTFILE"
echo "Deleted all history entries matching '$search_term'."
else
local num=$1
local cmd=$(fc -ln $num $num 2>/dev/null)
if [[ -z "$cmd" ]]; then
echo "No history entry found for index $num"
return 1
fi
history -d $num
local escaped_cmd=$(echo "$cmd" | sed 's/[\/&]/\\&/g')
if [[ "$OSTYPE" == "linux-gnu"* ]]; then
LC_ALL=C sed -i "/${escaped_cmd}/d" "$HISTFILE"
else
LC_ALL=C sed -i '' "/${escaped_cmd}/d" "$HISTFILE"
fi
fc -R "$HISTFILE"
echo "Deleted '$cmd' from history."
fi
}
# Remove hist command itself
remove_hist_command() {
[[ $1 != 'hist '* ]]
}
remove_hist_command
search() {
# Search for a pattern in the specified directory (non-recursive).
dir="${1:-.}"
ls -1 "$dir" | grep -i "$2"
}
deepsearch() {
# Perform a recursive search for a pattern in the specified directory.
dir="${1:-.}"
find "$dir" -iname "$2"
}
notes() {
local base_dir="$HOME/documents/main/"
if [[ -z "$1" ]]; then
# No argument → cd to notes directory
cd "$base_dir" || return
return
fi
local target="$1" # The argument itself
# Use find to check if the file exists anywhere in the base directory
local found_files=($(find "$base_dir" -type f -name "$target"))
if [[ ${#found_files[@]} -eq 1 ]]; then
# Only one match found, open it directly
$EDITOR "${found_files[0]}"
elif [[ ${#found_files[@]} -gt 1 ]]; then
# Multiple files found, prompt the user to select one
echo "Multiple files found for '$target'. Please choose one:"
PS3="Please enter a number to select a file (1-${#found_files[@]}): "
select selected_file in "${found_files[@]}"; do
if [[ -n "$selected_file" ]]; then
$EDITOR "$selected_file"
break
else
echo "Invalid selection, try again."
fi
done
else
# If no match found, search for a directory
local found_dir=$(find "$base_dir" -type d -name "$target" -print -quit)
if [[ -n "$found_dir" ]]; then
# Directory found, cd into it
cd "$found_dir" || return
else
# If no match found, create the file and open it
local full_target="$base_dir/$target"
mkdir -p "$(dirname "$full_target")"
$EDITOR "$full_target"
fi
fi
}
# Enable tab completion for files and directories
_notes_complete() {
local base_dir="$HOME/documents/main"
compadd -o nospace -- $(find "$base_dir" -type f -o -type d -printf '%P\n')
}
compdef _notes_complete notes
ship() {
local binary_dir="$HOME/.local/share"
local bin_symlink_dir="$HOME/.local/bin"
local project_dirs=(
"$HOME/projects/"
"$HOME/src/"
"$HOME/src/site/"
)
mkdir -p "$binary_dir" "$bin_symlink_dir"
local project_dir=""
if [[ -n "$1" ]]; then
# Project name specified
for dir in "${project_dirs[@]}"; do
if [[ -d "$dir/$1" ]]; then
project_dir="$dir/$1"
break
fi
done
if [[ -z "$project_dir" ]]; then
echo "Project '$1' not found."
return 1
fi
else
# No argument: pick latest edited
local bin_file
bin_file=$(find "${project_dirs[@]}" -type f -name "Cargo.toml" -exec stat --format="%Y %n" {} \; 2>/dev/null | sort -nr | head -n1 | cut -d' ' -f2-)
if [[ -z "$bin_file" ]]; then
echo "No Cargo.toml found."
return 1
fi
project_dir=$(dirname "$bin_file")
fi
cd "$project_dir" || return
echo "Building project in $project_dir..."
# Build it
cargo build --release || { echo "Build failed"; return 1; }
# Assume binary has same name as project dir
local binary_name
binary_name=$(basename "$project_dir")
local built_binary="target/release/$binary_name"
if [[ -x "$built_binary" ]]; then
echo "Copying $built_binary to $binary_dir/$binary_name"
cp "$built_binary" "$binary_dir/$binary_name"
# Create/Update symlink
local symlink_path="$bin_symlink_dir/$binary_name"
ln -sf "$binary_dir/$binary_name" "$symlink_path"
echo "Binary is now at: $binary_dir/$binary_name"
echo "Symlink created at: $symlink_path"
else
echo "Built binary not found: $built_binary"
echo "You may need to manually specify the output binary."
fi
}
forge() {
local install=no
local usage="Usage: forge [--install]"
# Handle --install flag
if [[ "$1" == "--install" ]]; then
install=yes
shift
elif [[ "$1" == "-h" || "$1" == "--help" ]]; then
echo "$usage"
return 0
fi
if [[ -f "CMakeLists.txt" ]]; then
echo "📦 CMake project detected"
[[ ! -d build ]] && mkdir build
cmake -B build -DCMAKE_BUILD_TYPE=Release || return 1
cmake --build build || return 1
[[ "$install" == "yes" ]] && sudo cmake --install build
elif [[ -f "meson.build" ]]; then
echo "📦 Meson project detected"
if [[ ! -d build ]]; then
meson setup build || return 1
fi
ninja -C build || return 1
[[ "$install" == "yes" ]] && sudo ninja -C build install
elif [[ -f "Makefile" ]]; then
echo "📦 Makefile project detected"
# Try `make all`, fallback to `make` if `all` fails
if make -q all 2>/dev/null; then
make all || return 1
else
make || return 1
fi
[[ "$install" == "yes" ]] && sudo make install
else
echo "❌ No supported build system found."
return 1
fi
}
# Windows Path:
windows_home() {
for dir in /mnt/windows/Users/*(N); do
base=${dir:t} # `:t` is zsh's "tail" = basename
if [[ -d $dir && ! $base =~ ^(All Users|Default|Default User|Public|nx|desktop.ini)$ ]]; then
echo "$dir"
return 0
fi
done
return 1
}
if winhome_path=$(windows_home); then
hash -d winhome="$winhome_path"
fi
# Allow nnn filemanager to cd on quit
nnn() {
declare -x +g NNN_TMPFILE=$(mktemp --tmpdir $0.XXXX)
trap "command rm -f $NNN_TMPFILE" EXIT
=nnn $@
[ -s $NNN_TMPFILE ] && source $NNN_TMPFILE
}
|