fe5d60fce42c9a8fb1a7b567ed0a2bce0517f77e
[distro-setup] / brc2
1 #!/bin/bash
2 # I, Ian Kelling, follow the GNU license recommendations at
3 # https://www.gnu.org/licenses/license-recommendations.en.html. They
4 # recommend that small programs, < 300 lines, be licensed under the
5 # Apache License 2.0. This file contains or is part of one or more small
6 # programs. If a small program grows beyond 300 lines, I plan to switch
7 # its license to GPL.
8
9 # Copyright 2024 Ian Kelling
10
11 # Licensed under the Apache License, Version 2.0 (the "License");
12 # you may not use this file except in compliance with the License.
13 # You may obtain a copy of the License at
14
15 # http://www.apache.org/licenses/LICENSE-2.0
16
17 # Unless required by applicable law or agreed to in writing, software
18 # distributed under the License is distributed on an "AS IS" BASIS,
19 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
20 # See the License for the specific language governing permissions and
21 # limitations under the License.
22
23 # this gets sourced. shebang is just for file mode detection
24
25
26 # * settings
27
28 if [[ $LESSHISTFILE == - ]]; then
29 HISTFILE=
30 c() { cd "$@"; }
31 elif [[ $HISTFILE ]]; then
32 # use an alternate history file when we are streaming.
33 if [[ -e $HOME/.iank-stream-on ]]; then
34 HISTFILE=/a/bin/data/stream_hist
35 else
36 HISTFILE=$HOME/.bh
37 fi
38 fi
39
40 # history personal
41 hip() {
42 history -c
43 HISTFILE=$HOME/.bh
44 history -r
45 }
46
47 # history for streaming
48 his() {
49 history -c
50 HISTFILE=/a/bin/data/stream_hist
51 history -r
52 }
53
54
55 source /a/bin/distro-setup/path-add-function
56 path-add /a/exe
57 # add this with absolute paths as needed for better security
58 #path-add --end /path/to/node_modules/.bin
59 ## for yarn, etc
60 #path-add --end /usr/lib/node_modules/corepack/shims/
61
62 # pip3 --user things go here:
63 path-add --end ~/.local/bin
64 path-add --ifexists --end /a/work/libremanage
65 path-add --ifexists --end /a/opt/adt-bundle*/tools /a/opt/adt-bundle*/platform-tools
66 path-add --ifexists --end /a/opt/scancode-toolkit-3.10.
67 path-add --ifexists --end /p/bin
68
69 case $HOSTNAME in
70 sy|bo)
71 # https://askubuntu.com/questions/1254544/vlc-crashes-when-opening-any-file-ubuntu-20-04
72 if grep -qE '^VERSION_CODENAME="(nabia|focal)"' /etc/os-release &>/dev/null; then
73 export MESA_LOADER_DRIVER_OVERRIDE=i965
74 fi
75 ;;
76 esac
77
78
79 export WCDHOME=/a
80
81
82 case $EUID in
83 0)
84 # shellcheck disable=SC2034 # used in brc
85 SL_SSH_ARGS="-F $HOME/.ssh/confighome"
86 ;;
87 esac
88
89
90 # * include files
91
92 # generated instead of dynamic for the benefit of shellcheck
93 #for x in /a/bin/distro-functions/src/* /a/bin/!(githtml)/*-function?(s); do echo source $x ; done
94 source /a/bin/distro-functions/src/identify-distros
95 source /a/bin/log-quiet/logq-function
96 # for x in /a/bin/bash_unpublished/source-!(.#*); do echo source $x; done
97 source /a/bin/bash_unpublished/source-semi-priv
98 source /a/bin/bash_unpublished/source-state
99
100 if [[ $HOSTNAME == "$MAIL_HOST" ]]; then
101 export MAIL_HOST_P=t
102 else
103 export NOT_MAIL_HOST_P=t
104 fi
105
106 source /a/bin/ds/beet-data
107
108
109 # * functions
110
111
112
113 multimic() {
114 local i
115 local -a sources
116
117 m pactl unload-module module-loopback
118 m pactl unload-module module-null-sink
119 m pactl unload-module module-remap-source
120
121 IFS=" " read -r -a sources <<<"$(pacmd list-sources | sed -rn 's/.*name: <([^>]+).*/\1/p')"
122
123 if (( ! $# )); then
124 i=0
125 for s in ${sources[@]}; do
126 e $i $s
127 i=$(( i+1 ))
128 done
129 read -r l
130 set -- $l
131 fi
132 m pactl load-module module-null-sink sink_name=ianinput sink_properties=device.description=ianinputs
133 for i; do
134 m pactl load-module module-loopback source=${sources[i]} sink_dont_move=true sink=ianinput
135 done
136 pactl load-module module-remap-source source_name=iancombine master=ianinput.monitor source_properties=device.description=iancombine
137 }
138
139 # h ssh test
140 # For testing restrictive ssh.
141 hstest() {
142 install-my-scripts
143 d=$(mktemp -d)
144 sed '/^ *IdentityFile/d' ~/.ssh/config >$d/config
145 s command ssh -F $d/config -i /q/root/h "$@"
146 }
147
148 # h rsync test
149 # For testing restrictive rsync
150 hrtest() { #
151 install-my-scripts
152 d=$(mktemp -d)
153 sed '/^ *IdentityFile/d' ~/.ssh/config >$d/config
154 s rsync -e "ssh -F $d/config -i /q/root/h" "$@"
155 }
156
157 # rsync as root and avoid the default restrictive h key & config.
158 rootrsync() {
159 s rsync -e "ssh -F /root/.ssh/confighome" "$@"
160 }
161
162 zcheck() {
163 ssh bow DISPLAY=:0 scrot /tmp/oegu.jpg
164 scp bow:/tmp/oegu.jpg /t
165 ssh bow rm /tmp/oegu.jpg
166 feh /t/oegu.jpg
167 }
168 zmon() {
169 while true; do
170 ziva-screen
171 sleep 15
172 done
173 }
174
175 slemacs() {
176 local arg rtime v
177 arg="$1"
178 remote="$2"
179 if [[ $arg == [89]0Etiona* ]]; then
180 v=${arg::1}
181 rtime=${arg#*Etiona} # remote time
182 if [[ ! $rtime ]]; then
183 rtime=0
184 fi
185 dir=/a/opt/emacs-trisquel${v}-nox/.iank
186 ltime=$(stat -c%Y $dir/e/e/.emacs.d/init.el)
187 if (( ltime > rtime )); then
188 m rsync -rptL --delete --filter=". /b/ds/sl/rsync-filter" $dir "$remote":/home/iank
189 fi
190 fi
191 }
192
193 sle() { # sl emacs
194 local f=/home/iank/.emacs.d/init.el
195 sl --sl-test-cmd "sed -rn '/^VERSION=/{s/^.*=//;s/[^[:alnum:]]//gp}' /etc/os-release; test -e $f && stat -c%Y $f" --sl-test-hook slemacs "$@"
196 }
197 ccomp ssh sle
198
199 # Run this manually after .emacs.d changes. Otherwise, to check if
200 # files changed with find takes 90ms. sl normally only adds 25ms. We
201 # could cut it down to 10ms if we put things on a btrfs filesystem and
202 # looked for changes there, or used some inotify thing, but that seems
203 # like too much work.
204 egh() { # emacs gnuhope
205 RSYNC_RSH=ssh m rsync -rptL --delete --filter=". /b/ds/sl/rsync-filter" /a/opt/emacs-trisquel9-nox/.iank lists2d.fsf.org:.ianktrisquel_9
206 RSYNC_RSH=ssh m rsync -rptL --delete --filter=". /b/ds/sl/rsync-filter" /a/opt/emacs-trisquel8-nox/.iank lists2d.fsf.org:/home/iank
207 }
208 ekw() {
209 local shell="bash -s"
210 if [[ $HOSTNAME != kw ]]; then
211 shell="ssh kw.office.fsf.org"
212 bbk -m /a -t kw
213 fi
214 $shell <<'EOF'
215 sudo mkdir /root/.ianktrisquel_9
216 sudo rsync -rptL --delete --filter=". /b/ds/sl/rsync-filter" /a/opt/emacs-trisquel9-nox/.iank /root/.ianktrisquel_9
217 rsync -rptL --delete --filter=". /b/ds/sl/rsync-filter" /a/opt/emacs-trisquel8-nox/.iank /home/iank
218 EOF
219 }
220
221 rm-docker-iptables() {
222 s iptables -S | gr docker | gr -- -A | sed 's/-A/-D/'| while read -r l; do sudo iptables $l; done
223 s iptables -S -t nat | gr docker | gr -- -A | sed 's/-A/-D/'| while read -r l; do sudo iptables -t nat $l; done
224 s iptables -S | gr docker | gr -- -N | sed 's/-N/-X/'| while read -r l; do sudo iptables $l; done
225 s iptables -S -t nat | gr docker | gr -- -N | sed 's/-N/-X/'| while read -r l; do sudo iptables -t nat $l; done
226 }
227
228 # usage mkschroot [-] distro codename packages
229 # - means no piping in of sources.list
230 #
231 # note some useful post mkschroot i've used in the past
232 # tu /nocow/schroot/flidas/etc/sudoers <<EOF
233 # $USER ALL=(ALL) NOPASSWD: ALL
234 # Defaults env_keep += SUDOD
235 # Defaults always_set_home
236 # Defaults !umask
237 # EOF
238 # sd /nocow/schroot/flidas//etc/locale.gen <<'EOF'
239 # en_US.UTF-8 UTF-8
240 # EOF
241 # s schroot -c flidas locale-gen
242 # s schroot -c flidas update-locale LANG=en_US.UTF-8
243
244 mkschroot() {
245 local sources force repo n distro
246 force=false
247 while [[ $1 == -* ]]; do
248 case $1 in
249 -f) force=true; shift ;;
250 -s)
251 sources="$2"
252 if [[ ! -s $sources ]]; then
253 echo mkschroot: error: sources file $sources does not exist or is empty
254 return 1
255 fi
256 shift 2
257 ;;
258 esac
259 done
260 distro=$1
261 shift
262 case $distro in
263 trisquel)
264 repo=http://mirror.fsf.org/trisquel/
265 ;;
266 ubuntu)
267 repo=http://archive.ubuntu.com/ubuntu/
268 ;;
269 debian)
270 repo=http://deb.debian.org/debian/
271 ;;
272 esac
273 n=$1
274
275 shift
276 if ! $force && schroot -l | grep -xFq chroot:$n; then
277 echo "$0: $n schroot already installed, skipping"
278 return 0
279 fi
280 apps=($@)
281 d=/nocow/schroot/$n
282 sd /etc/schroot/chroot.d/$n.conf <<EOF
283 [$n]
284 description=$n
285 type=directory
286 directory=$d
287 profile=desktop
288 preserve-environment=true
289 users=$USER,user2
290 EOF
291 cd
292 if [[ ! -e $d/bin ]]; then
293 sudo mkdir -p $d
294 # resolvconf otherwise schroot fails with
295 # cp: not writing through dangling symlink '/var/run/schroot/mount/flidas-7a2362e0-81b3-4848-92c1-610203ef5976/etc/resolv.conf'
296 sudo debootstrap --exclude=resolvconf $n $d $repo
297 fi
298 if [[ $sources ]]; then
299 sudo install -m 644 $sources $d/etc/apt/sources.list
300 fi
301 sudo chroot $d apt-get update
302 sudo DEBIAN_FRONTEND=noninteractive chroot $d apt-get -y dist-upgrade --purge --auto-remove
303 sudo cp -P {,$d}/etc/localtime
304 if (( ${#apps[@]} )); then
305 sudo DEBIAN_FRONTEND=noninteractive schroot -c $n -- apt-get install --allow-unauthenticated -y ${apps[@]}
306 fi
307 }
308
309
310 # note: this is incomplete and untested.
311 # https://wiki.archlinux.org/index.php/Install_Arch_Linux_from_existing_Linux#Creating_a_chroot
312 mkarchchroot() {
313 local tarball mirror
314 mirror=https://mirrors.edge.kernel.org/archlinux/iso/latest/
315 tarball=$(curl -s $mirror | sed -nr 's/.*"(archlinux-bootstrap-.*-x86_64.tar.gz)".*/\1/p')
316 wget -O /tmp/arch.tar.gz https://mirrors.edge.kernel.org/archlinux/iso/latest/$tarball
317 s mkdir -p /nocow/schroot/arch
318 cd _/nocow/schroot/arch
319 s sed -i '/## United States/,/^$/s,^#,,' etc/pacman.d/mirrorlist
320 # error: could not determine cachedir mount point /var/cache/pacman/pkg
321 s sed -i /^CheckSpace/d etc/pacman.conf
322 chroot . /bin/bash -s <<'EOF'
323 pacman-key --init
324 pacman-key --populate archlinux
325 pacman -Syyu
326 EOF
327 # example of building an aur package:
328 # pacman -Sy base-devel wget
329 # useradd -m iank
330 # f=$target/etc/sudoers
331 # line='iank ALL=(ALL) NOPASSWD: ALL'
332 # if [[ ! -e $f ]] || ! grep -xF "$line" $f; then
333 # echo "$line" >> $f
334 # fi
335 # su iank
336 # wget https://aur.archlinux.org/cgit/aur.git/snapshot/anbox-image-gapps.tar.gz
337 # tar xzf anbox-image-gapps.tar.gz
338 # cd anbox-image-gapps
339 # makepkg -s
340 }
341
342
343 # clock back in to timetrack from last entry
344 tback() {
345 sqlite3 /p/.timetrap.db "update entries set end = NULL where id = (select max(id) from entries);"
346 }
347
348 # sshfs example:
349 # s sshfs bu@$host:/bu/home/md /bu/mnt -o reconnect,ServerAliveInterval=20,ServerAliveCountMax=30 -o allow_other
350
351 edelayoff() {
352 echo all >/var/spool/exim4/gw/.no-delay-eximids
353 if [[ $EUID == 0 ]]; then
354 chown iank:iank /var/spool/exim4/gw/.no-delay-eximids
355 fi
356 }
357 edelayon() {
358 echo >/var/spool/exim4/gw/.no-delay-eximids
359 if [[ $EUID == 0 ]]; then
360 chown iank:iank /var/spool/exim4/gw/.no-delay-eximids
361 fi
362 }
363
364 eqgo() {
365 local -a array tmpstr delayon
366 delayon=true
367 if grep -qFx all /var/spool/exim4/gw/.no-delay-eximids; then
368 delayon=false
369 fi
370 if $delayon; then
371 echo all >/var/spool/exim4/gw/.no-delay-eximids
372 fi
373 tmpstr=$(exiqgrep -i -r.\*)
374 mapfile -t array <<<"$tmpstr"
375 enn -M "${array[@]}"
376 if $delayon; then
377 echo >/var/spool/exim4/gw/.no-delay-eximids
378 fi
379 }
380 eqgo1() {
381 local eid
382 eid="$(exipick -i -r.\*|h1)"
383 sed -n "/^all$/p;\$a $eid" /var/spool/exim4/gw/.no-delay-eximids
384 enn -M "$eid"
385 }
386 # exim -M (in namespace and without delay)
387 ennm() {
388 local eid
389 for eid; do
390 printf "%s\n" "$eid" >>/var/spool/exim4/gw/.no-delay-eximids
391 done
392 enn -M "$@"
393 }
394
395
396 gnupload(){
397 /a/f/gnulib/build-aux/gnupload "$@"
398 }
399
400 abrowserrmcompat() {
401 local f
402 ngset
403 f=(/p/c/firefox*/compatibility.ini)
404 if (( ${#f[@]} )); then
405 rm ${f[@]}
406 fi
407 ngreset
408 }
409
410 checkre() {
411 s checkrestart -b /a/bin/ds/checkrestart-blacklist -pv
412 }
413
414 cp-blocked-domains-to-brains() {
415 cp /a/f/ans/roles/exim/files/mx/simple/etc/exim4/bad-sender_domains /a/f/brains/sysadmin/kb/blocked_email_domains.mdwn
416 }
417 cp-blocked-domains-to-ansible() {
418 cp /a/f/brains/sysadmin/kb/blocked_email_domains.mdwn /a/f/ans/roles/exim/files/mx/simple/etc/exim4/bad-sender_domains
419 }
420
421
422 daycat() {
423 ngset
424 hrcat /m/md/daylert/{cur,new}/*
425 ngreset
426 }
427 dayclear() {
428 ngset
429 rm -f /m/md/daylert/{cur,new}/*
430 }
431
432
433 acat() {
434 ngset
435 hrcat /m/md/alerts/{cur,new}/*
436 ngreset
437 hr; echo bk; hr
438 ssh bk.b8.nz "shopt -s nullglob; hrcat /m/md/INBOX/new/* /m/md/INBOX/cur/*"
439 }
440 aclear() {
441 ngset
442 rm -f /m/md/alerts/{cur,new}/*
443 ngreset
444 ssh bk.b8.nz "shopt -s nullglob; rm -f /m/md/INBOX/new/* /m/md/INBOX/cur/*"
445 system-status _
446 }
447
448 alerts() {
449 find /var/local/cron-errors /home/iank/cron-errors /sysd-mail-once-state -type f
450 }
451 ralerts() { # remote alerts
452 local ret shell
453 # this list is duplicated in check-remote-mailqs
454 for h in bk je li frodo x3wg kdwg sywg; do
455 echo $h:
456 shell="ssh $h"
457 if [[ $HOSTNAME == "${h%wg}" ]]; then
458 shell=
459 fi
460 ret=0
461 $shell find /var/local/cron-errors /home/iank/cron-errors /sysd-mail-once-state -type f || ret=$?
462 if (( ret )); then
463 echo ret:$ret
464 fi
465 done
466 }
467
468 ap() {
469 # pushd in case current directory has an ansible.cfg file
470 pushd /a/xans >/dev/null
471 ansible-playbook -v -i ${1:- $(hostname -f)}, site.yml
472 popd >/dev/null
473 }
474 aw() {
475 pushd /a/work/ans >/dev/null
476 time ansible-playbook -i inventory adhoc.yml "$@"
477 popd >/dev/null
478 }
479 ad() {
480 pushd /a/bin/distro-setup/a >/dev/null
481 ansible-playbook site.yml "$@"
482 popd >/dev/null
483 }
484
485 astudio() {
486 # googling android emulator libGL error: failed to load driver: r600
487 # lead to http://stackoverflow.com/a/36625175/14456
488 export ANDROID_EMULATOR_USE_SYSTEM_LIBS=1
489 /a/opt/android-studio/bin/studio.sh "$@" & r
490 }
491
492 # Convert brains file path to url and vice versa
493 # usage: brains [URL_OR_PATH]
494 brains() {
495 _iki-convert brains.fsf.org/wiki "$@"
496 }
497 glue() {
498 _iki-convert gluestick.office.fsf.org "$@"
499 }
500
501 # usage: see above
502 _iki-convert() {
503 local url url_prefix path input repo_dir dir url_dir url name
504 url_prefix="$1"
505 name="${url_prefix%%.*}"
506 repo_dir="/f/$name"
507 shift
508 if [[ $1 ]]; then
509 input="$*"
510 else
511 read -r -p "enter path or url"$'\n' input
512 fi
513 case $input in
514 http*)
515 path="$repo_dir/${input##http*://"$url_prefix"/}"
516 # for files like x.jpg, we dont need to convert the extension.
517 if [[ $path == */ ]]; then
518 path=${path%/}.mdwn
519 # brains adds trailing slash, but without trailing is still
520 # valid. We can't be totally sure whether to add mdwn, but we
521 # can guess based on the existence of the file. We can't be sure
522 # because it could be a file like x.jpg, that we just don't have
523 # in our local repo.
524 elif [[ ! -f $path && -e $path.mdwn ]]; then
525 path=${path}.mdwn
526 fi
527 j printf "%s\n" "$path"
528 ;;
529 *)
530 path=$(fp "$input")
531 url_dir=$(echo "$path" | sed -r "s,^(/a)?$repo_dir/,,")
532 url="https://$url_prefix/$url_dir"
533 if [[ $url == *.mdwn ]]; then
534 url="${url%.mdwn}/"
535 fi
536 j echo "$url"
537 ;;
538 esac
539 }
540
541
542 # Generate beet smartplaylists for navidrome.
543 # for going in the reverse direction, run
544 # /b/ds/navidrome-playlist-export
545 beetsmartplaylists() {
546 install -m 0700 -d /tmp/ianbeetstmp
547 beet splupdate
548 # kill off any playlists we deleted. they will still need manual
549 # killing from a navidrome client.
550 rm -rf /i/converted/beetsmartplaylists
551 mkdir -p /i/converted/beetsmartplaylists
552 for f in /tmp/ianbeetstmp/*; do
553 sed 's,^/i/m,/i/converted,;s,\.flac$,.mp3,' "$f" >"/i/converted/beetsmartplaylists/${f##*/}"
554 rm "$f"
555 done
556 rmdir /tmp/ianbeetstmp
557 }
558
559 # internal function for beetrating, in case we need to ssh
560 beetrating-stdin() {
561 local tmp rating path cpath sqlpath userid
562 # plucked this from the db. im the only user.
563 userid=23cc2eb9-e35e-4811-a0f0-d5f0dd6eb634
564 while read -r rating path; do
565 cpath="/i/converted${path#/i/m}" # converted path
566 case $cpath in
567 *.flac)
568 cpath="${cpath%.*}.mp3"
569 ;;
570 esac
571 if [[ ! -e $cpath ]]; then
572 echo "beetraing: error: this should not happen, path does not exist: $cpath"
573 return 1
574 fi
575 sqlpath="${cpath//\'/\'\'}"
576 old_rating=$(sqlite3 /i/navidrome/navidrome.db "select rating from annotation inner join media_file on item_id = id where path = '$sqlpath' and item_type = 'media_file';")
577 if [[ $old_rating ]]; then
578 if [[ $old_rating != "$rating" ]]; then
579 echo "setting rating $old_rating -> $rating $cpath"
580 # https://stackoverflow.com/a/50317320
581 # we got a timeout error once. arbitrarily chose 15 seconds.
582 sqlite3 /i/navidrome/navidrome.db ".timeout 15000" "
583 update annotation set rating = $rating
584 where item_id in (
585 select media_file.id from annotation inner join media_file on annotation.item_id = media_file.id
586 where media_file.path = '$sqlpath' and annotation.item_type = 'media_file' );"
587 fi
588 else
589 echo "setting rating $rating $cpath"
590 # /a/opt/navidrome/persistence/sql_annotations.go v0.48.0
591 # https://www.sqlite.org/lang_insert.html
592 sqlite3 /i/navidrome/navidrome.db ".timeout 15000" "insert into annotation select '$(uuidgen)', '$userid', id, 'media_file', 0, NULL, $rating, 0, NULL from media_file where path = '$sqlpath';"
593 fi
594 done
595 }
596
597 # Export beets ratings into navidrome
598 beetrating() {
599 local ssh_prefix
600 if [[ $HOSTNAME != kd ]]; then
601 ssh_prefix="ssh b8.nz"
602 fi
603 # shellcheck disable=SC2016 # obvious reason
604 beet ls -f '$rating $path' $nav_convert_query | $ssh_prefix beetrating-stdin
605 }
606
607 # Do transcoding and hardlinking of audio files for navidrome.
608 beetconvert() {
609 local tmpf
610 tmpf="$(mktemp)"
611 # a bunch of effort to ignore output we dont care about...
612 sed 's/^format_item:.*/format_item: ignore_this/' ~/.config/beets/config.yaml >$tmpf
613 beet -c $tmpf convert -y $nav_convert_query > >(grep -vFx 'ignore_this' ||:) 2> >(grep -v '^convert: Skipping' ||:)
614 rm "$tmpf"
615 }
616 # This deletes files in the converted directory which should no longer
617 # be there due to a rename of the unconverted file.
618 beetconvert-rm-extras() {
619 local l tmpf
620 local -A paths
621 tmpf="$(mktemp)"
622 # shellcheck disable=SC2016 # obvious reason
623 beet ls -f '$path' $nav_convert_query >"$tmpf"
624 ## begin removal of files that are leftover from previous conversion,
625 # eg, previously rated > 1, now rated 1.
626 while read -r l; do
627 convertedpath="/i/converted${l#/i/m}"
628 case $convertedpath in
629 *.flac) convertedpath="${convertedpath%.flac}.mp3" ;;
630 esac
631 paths[$convertedpath]=t
632 done <"$tmpf"
633
634 find /i/converted -path /i/converted/beetsmartplaylists -prune -o \( -type f -print \) -name '*.mp3' -o -name '*.m4a' >"$tmpf"
635 while read -r l; do
636 if [[ ! ${paths[$l]} ]]; then
637 rm -v "$l"
638 fi
639 # note: the pruning is duplicative of filtering on name, but whatever.
640 done <"$tmpf"
641 rm "$tmpf"
642 }
643
644 beets-gen-playlists() {
645 local i str
646 local -a query_array query_str
647 for i in "${!bpla[@]}"; do
648 query_str=()
649 eval "query_array=(${bpla[$i]})"
650 for str in "${query_array[@]}"; do
651 query_str+=("\"$str\"")
652 done
653 cat <<EOF
654 - name: $i.m3u
655 query: '${query_str[@]}'
656 EOF
657 done
658 }
659
660 # beet playlist. use beetag with a playlist name
661 bpl() {
662 local playlist playlist_regex
663 case $1 in
664 -h|--help)
665 for playlist in "${!bpla[@]}"; do
666 printf "%s\n" "$playlist"
667 done
668 return 0
669 ;;
670 esac
671
672 playlist="${*: -1}"
673 playlist_regex='[a-z0-9_]'
674 if [[ ! $playlist =~ $playlist_regex ]]; then
675 echo "bpl: error unexpected chars in playlist: $playlist"
676 return 1
677 fi
678 # all but last arg as options
679 eval beetag -r "${*:1:$# - 1}" "${bpla[$playlist]}"
680 }
681 complete -W "${!bpla[*]}" bpl
682
683
684 # beet modify quietly
685 beetmq() {
686 local tmpf
687 tmpf="$(mktemp)"
688 # a bunch of effort to ignore output we dont care about...
689 sed 's/^format_item:.*/format_item: ignore_this/' ~/.config/beets/config.yaml >$tmpf
690 beet -c $tmpf modify -y "$@" > >(grep -vFx -e 'ignore_this' -e 'Modifying 1 items.' ||:)
691 rm "$tmpf"
692 beetag-nostatus 1
693 }
694
695 kill-bg-quiet() {
696 # https://stackoverflow.com/a/5722874
697 kill %% 2>/dev/null ||:; wait %% 2>/dev/null ||:
698 }
699
700 # debug variables
701 dv() {
702 for arg; do
703 printf "%s=%s " "$arg" "${!arg}"
704 done
705 echo
706 }
707
708 # Must be called from beetag for variables to be setup
709 beetag-help() {
710 local -i i j col_total row col button_total row_total remainder_cols remainder_term
711 col_total=4
712 button_total=${#button_map[@]}
713 row_total=$(( button_total / col_total ))
714 remainder_cols=$(( button_total % col_total ))
715 # for debugging
716 #dv button_total row_total remainder_cols
717 beetag-nostatus
718 # - 3 is just a constant that helps things work in practice.
719 if [[ $LINES ]] && (( LINES - 3 < scrolled )); then
720 hr
721 for (( i=0; i<button_total; i++)); do
722 row=$(( i / col_total ))
723 col=$(( i % col_total ))
724 remainder_term=$remainder_cols
725 if (( col < remainder_term )); then
726 remainder_term=$col
727 fi
728 j=$(( col * row_total + row + remainder_term ))
729 # avoid double newline when we have exactly row * col buttons
730 if (( i == button_total - 1 )); then
731 printf "%s %s" ${buttons[j]} ${button_map[j]}
732 elif (( i % col_total == col_total -1 )); then
733 printf "%s %s\n" ${buttons[j]} ${button_map[j]}
734 else
735 printf "%s %-15s" ${buttons[j]} ${button_map[j]}
736 fi
737 done
738 cat <<'EOF'
739
740
741 y other genres z fg player ' = toggle play 1-5 rate ] repeat1
742 ; previous _ = delete up/down skip mpv vol,pause,seek
743 EOF
744 hr
745 scrolled=10
746 fi
747 }
748
749 # Must be called from beetag for variables to be setup
750 beetag-nostatus() {
751 if (( $# )); then
752 scrolled=$(( scrolled + $1 ))
753 fi
754 if $erasable_line; then
755 # https://stackoverflow.com/a/71286261
756 # erase line / delete line in terminal
757 printf '\033[1A\033[K'
758 fi
759 erasable_line=false
760 }
761 # meant to be called from beetag
762 beetag-status() {
763 if $erasable_line; then
764 # https://stackoverflow.com/a/71286261
765 printf '\033[1A\033[K'
766 fi
767 erasable_line=true
768 }
769
770 # meant to be called from beetag
771 mpvrpc() {
772 if jobs -p | grep -q . &>/dev/null; then
773 printf "%s\n" "$*" | socat - /tmp/mpvsock >/dev/null ||:
774 fi
775 }
776 # meant to be called from beetag
777 # o for get output
778 mpvrpco() {
779 # note: testing for background jobs will output nothing if we are in a pipeline
780 printf "%s\n" "$*" | socat - /tmp/mpvsock ||:
781 }
782
783 # meant to be called from beetag
784 mpvrpc-percent-pos() {
785 mpvrpco '{ "command": ["get_property", "percent-pos"] }' | jq .data | sed 's/\..*/%/' 2>/dev/null ||:
786 }
787
788 # run if not running.
789 #
790 # Note: this does not work with shell scripts as they are normally
791 # invoked, because the ps output has the interpreter at the start.
792 # A workaround is to invoke the command in that format, or we could
793 # do various other workarounds.
794 #
795 # background, this relies on how ps converts newlines in arguments to spaces, and
796 # assumes we won't be searching for a command with spaces in its arguments
797 rinr() {
798 # shellcheck disable=SC2009 # pgrep has no fixed string option, plus see above.
799 if ps h -o args -C "${1##*/}" | grep -Fxqv "$*" &>/dev/null || [[ $? == 141 ]]; then
800 "$@"
801 fi
802 }
803 # variation of above: run or wait if running
804 rowir() {
805 local pid
806 pid=$(ps h -o 'pid,args' -C "${1##*/}" | sed -r 's/^[[:space:]]*([0-9]+)[[:space:]](.*)/\1\n\2/' | grep -B1 -Fx "$*" | head -n1 ||: )
807 if [[ $pid ]]; then
808 # https://unix.stackexchange.com/questions/427115/listen-for-exit-of-process-given-pid
809 tail --pid="$pid" -f /dev/null
810 else
811 "$@"
812 fi
813 }
814
815 mpvrpc-loadfile() {
816 local path nextpath cachedir finalpath nextpath count
817 cachedir=$HOME/.iank-music-cache
818 path="$1"
819 nextpath="$2"
820
821 # note: logic duplicated in beetpull
822 local remote_p=true
823 if [[ $HOSTNAME == kd ]]; then
824 remote_p=false
825 fi
826
827 if $remote_p; then
828 finalpath="$cachedir${path#/i/m}"
829 rowir rsync --partial -a --inplace --mkpath "b8.nz:$path" "$finalpath"
830 finalnextpath="$cachedir${nextpath#/i/m}"
831 count=$(pgrep -a -f "^rsync --partial -a --inplace --mkpath $cachedir" || [[ $? == 1 ]] )
832 # allow us to start 2 rsyncs in the background
833 if [[ $count == [01] ]]; then
834 rinr rsync --partial -a --inplace --mkpath "b8.nz:$nextpath" "$finalnextpath" &
835 fi
836 else
837 finalpath="$path"
838 fi
839 mpvrpc '{ "command": ["loadfile", "'"$finalpath"'"] }'
840 }
841
842 # tag with beets.
843 # usage: beetag [-r] [-s] QUERY
844 # it lists the query, reads an input char for tagging one by one.
845 #
846 # note, you may want to change the play command for doing rapid taging
847 # by immediately jumping forward into the song. this is set in the beets
848 # config yaml.
849 #
850 # (available buttons: ` \ ) ] [ and non-printing chars, see
851 # https://stackoverflow.com/questions/10679188/casing-arrow-keys-in-bash
852 #
853 #
854 # note: after foregrounding the player, must quit it to get back. can't ctrl-c.
855 #
856 # keys I dont need help to remember:
857 # 1-5 rate
858 # q quit
859 # ret next
860 #
861 # todo: enter should also unpause
862 beetag() {
863 local last_genre_i fstring tag id char new_item char_i genre tag remove doplay i j random path
864 local do_rare_genres read_wait line lsout tmp ls_line skip_lookback
865 local escape_char escaped_input expected_input skip_input_regex right_pad erasable_line seek_sec
866 local pl_state_path pl_state_dir pl_state_file tmpstr
867 local new_random pl_seed_path seed_num seed_file fmt first_play repeat1
868 local -a buttons button_map ids tags tmp_tags initial_ls ls_lines paths
869 local -A button_i
870 local -i i j volume scrolled id_count line_int skip_start pre_j_count head_count skip_lookback
871 local -i overflow_lines overflow
872
873 first_play=true
874 erasable_line=false
875 escape_char=$(printf "\u1b")
876 scrolled=999 # more than any $LINES
877 ### begin arg processing ###
878 random=false
879 repeat1=false
880 new_random=false
881 case $1 in
882 -r)
883 random=true
884 shift
885 ;;
886 -s)
887 random=false
888 shift
889 ;;
890 -x)
891 new_random=true
892 shift
893 ;;
894 esac
895 if (( ! $# )); then
896 echo beetag: error expected a query arg >&2
897 return 1
898 fi
899 ### end arg processing ###
900
901 # note: I used to do beetpull here, but mpv + ssfs on slowish
902 # connection leads to bad/buggy result.
903
904 do_rare_genres=false
905 volume=70
906 read_wait=2
907 doplay=true
908
909 last_genre_i=$(( ${#common_genres[@]} - 1 ))
910 buttons=( {a..p} {r..w} {6..8} , . / - "=")
911 button_map=(${common_genres[@]} ${pl_tags[@]})
912 fstring=
913 for tag in "${pl_tags[@]}"; do
914 fstring+="%ifdef{$tag,$tag }"
915 done
916
917 for (( i=0; i<${#buttons[@]}; i++ )); do
918 button_i[${buttons[i]}]=$i
919 done
920
921 # note: this structure of files is rather haphazard.
922 seed_num=1 # later we might want a few
923 seed_file=seed$seed_num
924 if $random; then
925 pl_state_file=$seed_num
926 else
927 pl_state_file=sorted
928 fi
929 pl_state_dir=/b/data/pl-state
930 if [[ $playlist ]]; then
931 pl_state_dir=$pl_state_dir/$playlist
932 else
933 pl_state_dir=$pl_state_dir/nopl
934 fi
935 pl_state_path=$pl_state_dir/$pl_state_file
936 pl_seed_path=$pl_state_dir/$seed_file
937
938
939 if $new_random || [[ ! -r $pl_seed_path ]]; then
940 mkdir -p $pl_state_dir
941 { base64 < /dev/urandom | head -c 200 ||:; echo; } > $pl_seed_path
942 fi
943
944 # PijokVipiotOzeph is just a random string for a delimiter
945 # shellcheck disable=SC2016 # false positive
946 fmt='%ifdef{rating,$rating }'"$fstring"'$genre | $title - $artist - $album $length $id PijokVipiotOzeph $path'
947 # shellcheck disable=SC2016 # obvious reason
948 tmpstr=$(beet ls -f "$fmt" "$@" | { if $random; then sort -R --random-source=$pl_seed_path; else cat; fi; } )
949 mapfile -t initial_ls <<<"$tmpstr"
950 if [[ ! ${initial_ls[0]} ]]; then
951 echo "beetag: error: no result from beet ls $*"
952 return 1
953 fi
954 id_count=${#initial_ls[@]}
955 for line in "${initial_ls[@]}"; do
956 path="${line#*PijokVipiotOzeph }"
957 # https://github.com/koalaman/shellcheck/issues/2171
958 # shellcheck disable=SC2190 # bug in shellcheck, looking at paths from an earlier function
959 paths+=("$path")
960 line_no_path="${line% PijokVipiotOzeph*}"
961 id="${line_no_path##* }"
962 ids+=("$id")
963 right_pad="${line_no_path%% |*}"
964 ls_line="$(printf %-11s "$right_pad")${line_no_path#"$right_pad"}"
965 ls_lines+=("$ls_line")
966 i=$(( i+1 ))
967 done
968
969
970
971
972 j=0
973 if [[ $playlist ]]; then
974 if [[ -r $pl_state_path ]]; then
975 j=$(cat $pl_state_path)
976 fi
977 fi
978
979 # i only care to see a smallish portion of the list when starting.
980 head_count=$(( LINES - 20 ))
981 head_start=$(( j - head_count / 2 ))
982 if (( head_start < 0 )); then
983 head_start=0
984 fi
985 for (( i=head_start; i < head_count && i < id_count; i++ )); do
986 ls_line="${ls_lines[$i]}"
987 if (( i == j )); then
988 echo "* $ls_line"
989 else
990 echo "$ls_line"
991 fi
992 done
993 if $doplay; then
994 #{ mpv --profile=a --volume=$volume --idle 2>&1 & } 2>/dev/null
995 mpv --profile=a --volume=$volume --idle &
996 # if we dont sleep, can expect an error like this:
997 # socat[1103381] E connect(5, AF=1 "/tmp/mpvsock", 14): Connection refused
998 sleep .1
999 fi
1000
1001 while true; do
1002 id=${ids[j]}
1003 path="${paths[$j]}"
1004 lsout="${ls_lines[j]}"
1005 tags=( ${lsout%%,*} )
1006 beetag-help
1007 printf "██ %s\n" "$lsout"
1008 beetag-nostatus 1
1009 if $doplay; then
1010 # https://stackoverflow.com/a/7687716
1011 # note: duplicated down below
1012 #
1013 # notes on old method of invoking mpv each time:
1014 # https://superuser.com/questions/305933/preventing-bash-from-displaying-done-when-a-background-command-finishes-execut
1015 # we can't disown or run in a subshell or set +m because all that
1016 # disabled job control from working properly in ways we want.
1017 # todo: figure out some kind of answer to this. I think the solution
1018 # is that we are waiting in 2 second intervals and checking if the
1019 # background job exists. Instead, we should make mpv just idle
1020 # when it is done with a song and then send it a command to play a new track.
1021 #{ mpv --profile=a --volume=$volume "$path" 2>&1 & } 2>/dev/null
1022 # old
1023 #{ beet play "--args=--volume=$volume" "id:$id" 2>&1 & } 2>/dev/null
1024
1025 # on slow systems, we may need to wait like .3 seconds before mpv
1026 # is ready. so impatiently check until it is ready
1027 if $first_play; then
1028 first_play=false
1029 for (( i=0; i<20; i++ )); do
1030 if [[ $(mpvrpco '{ "command": ["get_property", "idle-active"] }' 2>/dev/null | jq .data) == true ]]; then
1031 mpvrpc-loadfile "$path" 2>/dev/null
1032 break
1033 fi
1034 sleep .1
1035 done
1036 else
1037 mpvrpc-loadfile "$path"
1038 fi
1039 erasable_line=false
1040 fi
1041 while true; do
1042 char=
1043 if $doplay; then
1044 ret=0
1045 read -rsN1 -t $read_wait char || ret=$?
1046 read_wait=2
1047 # Automatically skip to the next song if this one ends, unless
1048 # we turn off the autoplay.
1049 if (( ret == 142 )) || [[ ! $char ]]; then
1050 if jobs -p | grep -q . &>/dev/null && \
1051 [[ $(mpvrpco '{ "command": ["get_property", "idle-active"] }' | jq .data) == false ]]; then
1052 continue
1053 else
1054 break
1055 fi
1056 fi
1057 else
1058 read -rsN1 char
1059 fi
1060 beetag-help
1061 if [[ $char == $'\n' ]]; then
1062 break
1063 fi
1064 case $char in
1065 ";")
1066 j=$(( j - 2 ))
1067 break
1068 ;;
1069 "'")
1070 if $doplay; then
1071 echo "play toggled off"
1072 doplay=false
1073 else
1074 doplay=true
1075 mpvrpc-loadfile "$path"
1076 erasable_line=false
1077 fi
1078 beetag-nostatus 1
1079 continue
1080 ;;
1081 _)
1082 m beet rm --delete --force "id:$id"
1083 beetag-nostatus 4 # guessing. dont want to test atm
1084 break
1085 ;;
1086 [1-5])
1087 beetmq "id:$id" rating=$char
1088 continue
1089 ;;
1090 9)
1091 volume=$(( volume - 5 ))
1092 if (( volume < 0 )); then
1093 volume=0
1094 fi
1095 ;;&
1096 0)
1097 volume+=5
1098 if (( volume > 130 )); then
1099 volume=130
1100 fi
1101 ;;&
1102 0|9)
1103 mpvrpc '{ "command": ["set_property", "volume", '$volume'] }'
1104 beetag-status
1105 echo volume=$volume
1106 continue
1107 ;;
1108 ']')
1109 if $repeat1; then
1110 repeat1=false
1111 else
1112 repeat1=true
1113 fi
1114 echo repeat1=$repeat1
1115 continue
1116 ;;
1117 q)
1118 kill-bg-quiet
1119 return
1120 ;;
1121 y)
1122 if $do_rare_genres; then
1123 do_rare_genres=false
1124 button_map=(${common_genres[@]} ${pl_tags[@]})
1125 last_genre_i=$(( ${#rare_genres[@]} - 1 ))
1126 else
1127 do_rare_genres=true
1128 button_map=(${rare_genres[@]} ${pl_tags[@]})
1129 last_genre_i=$(( ${#rare_genres[@]} - 1 ))
1130 fi
1131 local -A button_i
1132 for (( i=0; i<${#buttons[@]}; i++ )); do
1133 button_i[${buttons[i]}]=$i
1134 done
1135 for (( i=0; i<${#button_map[@]}; i++ )); do
1136 echo ${buttons[i]} ${button_map[i]}
1137 done
1138 continue
1139 ;;
1140 z)
1141 beetag-nostatus 3
1142 # if we ctrl-z, it will put the whole function into sleep. so
1143 # basically, we can't return from a foregrounded mpv like we
1144 # would like to without some strange mechanism I can't think
1145 # of. So, instead, detect ctrl-c and wait a while for prompt
1146 # input. One idea would be to use a music player like mpd where
1147 # we can send it messages.
1148 if ! fg; then
1149 read_wait=10
1150 fi
1151 continue
1152 ;;
1153
1154 #
1155 " ")
1156 # output time if we aren't already paused
1157 if [[ $(mpvrpco '{ "command": ["get_property", "pause"] }' | jq .data) == false ]]; then
1158 # minutes/seconds
1159 #date -d @"$(mpvrpco '{ "command": ["get_property", "playback-time"] }' | jq .data)" +%M:%S ||:
1160 beetag-status
1161 mpvrpc-percent-pos
1162 fi
1163 # originally found this solution, which worked fine.
1164 #kill -STOP %% &>/dev/null
1165 #
1166 mpvrpc '{ "command": ["cycle", "pause"] }'
1167 continue
1168 ;;
1169 "$escape_char")
1170 expected_input=true
1171 read -rsn2 escaped_input
1172 skip_input_regex="^[0-9]+$"
1173 case $escaped_input in
1174 # up char: show all the songs, use less
1175 '[A')
1176 skip_start=0
1177 skip_lookback=5
1178 if (( j - skip_lookback > skip_start )); then
1179 skip_start=$(( j - skip_lookback ))
1180 fi
1181 beetag-nostatus $(( id_count - skip_start - 1 ))
1182 {
1183 line_int=0
1184 for (( i=skip_start; i < id_count; i++ )); do
1185 if (( i == j )); then
1186 echo " * ${ls_lines[i]}"
1187 continue
1188 fi
1189 echo "$line_int | ${ls_lines[i]}"
1190 line_int+=1
1191 done
1192 } | less -F
1193 ;;
1194 # down char
1195 '[B')
1196 # skip forward, but show the last few songs anyways.
1197 skip_start=0
1198 skip_lookback=3
1199 if (( j - skip_lookback > skip_start )); then
1200 skip_start=$(( j - skip_lookback ))
1201 fi
1202 beetag-nostatus $(( id_count - skip_start - 1 ))
1203
1204 line_int=0
1205 overflow_lines=$LINES
1206 for (( i=skip_start; i < overflow_lines - 1 && i < id_count; i++ )); do
1207 ls_line="${ls_lines[i]}"
1208 overflow=$(( ${#ls_line} / ( COLUMNS - 1 ) ))
1209 overflow_lines=$(( overflow_lines - overflow ))
1210 if (( i == j )); then
1211 echo " * $ls_line"
1212 continue
1213 fi
1214 echo "$line_int | $ls_line"
1215 line_int+=1
1216 done
1217 ;;
1218 # left key
1219 '[D')
1220 seek_sec=-8
1221 ;;&
1222 # right key
1223 '[C')
1224 seek_sec=8
1225 ;;&
1226 '[C'|'[D')
1227 beetag-status
1228 mpvrpc-percent-pos
1229 erasable_line=true
1230 mpvrpc '{ "command": ["seek", "'$seek_sec'"] }'
1231 continue
1232 ;;
1233 *)
1234 expected_input=false
1235 ;;
1236 esac
1237 if $expected_input; then
1238 read -r skip_input
1239 case $skip_input in
1240 q)
1241 kill-bg-quiet
1242 return
1243 ;;
1244 esac
1245 if [[ $skip_input =~ $skip_input_regex ]]; then
1246 pre_j_count=$(( j - skip_start ))
1247 j=$(( j + skip_input - pre_j_count ))
1248 if (( skip_input < pre_j_count )); then
1249 j=$(( j - 1 ))
1250 fi
1251 fi
1252 break
1253 fi
1254 ;;
1255 esac
1256 char_i=${button_i[$char]}
1257 new_item=${button_map[$char_i]}
1258 if [[ ! $char_i || ! $new_item ]]; then
1259 echo "error: no mapping of input: $char found, try again"
1260 continue
1261 fi
1262 if (( char_i <= last_genre_i )); then
1263 m beetmq "id:$id" genre=$new_item
1264 else
1265 remove=false
1266 tmp_tags=()
1267 for tag in ${tags[@]}; do
1268 if [[ $new_item == "$tag" ]]; then
1269 remove=true
1270 else
1271 tmp_tags+=("$tag")
1272 fi
1273 done
1274 if $remove; then
1275 tags=("${tags[@]}")
1276 m beetmq "id:$id" "$new_item!"
1277 else
1278 tags+=("$new_item")
1279 m beetmq "id:$id" $new_item=t
1280 fi
1281 fi
1282 done
1283 if ! $repeat1; then
1284 if (( j < id_count - 1 )); then
1285 j+=1
1286 else
1287 j=0
1288 fi
1289 fi
1290 if [[ $playlist ]]; then
1291 echo $j >$pl_state_path
1292 fi
1293 done
1294 }
1295
1296 # usage: FILE|ALBUM_DIR [GENRE]
1297 beetadd() {
1298 local import_path genre_arg single_track_arg
1299 import_path="$1"
1300 if [[ ! -e $import_path ]]; then
1301 echo "beetadd error: path does not exist"
1302 fi
1303 if [[ $2 ]]; then
1304 genre_arg="--set genre=$2"
1305 fi
1306 if [[ -f $import_path ]]; then
1307 single_track_arg=-s
1308 fi
1309 beet import --set totag=t $single_track_arg $genre_arg "$import_path"
1310 beetag totag:t
1311 beet modify -y totag:t "totag!"
1312 }
1313
1314 # update navidrome music data after doing beets tagging
1315 beet2nav() {
1316 m beetpull
1317 m beetconvert
1318 m beetrating
1319 # this function would naturally just be part of beetconvert,
1320 # but we want beetrating to happen sooner so that our ssh auth dialog
1321 # happens earlier. Currently 17 seconds for that.
1322 m beetconvert-rm-extras
1323 m beetsmartplaylists
1324 }
1325
1326 # pull in beets library locally
1327 beetpull() {
1328 local sshfs_host sshfs_cmd
1329 sshfs_host=b8.nz
1330 if [[ $HOSTNAME == kd ]]; then
1331 return 0
1332 fi
1333 if [[ ! -e /i ]]; then
1334 s mkdir /i
1335 s chown iank:iank /i
1336 fi
1337 sshfs_cmd="sshfs -o ServerAliveInterval=15,reconnect $sshfs_host:/i /i"
1338 if ! pgrep -f "^$sshfs_cmd$" >/dev/null; then
1339 m $sshfs_cmd
1340 fi
1341 }
1342
1343 # remove all playlists in navidrome, for when I make big
1344 # playlist name changes and just want to scrap everything.
1345 nav-rm-plists() {
1346 local tmpf id
1347 tmpf=$(mktemp)
1348 if [[ $HOSTNAME != kd ]]; then
1349 echo "error: run on kd"
1350 return 1
1351 fi
1352 sqlite3 /i/navidrome/navidrome.db "select id from playlist" >$tmpf
1353 while read -r id; do
1354
1355 curl --http1.1 --user "iank:$navidrome_pw" "https://b8.nz/rest/deletePlaylist.view?u=iank&s=sb219dvv7egnoe4i47k75cli0m&t=1c8f5575cd0fdf03deb971187c9c88b1&v=1.2.0&c=DSub&id=$id"
1356 done <$tmpf
1357 rm $tmpf
1358 }
1359
1360 # escape regex.
1361 #
1362 # This is not perfect but generally good enough. It escapes all
1363 # metachars listed man 3 pcrepattern.
1364 er() {
1365 sed 's/[]\\^$.[|()?*+{}]/[&]/g; s/\^/\\^/g' <<<"$*"
1366 }
1367
1368 # usage beegenre QUERY
1369 #
1370 # beet set genre for QUERY based on existing artist most used genre on
1371 #
1372 # inverse of query for each artist found in QUERY. If query starts with
1373 # "artist:" it is used as the artist instead of each artist in QUERY.
1374 #
1375 beegenre() {
1376 local count artist artregex genre singleartist tmpf tmpf2
1377 local -a artists genres
1378 singleartist=false
1379 case $1 in
1380 artist:*)
1381 singleartist=true
1382 artist="$1"
1383 shift
1384 ;;
1385 esac
1386 tmpf=$(mktemp)
1387 tmpf2=$(mktemp)
1388 if $singleartist; then
1389 # shellcheck disable=SC2016 # obvious reason
1390 beet ls -f '$genre' "$artist" "${@/#/^}" | sort | uniq -c | sort -n | tail -n1 >$tmpf
1391 read -r count genre <$tmpf ||:
1392 beet modify "$artist" "$@" genre=$genre
1393 else
1394 # shellcheck disable=SC2016 # obvious reason
1395 beet ls -f '$artist' "$@" | sort -u >$tmpf
1396 while read -r artist; do
1397 artregex=$(er "$artist")
1398 # shellcheck disable=SC2016 # obvious reason
1399 beet ls -f '$genre' "artist::^$artregex$" "${@/#/^}" | sort | uniq -c | sort -n | tail -n1 >$tmpf2
1400 read -r count genre <$tmpf2 || continue
1401 if [[ $count ]]; then
1402 artists+=("$artregex")
1403 genres+=("$genre")
1404 echo "beet modify -y $* \"artist::^$artist$\" genre=$genre # $count"
1405 fi
1406 done <$tmpf
1407 read -r -N 1 -s -p "Y/n " char
1408 case $char in
1409 [Yy$'\n'])
1410 for (( i=0; i<${#artists[@]}; i++ )); do
1411 beet modify -y "$@" "artist::^${artists[i]}$" genre=${genre[i]}
1412 done
1413 ;;
1414 esac
1415 fi
1416 rm $tmpf
1417 }
1418
1419 # prettify the date
1420 btrbk-date() {
1421 local indate
1422 indate="$1"
1423 shift
1424 date +%F_%T%:::z -d "$(sed -r 's/(.{4})(..)(.{5})(..)(.*)/\1-\2-\3:\4:\5/' <<<"$indate")" "$@"
1425 }
1426 btrbk-undate() {
1427 # fudCaHougfirp is a random string
1428 { if [[ $1 ]]; then
1429 echo "$1"
1430 else
1431 cat
1432 fi
1433 } | sed -r 's/-0([45])( |$)/fudCaHougfirp0\100/;s/_/T/;s/[:-]//g;s/fudCaHougfirp/-/'
1434
1435 }
1436 btrbk-date-sed() {
1437 local line
1438 while read -r line; do
1439 if [[ $line == *20[0-9][0-9][0-9][0-9][0-9][0-9]T[0-9][0-9][0-9][0-9][0-9][0-9]-0[45]00* ]]; then
1440 pre="${line%%20[0-9][0-9][0-9][0-9][0-9][0-9]T[0-9][0-9][0-9][0-9][0-9][0-9]-0[45]00*}"
1441 post="${line##*20[0-9][0-9][0-9][0-9][0-9][0-9]T[0-9][0-9][0-9][0-9][0-9][0-9]-0[45]00}"
1442 mid="${line:${#pre}:22}"
1443 echo "$pre$(btrbk-date "$mid")$post"
1444 else
1445 echo "$line"
1446 fi
1447 done
1448 }
1449 jrbtrbk() {
1450 jr -u btrbk-run -u btrbk -u switch-mail-host "$@"
1451 }
1452
1453 # internal function
1454 btrbk-host-debug-show-host() {
1455 for f; do
1456 snaphost=
1457 for host in $remote $alt local; do
1458 if line=$(grep -P "\S*$f" /tmp/b/s/$host.log); then
1459 if [[ $snaphost ]]; then
1460 e error: snaphost=$snaphost, host=$host line="$line"
1461 fi
1462 if [[ $line == ssh* ]]; then
1463 tmp="${line#ssh://}"
1464 snaphost="${tmp%%/*}"
1465 else
1466 snaphost=$host
1467 fi
1468 fi
1469 done
1470 echo $snaphost $f | btrbk-date-sed
1471 done
1472 }
1473
1474 # If we get a btrfs receive error like this:
1475 # ERROR: ... clone: did not find source subvol
1476 # running this command will help track down the problem.
1477 # Alter remote= and alt=. When I used it, remote is
1478 # the host having the error when I push a snapshot.
1479 # Alt is just the other host that takes snapshots
1480 # besides the local host.
1481 btrbk-host-debug() {
1482
1483 remote=b8.nz
1484 alt=sywg.b8.nz
1485
1486 mkdir -p /tmp/b/s
1487 for host in $remote $alt; do
1488 h=$(ssh $host hostname)
1489 rsync -a /var/log/btrbk $host:/var/log/btrbk /var/log/btrbk/$h
1490 grr '\bsnapshot success' /var/log/btrbk/$h >/tmp/b/$h.log
1491
1492 ## this takes a while, we only want to do it on 1st run
1493 # if [[ -s /tmp/b/$host.log ]]; then continue; fi
1494 # ssh $host journalctl -u btrbk-run -u btrbk -u switch-mail-host >/tmp/b/$host.log
1495 done
1496 gr '\bsnapshot success' /var/log/btrbk/*.log >/tmp/b/local.log
1497 cd /tmp/b
1498 for f in *.log; do
1499 gr '\bsnapshot success' $f >s/$f
1500 done
1501 cd /mnt/root/btrbk
1502 localq=(q.*)
1503 declare -A localq_a
1504 for f in "${localq[@]}"; do
1505 localq_a[$f]=t
1506 done
1507
1508 remoteq=()
1509 for f in $(ssh $remote "cd /mnt/root/btrbk; echo q.*"); do
1510 if [[ ! ${localq_a[$f]} ]]; then
1511 remoteq+=($f)
1512 fi
1513 done
1514 btrbk-host-debug-show-host "${localq[@]}"
1515 if (( ${#remoteq[@]} >= 1 )); then
1516 echo "=== $remote only ===="
1517 btrbk-host-debug-show-host ${remoteq[@]}
1518 fi
1519
1520 }
1521
1522 # note, to check for glue records
1523 # First, find some the .org nameservers:
1524 # dig +trace iankelling.org
1525 # then, query one:
1526 # dig ns1.iankelling.org @b0.org.afilias-nst.org.
1527
1528 # Now, compare for a domain that does have glue records setup (note the A
1529 # and AAAA records in ADDITIONAL SECTION, those are glue records like the
1530 # one I'm asking for):
1531
1532 # $ dig ns1.gnu.org @b0.org.afilias-nst.org.
1533
1534 bbk() { # btrbk wrapper
1535 local ret=0
1536 c /
1537 local active=true
1538 systemctl is-active btrbk.timer || active=false
1539 if $active; then
1540 ser stop btrbk.timer
1541 fi
1542 btrbk_is_active=$(systemctl is-active btrbk.service ||:)
1543 case $btrbk_is_active in
1544 inactive|failed) : ;;
1545 *)
1546 echo "bbk: error: systemctl is-active btrbk.service output: $btrbk_is_active"
1547 if $active; then ser start btrbk.timer; fi
1548 return 1
1549 ;;
1550 esac
1551 # todo: consider changing this to srun and having the args come
1552 # from a file like /etc/default/btrbk, like is done in exim
1553 s jdo btrbk-run "$@"
1554 if $active; then
1555 if (( ret )); then
1556 echo bbk: WARNING: btrbk.timer not restarted due to failure
1557 else
1558 ser start btrbk.timer
1559 fi
1560 fi
1561 return $ret
1562 }
1563
1564 faimon() {
1565 fai-monitor | pee cat "fai-monitor-gui -"
1566 }
1567
1568 bfg() { java -jar /a/opt/bfg-1.12.14.jar "$@"; }
1569
1570 bigclock() {
1571 xclock -digital -update 1 -face 'arial black-80:bold'
1572 }
1573
1574 nnn() { /a/opt/nnn -H "$@"; }
1575
1576 locat() { # log-once cat
1577 local files
1578 ngset
1579 files=(/var/local/cron-errors/* /home/iank/cron-errors/* /sysd-mail-once-state/*)
1580 case ${#files[@]} in
1581 0) : ;;
1582 1)
1583 echo ${files[0]}
1584 head ${files[0]}
1585 ;;
1586 *)
1587 head ${files[@]}
1588 ;;
1589 esac
1590 ngreset
1591 }
1592
1593 scr() {
1594 screen -RD "$@"
1595 }
1596
1597 # usage: first get an adb shell on the phone.
1598 #
1599 # just followed instructions in readme at
1600 # https://github.com/Yuubi-san/ceb-tools
1601 # tried to use ceb2txt but it failed because of schema
1602 # slightly different than what it expected.
1603 cheogram-get-logs() {
1604 #adb shell rm -r /storage/emulated/0/Download/Cheogram/Backup
1605 read -r -p "do cheogram backup on phone, do not enable extra cheogram data. press any key when done"
1606 cd /p/cheogram
1607 rm -rf Backup b
1608 adb pull /storage/emulated/0/Download/Cheogram/Backup
1609 sqlite3 b </a/opt/ceb-tools/schema.sql
1610 echo "note: the next step took 39 seconds last time i measured"
1611 # expected failure: Error: near line 1: in prepare, table accounts has no column named pinned_mechanism (1)
1612 # the sql needs an update
1613 /a/opt/ceb-tools/ceb2sqlgz Backup/iank@fsf.org.ceb <pas | gunzip | sqlite3 b ||:
1614 rm -r Backup
1615 }
1616
1617 # usage: cheologs [DAYS_LIMIT]
1618 # default days is 100
1619 cheologs() {
1620 local days q
1621 days=${1:-100}
1622 q="
1623 select
1624 datetime(substr(timeSent,0,11), 'unixepoch'),
1625 replace(replace(counterpart,'@fsf.org',''),
1626 '@conference.fsf.org',''),
1627 body
1628 from messages
1629 where timeSent > $(( (EPOCHSECONDS - days * 60 * 60 * 24) * 1000 ))
1630 order by timeSent;"
1631 sqlite3 /p/cheogram/b ".mode tabs" "$q" | less
1632 }
1633
1634 mycheologs() {
1635 local days q
1636 days=${1:-16}
1637 # timezone compared to utc. note: this takes the current offset, so if daylight savings change
1638 # happened in the looking back period, this won't account for it.
1639 zone_offset=$(( $( date +%z | sed 's/[^1-9-]*//g' ) * 60 * 60))
1640 case $zone_offset in
1641 -*) : ;;
1642 *) zone_offset="+ $zone_offset"
1643 esac
1644 echo zone_offset=$zone_offset
1645 q="
1646 select
1647 datetime(substr(timeSent,0,11) $zone_offset, 'unixepoch'),
1648 body
1649 from messages
1650 where timeSent > $(( (EPOCHSECONDS - days * 60 * 60 * 24) * 1000 ))
1651 and counterpart = 'office@conference.fsf.org/iank'
1652 order by timeSent;"
1653 sqlite3 /p/cheogram/b ".mode tabs" "$q" | sed 's/ /./' | less
1654 }
1655
1656 # version of jdo for my non-root user
1657 jdo() {
1658 # comparison of alternative logging methods:
1659 #
1660 # systemd-run command (what this function does)
1661 #
1662 # If there is a user prompt, the program will detect that it is not
1663 # connected to a terminal and act in a non-interactive way, skipping
1664 # the prompt. This has the benefit that you know exactly how the
1665 # program will act if you want to move it into a service that runs
1666 # automatically.
1667 #
1668 # If run with sudo and command is a shell script which does a sleep,
1669 # it can (sometimes?) output some extra whitespace in front of
1670 # messages, more for each subsequent message. This can be avoided by
1671 # becoming root first.
1672 #
1673 # It logs the command's pid and exit code, which is nice.
1674 #
1675 #
1676 ### command |& ts | tee file.log
1677 #
1678 # If there is a user prompt, like "read -p prompt var", it will hang
1679 # without outputting the prompt.
1680 #
1681 # I've had a few times where ts had an error and I wasn't totally sure
1682 # if it was really the command or ts having the problem.
1683 #
1684 # Sometimes some output will get hidden until you hit enter.
1685 #
1686 #
1687 ### command |& pee cat logger
1688 #
1689 # This seems to work. I need to test more.
1690 #
1691 #
1692 ### command |& logger -s
1693 #
1694 # User prompts get confusingly prefixed to earlier output, and all log
1695 # entries get prefixed with annoying priority level.
1696 #
1697 #
1698 ### systemd-cat
1699 #
1700 # Had a few problems. One major one is that it exited in the middle of
1701 # a command on systemctl daemon-reload
1702 #
1703 # Related commands which can log a whole session: script, sudo, screen
1704 local cmd cmd_name jr_pid ret
1705 ret=0
1706 cmd="$1"
1707 shift
1708 cmd_name=${cmd##*/}
1709 if [[ $cmd != /* ]]; then
1710 cmd=$(type -P "$cmd")
1711 fi
1712 #note date format for since is date '+%F %T'
1713 # -q = quiet
1714 journalctl --since=now -qn2 -f -u "$cmd_name" &
1715 jr_pid=$!
1716 # note, we could have a version that does system --user, but if for example
1717 # it does sudo ssh, that will leave a process around that we can't kill
1718 # and it will leave the unit hanging around in a failed state needing manual
1719 # killing of the process.
1720 s systemd-run --uid "$(id -u)" --gid "$(id -g)" \
1721 -E SSH_AUTH_SOCK=/run/openssh_agent \
1722 --unit "$cmd_name" --wait --collect "$cmd" "$@" || ret=$?
1723 # The sleep lets the journal output its last line
1724 # before the prompt comes up.
1725 sleep .5
1726 kill $jr_pid &>/dev/null ||:
1727 unset jr_pid
1728 fg &>/dev/null ||:
1729 # this avoids any err-catch
1730 (( ret == 0 )) || return $ret
1731 }
1732
1733 # service run, and watch the output
1734 srun() {
1735 local unit
1736 ret=0
1737 unit=$1
1738 journalctl -qn2 -f -u $unit &
1739 systemctl start $unit
1740 sleep 2
1741 kill $jr_pid &>/dev/null ||:
1742 unset jr_pid
1743 fg &>/dev/null ||:
1744 }
1745
1746 sm() { # switch mail host
1747 local tmp keyhash
1748 c /
1749 # run latest
1750 keyhash=$(s ssh-keygen -lf /root/.ssh/home | awk '{print $2}')
1751 tmp=$(s ssh-add -l | awk '$2 == "'$keyhash'"' ||:)
1752 if [[ ! $tmp ]]; then
1753 s ssh-add /root/.ssh/home
1754 fi
1755 s jdo switch-mail-host "$@"
1756 return $ret
1757 }
1758 sh2() { # switch host2
1759 local tmp keyhash
1760 c /
1761 # run latest
1762 keyhash=$(s ssh-keygen -lf /root/.ssh/home | awk '{print $2}')
1763 tmp=$(s ssh-add -l | awk '$2 == "'$keyhash'"')
1764 if [[ ! $tmp ]]; then
1765 s ssh-add /root/.ssh/home
1766 fi
1767 install-my-scripts
1768 s jdo switch-host2 "$@"
1769 return $ret
1770 }
1771
1772 # shellcheck disable=SC2120
1773 lipush() {
1774 # note, i had --delete-excluded, but that deletes all files in --exclude-from on
1775 # the remote site, which doesn't make sense, so not sure why i had it.
1776 local p a
1777 # excluding emacs for now
1778 #p=(/a/opt/{emacs-debian11{,-nox},mu,emacs} /a/bin /a/exe /a/h /a/c /p/c/machine_specific/vps{,.hosts})
1779 p=(
1780 /a/bin /a/exe /a/h /a/c /p/c/machine_specific/vps{,.hosts}
1781 /c/roles/prom_export/files/simple/usr/local/bin/fsf-install-node-exporter
1782 /a/opt/fpaste
1783 /p/c/user-specific/www-data/icecast-fsf{,-tech}-htpasswd
1784 /p/c/icecast.xml
1785 )
1786 a="-ahviSAXPH --specials --devices --delete --relative --exclude-from=/p/c/li-rsync-excludes"
1787 ret=0
1788 for h in li je bk; do
1789 m s rsync "$@" $a ${p[@]} /p/c/machine_specific/$h root@$h.b8.nz:/
1790 ## only li is debian11
1791 #p[0]=/a/opt/emacs-trisuqel10
1792 #p[1]=/a/opt/emacs-trisquel10-nox
1793 done
1794 m s rsync "$@" -ahviSAXPH root@li.b8.nz:/a/h/proposed-comments/ /a/h/proposed-comments || ret=$?
1795 return $ret
1796 }
1797 bkpush() { # no emacs. for running faster.
1798 p=(/a/bin /a/exe /a/h /a/c /p/c/machine_specific/vps{,.hosts} /c/roles/prom_export/files/simple/usr/local/bin/fsf-install-node-exporter)
1799 a="-ahviSAXPH --specials --devices --delete --relative --exclude-from=/p/c/li-rsync-excludes"
1800 ret=0
1801 m rsync "$@" $a ${p[@]} /p/c/machine_specific/bk root@bk.b8.nz:/ || ret=$?
1802 return $ret
1803 }
1804 jepush() { # no emacs. for running faster.
1805 p=(/a/bin /a/exe /a/h /a/c /p/c/machine_specific/vps{,.hosts} /c/roles/prom_export/files/simple/usr/local/bin/fsf-install-node-exporter)
1806 a="-ahviSAXPH --specials --devices --delete --relative --exclude-from=/p/c/li-rsync-excludes"
1807 ret=0
1808 m rsync "$@" $a ${p[@]} /p/c/machine_specific/je root@je.b8.nz:/ || ret=$?
1809 return $ret
1810 }
1811
1812 bindpush() {
1813 dsign iankelling.org expertpathologyreview.com zroe.org amnimal.ninja
1814 lipush
1815 for h in li bk; do
1816 m ssh iank@$h.b8.nz dnsup
1817 done
1818 }
1819 bindpushb8() {
1820 lipush
1821 for h in li bk; do
1822 m ssh $h.b8.nz dnsb8
1823 done
1824 }
1825
1826 dnsup() {
1827 conflink -f
1828 m ser reload named
1829 }
1830 dnsb8() {
1831 local f=/var/lib/bind/db.b8.nz
1832 m ser stop named
1833 # jbk is like a temp file. dunno if removing it helps
1834
1835 i=0
1836 while pgrep '^named$' &>/dev/null; do
1837 sleep .5
1838 i=$(( i + 1 ))
1839 if (( i > 100 )); then
1840 echo "dnsb8: error: timeout waiting for named to exit"
1841 return 1
1842 fi
1843 done
1844 m sudo rm -fv $f.jnl $f.signed.jnl $f.jbk
1845 m sudo install -m 644 -o bind -g bind /p/c/machine_specific/vps/bind-initial/db.b8.nz $f
1846 m ser restart named
1847 }
1848 dnsecgen() {
1849 # keys generated like this
1850 # because of https://ftp.isc.org/isc/dnssec-guide/dnssec-guide.pdf
1851 # https://blog.apnic.net/2019/05/23/how-to-deploying-dnssec-with-bind-and-ubuntu-server/
1852
1853 # key length is longer than that guide because
1854 # we are using those at fsf and when old key lengths
1855 # become insecure, I want some extra time to update.
1856 # dnsecgen (in brc2)
1857
1858 local zone=$1
1859 dnssec-keygen -a RSASHA256 -b 2048 $zone
1860 dnssec-keygen -f KSK -a RSASHA256 -b 4096 $zone
1861 for f in K"$zone".*.key; do
1862 # eg Kb8.nz.+008+47995.key tag=47995
1863 # in dnsimple, you add the long string from this.
1864 # in gandi, you add the long string from the .key file,
1865 # then see that the digest matches the ds.
1866 echo "tag is the number after DS"
1867 dnssec-dsfromkey -a SHA-256 $f
1868 done
1869 # For b8.nz, we let bind read the keys and sign, and
1870 # right now they have root ownership, so let them
1871 # get group read.
1872 chmod g+r ./*.private
1873 }
1874 dsign() {
1875 # create .signed file
1876 # note: full paths probably not needed.
1877 local arg
1878 for arg; do
1879 local zone=${arg#db.}
1880 local dir=/p/c/machine_specific/vps/filesystem/var/lib/bind
1881 dnssec-signzone -S -e +31536000 -o $zone -K $dir -d $dir $dir/db.$zone
1882 done
1883 }
1884
1885 # set day start for use in other programs.
1886 # expected to do be in a format like 830, or 800 or 1300.
1887 ds() {
1888 local regex
1889 regex='[0-9]?[0-9]?[0-9][0-9]'
1890 if [[ $1 ]]; then
1891 if [[ ! $1 =~ $regex ]]; then
1892 echo "ds: error. expected \$1 to match $regex, got \$1: $1"
1893 return 1
1894 fi
1895 echo $1 >/b/data/daystart
1896 else
1897 cat /b/data/daystart
1898 fi
1899 }
1900
1901 #### begin bitcoin related things
1902 btc() {
1903 local f=/etc/bitcoin/bitcoin.conf
1904 # importprivkey will timeout if using the default of 15 mins.
1905 # upped it to 1 hour.
1906 bitcoin-cli -rpcclienttimeout=60000 -"$(s grep rpcuser= $f)" -"$(s grep rpcpassword= $f)" "$@"
1907 }
1908 btcusd() { # $1 btc in usd
1909 local price
1910 price="$(curl -s https://api.coinbase.com/v2/prices/BTC-USD/spot | jq -r .data.amount)"
1911 printf "$%s\n" "$price"
1912 if [[ $1 ]]; then
1913 printf "$%.2f\n" "$(echo "scale=4; $price * $1"| bc -l)"
1914 fi
1915 }
1916 usdbtc() { # $1 usd in btc
1917 local price
1918 price="$(curl -s https://api.coinbase.com/v2/prices/BTC-USD/spot | jq -r .data.amount)"
1919 printf "$%s\n" "$price"
1920 if [[ $1 ]]; then
1921 # 100 mil satoshi / btc. 8 digits after the 1.
1922 printf "%.8f btc\n" "$(echo "scale=10; $1 / $price "| bc -l)"
1923 fi
1924 }
1925 satoshi() { # $1 satoshi in usd
1926 local price
1927 price="$(curl -s https://api.coinbase.com/v2/prices/BTC-USD/spot | jq -r .data.amount)"
1928 price=$(echo "scale=10; $price * 0.00000001"| bc -l)
1929 printf "$%f\n" "$price"
1930 if [[ $1 ]]; then
1931 printf "$%.2f\n" "$(echo "scale=10; $price * $1"| bc -l)"
1932 fi
1933 }
1934
1935 # Bitcoin holds open the wallet file. this causes problems for a
1936 # secondary computer running bitcoin and receiving a backup (as of
1937 # 2023). However, in 2024-02, I ran a backup where a receiving machine
1938 # had the wallet enabled and there was no error, so I don't know if this
1939 # is still an issue or likely it is an inconsistent behavior.
1940 # Note: a pruned node won't allow for a wallet to be added, super lame
1941 # so i'm just not running a bitcoin node for now.
1942 # Error: Prune: last wallet synchronisation goes beyond pruned data. You
1943 # need to -reindex (download the whole blockchain again in case of
1944 # pruned node)
1945 #
1946 #### end bitcoin related things
1947
1948
1949
1950 cbfstool () { /a/opt/coreboot/build/cbfstool "$@"; }
1951
1952
1953 cgpl()
1954 {
1955 if (($#)); then
1956 cp /a/bin/data/COPYING "$@"
1957 else
1958 cp /a/bin/data/COPYING .
1959 fi
1960 }
1961
1962 capache()
1963 {
1964 if (($#)); then
1965 cp /a/bin/data/LICENSE "$@"
1966 else
1967 cp /a/bin/data/LICENSE .
1968 fi
1969 }
1970
1971
1972
1973 apache-header() {
1974 # First paragraph is to avoid people being confused about why a
1975 # file is apache licensed.
1976 cat <<'EOF'
1977 # I, Ian Kelling, follow the GNU license recommendations at
1978 # https://www.gnu.org/licenses/license-recommendations.en.html. They
1979 # recommend that small programs, < 300 lines, be licensed under the
1980 # Apache License 2.0. This file contains or is part of one or more small
1981 # programs. If a small program grows beyond 300 lines, I plan to change
1982 # to a recommended GPL license.
1983
1984 # Copyright 2024 Ian Kelling
1985
1986 # Licensed under the Apache License, Version 2.0 (the "License");
1987 # you may not use this file except in compliance with the License.
1988 # You may obtain a copy of the License at
1989
1990 # http://www.apache.org/licenses/LICENSE-2.0
1991
1992 # Unless required by applicable law or agreed to in writing, software
1993 # distributed under the License is distributed on an "AS IS" BASIS,
1994 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
1995 # See the License for the specific language governing permissions and
1996 # limitations under the License.
1997
1998 EOF
1999
2000 }
2001
2002 # apply apache to git tracked bash files + README, except files with A?GPL3 header.
2003 apache-apply-repo() {
2004 for f in $(git ls-files); do
2005 [[ -L $f || ! -f $f ]] && continue
2006 if [[ $f != README ]]; then
2007 if ! grep -n '^#!/bin/bash' $f | grep ^1: &>/dev/null; then continue; fi
2008 if head -n 10 $f | grep 'it under the terms of the GNU General Public License as published by' &>/dev/null; then continue; fi
2009 fi
2010 apache-apply $f
2011 done
2012 }
2013
2014 apache-apply() {
2015 for file; do
2016 if [[ ! -e $file ]]; then
2017 echo '#!/bin/bash' >$file
2018 chmod +x $file
2019 fi
2020 if head -n1 "$file"| grep -E '^#!/' &>/dev/null; then
2021 {
2022 head -n1 "$file"
2023 apache-header
2024 tail -n+2 "$file"
2025 } | sponge "$file"
2026 else
2027 {
2028 apache-header
2029 cat "$file"
2030 } | sponge "$file"
2031 fi
2032 done
2033 }
2034 # strip out the apache license from a file.
2035 apache-strip() {
2036 # shellcheck disable=SC2044 # meh
2037 for f in $(find . -type f -maxdepth 1); do if head -n1 "$f"| grep -E '^#!/bin/bash\b' &>/dev/null; then { head -n 20 $f | tac | sed '/^# limitations under the License.$/,/^# Copyright.*Ian Kelling$/d' | tac; tail -n+21 $f; } |sponge $f; fi ; done
2038 }
2039
2040 chro() {
2041 if type -p chromium &>/dev/null; then
2042 cmd=chromium
2043 else
2044 cd /
2045 cmd="schroot -c bookworm chromium"
2046 CHROMIUM_FLAGS='--enable-remote-extensions' $cmd & r
2047 fi
2048 }
2049
2050
2051 # do all tee.
2052 # pipe to this, or just type like a shell
2053 # todo: test this
2054 dat() {
2055 tee >(ssh frodo.b8.nz) >(ssh x2) >(ssh tp.b8.nz) >(ssh kw) >(ssh tp.b8.nz)
2056 }
2057 da() { # do all
2058 local host
2059 for host in x2 kw tp.b8.nz x3.b8.nz frodo.b8.nz; do
2060 ssh $host "$@"
2061 done
2062 }
2063
2064
2065 debian_pick_mirror () {
2066 # netselect-apt finds a fast mirror.
2067 # but we need to replace the mirrors ourselves,
2068 # because it doesnt do that. best it can do is
2069 # output a basic sources file
2070 # here we get the server it found, get the main server we use
2071 # then substitute all instances of one for the other in the sources file
2072 # and backup original to /etc/apt/sources.list-original.
2073 # this is idempotent. the only way to identify debian sources is to
2074 # note the original server, so we put it in a comment so we can
2075 # identify it later.
2076 local file
2077 file=$(mktemp -d)/f # safe way to get file name without creating one
2078 sudo netselect-apt -o "$file" || return 1
2079 url=$(grep ^\\w $file | head -n1 | awk '{print $2}')
2080 sudo cp -f /etc/apt/sources.list /etc/apt/sources.list-original
2081 sudo sed -ri "/http.us.debian.org/ s@( *[^ #]+ +)[^ ]+([^#]+).*@\1$url\2# http.us.debian.org@" /etc/apt/sources.list
2082 sudo apt-get update
2083 }
2084 digme() {
2085 digdiff @ns{1,2}.iankelling.org "$@"
2086 }
2087
2088 dup() {
2089 local ran_d
2090 ran_d=false
2091 system-status _
2092 case $PS1 in
2093 *[\ \]]D\ *)
2094 pushd /
2095 /b/ds/distro-begin |& ts || return $?
2096 /b/ds/distro-end |& ts || return $?
2097 popd
2098 ran_d=true
2099 ;;&
2100 *[\ \]]DB\ *)
2101 pushd /
2102 /b/ds/distro-begin |& ts || return $?
2103 popd
2104 ran_d=true
2105 ;;
2106 *[\ \]]DE\ *)
2107 pushd /
2108 /b/ds/distro-end |& ts || return $?
2109 popd
2110 ran_d=true
2111 ;;&
2112 *CONFLINK*)
2113 if ! $ran_d; then
2114 conflink
2115 fi
2116 ;;
2117 esac
2118 system-status _
2119 }
2120
2121 envload() { # load environment from a previous: export > file
2122 local file=${1:-$HOME/.${USER}_env}
2123 eval "$(export | sed 's/^declare -x/export -n/')"
2124 while IFS= read -r line; do
2125 # declare -x makes variables local to a function
2126 eval ${line/#declare -x/export}
2127 done < "$file"
2128 }
2129
2130 failfunc() { asdf a b c; }
2131 failfunc2() { failfunc d e f; }
2132
2133 # one that comes with distros is too old for newer devices
2134 fastboot() {
2135 /a/opt/android-platform-tools/fastboot "$@";
2136 }
2137
2138 kdecd() { /usr/lib/x86_64-linux-gnu/libexec/kdeconnectd; }
2139
2140 batp() {
2141 cat /sys/class/power_supply/BAT0/capacity
2142 }
2143
2144 # List of apps to install/update
2145 # Create from existing manually installed apps by doing
2146 # fdroidcl update
2147 # fdroidcl search -i, then manually removing
2148 # automatically installed/preinstalled apps
2149
2150 #
2151 # # my attempt at recovering from boot loop:
2152 # # in that case, boot to recovery (volume up, home button, power, let go of power after samsun logo)
2153 # # then
2154 # mount /dev/block/mmcblk0p12 /data
2155 # cd /data
2156 # find -iname '*appname*'
2157 # rm -rf FOUND_DIRS
2158 # usually good enough to just rm -rf /data/app/APPNAME
2159 #
2160 # currently broken:
2161 # # causes replicant to crash
2162 # org.quantumbadger.redreader
2163 # org.kde.kdeconnect_tp
2164
2165 # not broke, but wont work without gps
2166 #com.zoffcc.applications.zanavi
2167 # not broke, but not using atm
2168 #com.nutomic.syncthingandroid
2169 # # doesn\'t work on replicant
2170 #net.sourceforge.opencamera
2171 #
2172 fdroid_pkgs=(
2173 net.mullvad.mullvadvpn
2174 org.schabi.newpipe
2175 io.github.subhamtyagi.lastlauncher
2176 io.anuke.mindustry
2177 com.biglybt.android.client
2178 de.marmaro.krt.ffupdater
2179 me.ccrama.redditslide
2180 org.fedorahosted.freeotp
2181 at.bitfire.davdroid
2182 com.alaskalinuxuser.justnotes
2183 com.artifex.mupdf.viewer.app
2184 com.danielkim.soundrecorder
2185 com.fsck.k9
2186 com.ichi2.anki
2187 com.jmstudios.redmoon
2188 com.jmstudios.chibe
2189 org.kde.kdeconnect_tp
2190 com.notecryptpro
2191 com.termux
2192 cz.martykan.forecastie
2193 de.danoeh.antennapod
2194 de.blinkt.openvpn
2195 de.marmaro.krt.ffupdater
2196 eu.siacs.conversations
2197 free.rm.skytube.oss
2198 im.vector.alpha # riot
2199 info.papdt.blackblub
2200 me.tripsit.tripmobile
2201 net.gaast.giggity
2202 net.minetest.minetest
2203 net.osmand.plus
2204 org.isoron.uhabits
2205 org.linphone
2206 org.gnu.icecat
2207 org.smssecure.smssecure
2208 org.yaaic
2209 sh.ftp.rocketninelabs.meditationassistant.opensource
2210 )
2211 # https://forum.xda-developers.com/android/software-hacking/wip-selinux-capable-superuser-t3216394
2212 # for maru,
2213 #me.phh.superuser
2214
2215 fdup() {
2216 local -A installed updated
2217 local p
2218 # tried putting this in go buildscript cronjob,
2219 # but it failed with undefined: os.UserCacheDir. I expect its due to
2220 # an environment variable missing, but its easier just to stick it here.
2221 m go get -u mvdan.cc/fdroidcl || return 1
2222 m fdroidcl update
2223 if fdroidcl search -u | grep ^org.fdroid.fdroid; then
2224 fdroidcl install org.fdroid.fdroid
2225 sleep 5
2226 m fdroidcl update
2227 fi
2228 for p in $(fdroidcl search -i| grep -o "^\S\+"); do
2229 installed[$p]=true
2230 done
2231 for p in $(fdroidcl search -u| grep -o "^\S\+"); do
2232 updated[$p]=false
2233 done
2234 for p in ${fdroid_pkgs[@]}; do
2235 if ! ${installed[$p]:-false}; then
2236 m fdroidcl install $p
2237 # sleeps are just me being paranoid since replicant has a history of crashing when certain apps are installed
2238 sleep 5
2239 fi
2240 done
2241 for p in ${!installed[@]}; do
2242 if ! ${updated[$p]:-true}; then
2243 m fdroidcl install $p
2244 sleep 5
2245 fi
2246 done
2247 }
2248
2249 firefox-default-profile() {
2250 local key value section
2251 key=Default
2252 value=1
2253 section=$1
2254 file=/p/c/subdir_files/.mozilla/firefox/profiles.ini
2255 sed -ri "/^ *$key/d" "$file"
2256 sed -ri "/ *\[$section\]/,/^ *\[[^]]+\]/{/^\s*${key}[[:space:]=]/d};/ *\[$section\]/a $key=$value" "$file"
2257 }
2258 fdhome() { #firefox default home profile
2259 firefox-default-profile Profile0
2260 }
2261
2262 fdwork() {
2263 firefox-default-profile Profile4
2264 }
2265
2266 ff() {
2267 if type -P firefox &>/dev/null; then
2268 firefox "$@"
2269 else
2270 iceweasel "$@"
2271 fi
2272 }
2273
2274 fn() {
2275 firefox -P alt "$@" >/dev/null 2>&1
2276 }
2277
2278
2279 fsdiff () {
2280 local missing=false
2281 local dname="${PWD##*/}"
2282 local m="/a/tmp/$dname-missing"
2283 local d="/a/tmp/$dname-diff"
2284 [[ -e $d ]] && rm "$d"
2285 [[ -e $m ]] && rm "$m"
2286 local msize=0
2287 local fsfile
2288 while read -r line; do
2289 fsfile="$1${line#.}"
2290 if [[ -e "$fsfile" ]]; then
2291 md5diff "$line" "$fsfile" && tee -a "/a/tmp/$dname-diff" <<< "$fsfile $line"
2292 else
2293 missing=true
2294 echo "$line" >> "$m"
2295 msize=$((msize + 1))
2296 fi
2297 done < <(find . -type f )
2298 if $missing; then
2299 echo "$m"
2300 (( msize <= 100 )) && cat $m
2301 fi
2302 }
2303 fsdiff-test() {
2304 local tmpd x
2305 # expected output, with different tmp dirs
2306 # /tmp/tmp.HDPbwMqdC9/c/d ./c/d
2307 # /a/tmp/tmp.qLDkYxBYPM-missing
2308 # ./b
2309 tmpd="$(mktemp -d)"
2310 cd "$tmpd"
2311 echo ok > a
2312 echo nok > b
2313 mkdir c
2314 echo ok > c/d
2315 local x
2316 x=$(mktemp -d)
2317 mkdir $x/c
2318 echo different > $x/c/d
2319 echo ok > $x/a
2320 fsdiff $x
2321 rm -r "$x" "$tmpd"
2322 }
2323 rename-test() {
2324 # test whether missing files were renamed, generally for use with fsdiff
2325 # $1 = fsdiff output file, $2 = directory to compare to. pwd = fsdiff dir
2326 # echos non-renamed files
2327 local x line found renamed
2328 local -a sums
2329 for x in "$2"/*; do
2330 { sums+=( "$(md5sum < "$x")" ) ; } 2>/dev/null
2331 done
2332 while read -r line; do
2333 { missing_sum=$(md5sum < "$line") ; } 2>/dev/null
2334 renamed=false
2335 for x in "${sums[@]}"; do
2336 if [[ $missing_sum == "$x" ]]; then
2337 renamed=true
2338 break
2339 fi
2340 done
2341 $renamed || echo "$line"
2342 done < "$1"
2343 return 0
2344 }
2345
2346 feh() {
2347 # F = fullscren, z = random, Z = auto zoom
2348 command feh --auto-rotate -FzZ "$@"
2349 }
2350
2351
2352
2353 fw() {
2354 firefox -P default "$@" >/dev/null 2>&1
2355 }
2356
2357 gitian() {
2358 git config user.email ian@iankelling.org
2359 }
2360
2361 # at least in flidas, things rely on gpg being gpg1
2362 gpg() {
2363 if type -P gpg2 &>/dev/null; then
2364 command gpg2 "$@"
2365 else
2366 command gpg "$@"
2367 fi
2368 }
2369
2370 gse() {
2371 local email=iank@fsf.org
2372 git send-email --notes "--envelope-sender=<$email>" \
2373 --suppress-cc=self "$@"
2374 }
2375
2376 gup() { /a/f/gnulib/build-aux/gnupload "$@"; }
2377
2378 dejagnu() { /a/opt/dejagnu/dejagnu "$@"; }
2379
2380 # do git status on published repos.
2381 hstatus() {
2382 c /a/bin/githtml
2383 for x in *; do
2384 cd "$(readlink -f $x)"/..
2385 status=$(i status -s) || pwd
2386 if [[ $status ]]; then
2387 hr
2388 echo $x
2389 printf "%s\n" "$status"
2390 fi
2391 cd /a/bin/githtml
2392 done
2393 }
2394
2395 hsk() {
2396 local x
2397 c /a/bin/githtml
2398 for x in *; do
2399 cd "$(readlink -f $x)"/..
2400 skgit
2401 cd /a/bin/githtml
2402 done
2403 }
2404
2405 ## work log
2406 #
2407 # note: database location is specified in ~/.timetrap.yml, currently /p/.timetrap.db
2408 wlog() {
2409 local day i days_back
2410 days_back=${1:-16}
2411 for (( i=days_back; i>=0; i-- )); do
2412 day=$( date +%F -d @$((EPOCHSECONDS - 86400*i )) )
2413 date "+%a %b %d" -d @$((EPOCHSECONDS - 86400*i )) | tr '\n' ' '
2414 /a/opt/timetrap/bin/t d -ftotal -s $day -e $day all -m '^w|lunch$'
2415 done
2416 }
2417 to() { t out -a "$@"; }
2418 ti() { t in -a "$@"; }
2419 tl() {
2420 local in_secs
2421 to "$*"
2422 t s lunch
2423 t in -a "$*"
2424 in_secs="$(date -d "${*//[_.]/ }" +%s)"
2425 m t out -a "$(date +%F.%T -d @$(( in_secs + 60*45 )) )"
2426 t s w
2427 }
2428
2429
2430 # help me focus. opens 2 windows.
2431 focus() {
2432 /p/c/proc/focus/linux-amd64/focus &
2433 watcharb5
2434 kill %%
2435 }
2436
2437
2438 # Display a list of the active window title
2439 # i've been on with 10 second samples going back
2440 # 5 minutes. If I've been on one window for 10 seconds
2441 # or longer, then display the second count.
2442 #
2443 # Press any key to exit.
2444 watcharb5() {
2445 local char ret
2446 killall arbtt-capture &>/dev/null ||:
2447 rm -f ~/.arbtt/capture.log
2448 arbtt-capture --sample-rate=10 &
2449 while true; do
2450 arb5
2451 ret=0
2452 # i first thought to sleep and capture ctrl-c, but it seems we can't
2453 # capture control-c, unless maybe we implement the commands in a
2454 # separate script or maybe add err-cleanup to err. Anyways, this
2455 # method is superior because any single char exits.
2456 read -rsN1 -t 5 char || ret=$?
2457 if (( ret == 142 )) || [[ ! $char ]]; then
2458 # debug
2459 #e ret=$ret char=$char
2460 :
2461 else
2462 killall arbtt-capture ||:
2463 return 0
2464 fi
2465 done
2466
2467 }
2468
2469 arb5() {
2470 local i j l sec blanks line
2471 local -a arbtt_lines
2472 if [[ ! -e ~/.arbtt/capture.log ]]; then
2473 sleep 5
2474 fi
2475 blanks=$(( LINES - 34 ))
2476 for (( i=0; i < blanks; i++ )); do
2477 echo
2478 done
2479
2480 {
2481 i=0
2482 j=0
2483 # https://stackoverflow.com/questions/56486272/how-to-concat-multiple-fields-to-same-line-with-jq
2484 arbtt_lines=$(arbtt-dump -l 30 -t json | \
2485 jq -r '.[] | [ ( .inactive / 1000 | floor ) , ( .windows[] | select (.active == true) |.title) ] | @tsv' | tac)
2486 for line in "${arbtt_lines[@]}"; do
2487 read -r sec l <<<"$line"
2488 if (( j >= LINES )); then
2489 break
2490 fi
2491 if (( i % 6 == 0 && i >= 2 )); then
2492 j=$(( j + 1 ))
2493 echo "## $(( i / 6 + 1 )) ##"
2494 fi
2495 if (( sec > 10 )); then
2496 printf "%3d %s\n" $sec "$l" | sed -r "s/^(.{$COLUMNS}).*/\1/"
2497 else
2498 printf " %s\n" "$l" | sed -r "s/^(.{$COLUMNS}).*/\1/"
2499 fi
2500 i=$(( i + 1 ))
2501 j=$(( j + 1 ))
2502 done
2503 while (( j < 34 && j < LINES )); do
2504 echo
2505 j=$(( j + 1 ))
2506 done
2507 } | tac
2508 }
2509
2510 arbttlog() {
2511 # from the log, show only the currently active window, and the number of
2512 # seconds of input inactivity.
2513 arbtt-dump "$@" | grep -v '( )\|Current Desktop' | sed -rn '/^[^ ]/{N;s/^(.{21})([0-9]*)[0-9]{3}m.*\(\*/\1\2/;s/^(.{21})[0-9]*.*\(\*/\1/;s/\n//;p}' ; }
2514
2515 idea() {
2516 /a/opt/idea-IC-163.7743.44/bin/idea.sh "$@" & r
2517 }
2518
2519 ilogs-local() {
2520 d=/var/lib/znc/moddata/log/iank/
2521 for n in freenode libera; do
2522 cd $d/$n
2523 hr
2524 for x in "#$1/"*; do
2525 base=${x##*/}
2526 files=()
2527 for f in $@; do
2528 tmp=\#$f/$base
2529 if [[ -e $tmp ]]; then
2530 files+=(\#$f/$base)
2531 fi
2532 done
2533 sed \"s/^./${base%log}/\" ${files[@]}|sort -n
2534 hr
2535 done
2536 done
2537 }
2538 ilogs() {
2539 sl root@iankelling.org ilogs-local "$@"
2540 }
2541
2542
2543 ilog-local() {
2544 local d chan
2545 chan="$1"
2546 d=/var/lib/znc/moddata/log/iank/
2547 for n in freenode libera; do
2548 if [[ ! -d $d$n/"$chan" ]]; then
2549 continue
2550 fi
2551 cd $d$n/"$chan"
2552 hr
2553 for x in *; do
2554 # *** are parts and joins and such, and they make reading hard.
2555 # I probably will want to see them sometimes, just have to
2556 # remove that part.
2557 echo $x; sed "s/^./${x%log}/;/\*\*\*/d" $x; hr;
2558 done
2559 done
2560 }
2561 ilog() {
2562 local chan tmpf
2563 tmpf=$(mktemp)
2564 chan="${1:-#fsfsys}"
2565 # use * instead of -r since that does sorted order
2566 sl root@li.b8.nz ilog-local "$chan" > $tmpf
2567 less +G $tmpf
2568 rm -f $tmpf
2569 }
2570
2571 o() {
2572 if type gio &> /dev/null ; then
2573 gio open "$@"
2574 elif type gvfs-open &> /dev/null ; then
2575 gvfs-open "$@"
2576 else
2577 xdg-open "$@"
2578 fi
2579 # another alternative is run-mailcap
2580 }
2581 ccomp xdg-open o
2582
2583 # jfilter() {
2584 # grep -Evi -e "^(\S+\s+){4}(sudo|sshd|cron)\[\S*:" \
2585 # -e "^(\S+\s+){4}systemd\[\S*: (starting|started) (btrfsmaintstop|dynamicipupdate|spamd dns bug fix cronjob|rss2email)\.*$"
2586 # }
2587 # jtail() {
2588 # journalctl -n 10000 -f "$@" | jfilter
2589 # }
2590 # jr() { journalctl "$@" | jfilter | less ; }
2591 # jrf() { journalctl -n 200 -f "$@" | jfilter; }
2592
2593
2594 ## old version for model01. i need to get that firmware working again.
2595 # kff() { # keyboardio firmware flash. you must hold down the tilde key
2596 # pushd /a/opt/Model01-Firmware
2597 # # if we didn't want this yes hack, then remove "shell read" from
2598 # # /a/opt/Kaleidoscope/etc/makefiles/sketch.mk
2599 # yes $'\n' | VERBOSE=1 make flash
2600 # popd
2601 # }
2602
2603
2604 kff() {
2605 pushd /a/opt/Kaleidoscope/examples/Devices/Keyboardio/Model100
2606 make flash
2607 popd
2608 }
2609
2610 wgkey() {
2611 local umask_orig name
2612 if (( $# != 1 )); then
2613 e expected 1 arg >&2
2614 return 1
2615 fi
2616 name=$1
2617 umask_orig=$(umask)
2618 umask 0077
2619 wg genkey | tee $name-priv.key | wg pubkey > $name-pub.key
2620 umask $umask_orig
2621 }
2622
2623 host-info-all() {
2624 host-info-update
2625
2626 bindpushb8
2627 # for wireguard configs
2628 ssh iank@li.b8.nz conflink
2629 wrt-setup
2630 }
2631
2632
2633
2634
2635
2636 ## for updating host info like ip, location, update /p/c/host-info and
2637 ## host_info below. the host_info array should probably be in its own
2638 ## file that gets sourced so that it can be more easily updated.
2639
2640 # todo: this is so long that it becomes confusing,
2641 # try to split it up.
2642 #
2643 # To make some changes take effect, run host-info-all.
2644 host-info-update() {
2645
2646 local -A vpn_ips host_ips host_macs portfw_ips nonvpn_ips all_ips
2647 local -a root_hosts nonroot_hosts
2648
2649 # the hosts with no mac
2650 root_hosts=( bk je li b8.nz )
2651 for h in ${root_hosts[@]}; do
2652 root_hosts+=(${h}ex)
2653 done
2654 root_hosts+=(cmc)
2655
2656 while read -r ip host mac opts; do
2657 if [[ $ip == *#* || ! $host ]]; then continue; fi
2658
2659 # opt parsing
2660 vpn=false
2661 root=false
2662 for opt in $opts; do
2663 case $opt in
2664 user=root)
2665 root=true
2666 ;;
2667 vpn)
2668 vpn=true
2669 ;;
2670 esac
2671 done
2672
2673 all_ips[$host]=$ip
2674 if $vpn; then
2675 portfw_ips[$host]=$ip
2676 vpn_ips[$host]=$ip
2677 else
2678 nonvpn_ips[$host]=$ip
2679 fi
2680 if $root; then
2681 # note: the reason we have b8.nz suffix here but not for non_root
2682 # hosts is that it is for the User part, the IdentityFile part is
2683 # redundant to *.b8.nz. Also note ${host}i, we only setup those for vpn hosts, but there is no harm in overspecifying here.
2684 root_hosts+=($host ${host}i $host.b8.nz ${host}i.b8.nz)
2685 # shellcheck disable=SC2004 # false positive
2686 root_hosts_a[$host]=t # a for associative array
2687 else
2688 nonroot_hosts+=($host ${host}i)
2689 fi
2690 host_ips[$host]=$ip
2691 if [[ $mac ]]; then
2692 host_macs[$host]=$mac
2693 fi
2694
2695 done </p/c/host-info
2696
2697 {
2698 cat <<EOF
2699 Host ${nonroot_hosts[@]}
2700 User iank
2701 IdentityFile ~/.ssh/home
2702
2703 Host ${root_hosts[@]}
2704 IdentityFile ~/.ssh/home
2705
2706 EOF
2707 for host in ${!vpn_ips[@]}; do
2708 ipsuf=${vpn_ips[$host]}
2709 cat <<EOF
2710 Host ${host}i ${host}i.b8.nz
2711 Port $((2200 + ipsuf))
2712 EOF
2713 done
2714
2715 # convenience of one auth key entry
2716 for host in ${!all_ips[@]}; do
2717 cat <<EOF
2718 Host $host ${host}i $host.b8.nz ${host}i.b8.nz
2719 HostKeyAlias $host.b8.nz
2720 EOF
2721 done
2722 } | cedit -e /p/c/subdir_files/.ssh/config-static
2723
2724 {
2725 # hack to please emacs parser
2726 here_begin="cat <<EOF"
2727 echo "$here_begin"
2728 for host in ${!vpn_ips[@]}; do
2729 ipsuf=${vpn_ips[$host]}
2730 i_port=$(( 2200 + ipsuf ))
2731 cat <<EOF
2732 config redirect
2733 option name ssh$host
2734 option src wan
2735 option src_dport $i_port
2736 option dest_port 22
2737 option dest_ip \$l.$ipsuf
2738 option dest lan
2739 config rule
2740 option src wan
2741 option target ACCEPT
2742 option dest_port $i_port
2743 EOF
2744 done
2745 echo "EOF"
2746 } >/p/c/cmc-firewall-data
2747
2748
2749 local host ipsuf f files
2750
2751 # shellcheck disable=SC2016 # shellcheck doesnt know this is sed
2752 sedi '/edits below here are made automatically/,$d' /p/c/machine_specific/li/filesystem/etc/wireguard/wgmail.conf
2753 for host in ${!vpn_ips[@]}; do
2754 if [[ ${root_hosts_a[$host]} ]]; then
2755 # root machines dont actually need vpn, but
2756 # the classification still helps with other
2757 # configurations.
2758 continue
2759 fi
2760 ipsuf=${vpn_ips[$host]}
2761 wghole $host $ipsuf
2762 u /b/ds/machine_specific/li/filesystem/etc/openvpn/client-config-hole/$host <<EOF
2763 ifconfig-push 10.5.5.${vpn_ips[$host]} 255.255.255.0
2764 EOF
2765 u /a/bin/ds/machine_specific/$host/filesystem/etc/systemd/system/openvpn-client-tr@.service <<EOF
2766 [Unit]
2767 Description=OpenVPN tunnel for %I
2768 After=syslog.target network-online.target
2769 Wants=network-online.target
2770 Documentation=man:openvpn(8)
2771 Documentation=https://community.openvpn.net/openvpn/wiki/Openvpn24ManPage
2772 Documentation=https://community.openvpn.net/openvpn/wiki/HOWTO
2773 Requires=iptables.service
2774
2775 [Service]
2776 Type=notify
2777 RuntimeDirectory=openvpn-client
2778 RuntimeDirectoryMode=0710
2779 WorkingDirectory=/etc/openvpn/client
2780 ExecStart=/usr/sbin/openvpn --suppress-timestamps --nobind --config /etc/openvpn/client/%i.conf
2781 # todo, try reenabling this from the default openvpn,
2782 # it was disabled so we could do bind mounts as a command,
2783 # but now systemd handles it
2784 #CapabilityBoundingSet=CAP_IPC_LOCK CAP_NET_ADMIN CAP_NET_RAW CAP_SETGID CAP_SETUID CAP_SYS_CHROOT CAP_DAC_OVERRIDE
2785 LimitNPROC=10
2786 # DeviceAllow=/dev/null rw
2787 # DeviceAllow=/dev/net/tun rw
2788
2789 # we use .1 to make this be on a different network than kd, so that we can
2790 # talk to transmission on kd from remote host, and still use this
2791 # vpn.
2792 ExecStartPre=/usr/bin/flock -w 20 /tmp/newns.flock /a/bin/newns/newns -n 10.174.$ipsuf start %i
2793 ExecStartPre=/sbin/iptables-restore /a/bin/distro-setup/transmission-firewall/netns.rules
2794 # allow wireguard network to connect
2795 ExecStartPre=/usr/sbin/ip r add 10.8.0.0/24 via 10.174.$ipsuf.1 dev veth1-client
2796 ExecStopPost=/usr/bin/flock -w 20 /tmp/newns.flock /a/bin/newns/newns stop %i
2797 PrivateNetwork=true
2798 BindReadOnlyPaths=/etc/tr-resolv:/run/systemd/resolve:norbind /etc/basic-nsswitch:/etc/resolved-nsswitch:norbind
2799
2800 [Install]
2801 WantedBy=multi-user.target
2802 EOF
2803 done
2804
2805 {
2806 echo "cat <<EOF"
2807 for host in ${!host_ips[@]}; do
2808 ipsuf=${host_ips[$host]}
2809 # shellcheck disable=SC2016 # intentional
2810 echo 'local-data-ptr: "$l.'$ipsuf $host.b8.nz'"'
2811 done
2812 echo "EOF"
2813 } | u /p/c/ptr-data
2814
2815 {
2816 echo "cat <<EOF"
2817 for host in ${!host_macs[@]}; do
2818 ipsuf=${host_ips[$host]}
2819 echo "dhcp-host=${host_macs[$host]},set:$host,\$l.$ipsuf,$host"
2820 done
2821 echo "EOF"
2822 } | u /p/c/dnsmasq-data
2823
2824
2825 b8_ip=$(dig +short b8.nz @iankelling.org | tail -1)
2826 # if our dynamic ip updates broke, set manually, eg:
2827 #b8_ip=72.74.193.xxx
2828 if [[ ! $b8_ip ]]; then
2829 echo "$0: error: got empty b8.nz ip. returning 1"
2830 return 1
2831 fi
2832 {
2833 cat <<EOF
2834 @ A $b8_ip
2835 i A $b8_ip
2836 EOF
2837 for host in ${!nonvpn_ips[@]}; do
2838 ipsuf=${nonvpn_ips[$host]}
2839 echo "$host A 10.2.0.$ipsuf"
2840 done
2841 for host in ${!vpn_ips[@]}; do
2842 ipsuf=${vpn_ips[$host]}
2843 cat <<EOF
2844 $host A 10.2.0.$ipsuf
2845 ${host}wg A 10.8.0.$ipsuf
2846 ${host}vp A 10.5.5.$ipsuf
2847 ${host}tr A 10.174.$ipsuf.2
2848 ${host}i CNAME i.b8.nz.
2849 EOF
2850 done
2851 } | cedit -e vpn-ips-update /p/c/machine_specific/vps/bind-initial/db.b8.nz
2852
2853
2854 echo checking for stray files:
2855
2856 initial_dir="$PWD"
2857 while read -r dir path; do
2858 cd $dir
2859 ngset
2860 files=( */$path )
2861 ngreset
2862 cd "$initial_dir"
2863 for f in "${files[@]}"; do
2864 host=${f%%/*}
2865 if [[ ! ${vpn_ips[$host]} ]]; then
2866 e rm $dir/$f
2867 fi
2868 done
2869 done <<'EOF'
2870 /a/bin/ds/machine_specific filesystem/etc/systemd/system/openvpn-client-tr@.service
2871 /p/c/machine_specific filesystem/etc/wireguard/wghole.conf
2872 EOF
2873
2874 files=( /b/ds/machine_specific/li/filesystem/etc/openvpn/client-config-hole/* )
2875 for f in "${files[@]}"; do
2876 host=${f##*/}
2877 if [[ ! ${vpn_ips[$host]} ]]; then
2878 e rm $f
2879 e ssh root@li.b8.nz rm -f $f
2880 fi
2881 done
2882
2883 tmpf=$(mktemp)
2884 {
2885 printf "%s" "Host * "
2886 sed -n '/^Host /h;/^IdentityFile .*\/home/{g;s/^Host//;s/ / !/gp}' /p/c/subdir_files/.ssh/config-static | tr '\n' ' ' \
2887 | sed -r 's/ *$/\n/'
2888 echo "IdentityFile ~/.ssh/work"
2889 } >$tmpf
2890 cedit -e work-identity /p/c/subdir_files/.ssh/config-static <$tmpf
2891 rm -f $tmpf
2892
2893 ### begin focus on hosts file update ###
2894 #
2895 # This started as its own function, but it actually
2896 # needed to alter the ssh config, so combined it.
2897 #
2898 # background: This is finally doing dynamic ip resolution via the hosts
2899 # file. I considered detecting where each host was dynamically or
2900 # something, but ultimately decided to mostly avoid that, other than
2901 # detecting the status of the current machine I'm on. I want to be able
2902 # to move it around without having to manually type much of anything.
2903 local -a host_domain_suffix hosts
2904 local -A ip_to_hosts
2905 local suf ip i host at_home suf_from_here
2906
2907 source /p/c/domain-info
2908
2909 at_home=false
2910 if ip n | grep -q "10.2.0.1 .* b4:75:0e:fd:06:4a"; then
2911 at_home=true
2912 fi
2913
2914 for i in ${host_domain_suffix[@]}; do
2915 if [[ $i == *.* ]]; then
2916 suf=$i
2917 continue
2918 fi
2919 hosts+=($i)
2920 if [[ $i == "$HOSTNAME" ]]; then
2921 unset "portfw_ips[$i]"
2922 continue
2923 fi
2924
2925 suf_from_here=$suf
2926 if ! $at_home && [[ $suf == .b8.nz || $suf == [wc].b8.nz ]]; then
2927 suf_from_here=i.b8.nz
2928 else
2929 unset "portfw_ips[$i]"
2930 fi
2931
2932 # note this might be outdated until we do a dns push
2933 ip=$(dig +short "$i$suf_from_here" @iankelling.org | tail -n1) ||:
2934 if [[ ! $ip ]]; then
2935 if [[ $suf == .office.fsf.org ]]; then
2936 suf_from_here=wg.b8.nz
2937 ip=$(getent ahostsv4 "$i$suf_from_here" | awk '{ print $1 }' | head -n1) ||:
2938 fi
2939 fi
2940 if [[ $ip ]]; then
2941 ip_to_hosts[$ip]+=" $i"
2942 else
2943 echo error: failed to get ip of "$i$suf_from_here"
2944 fi
2945 done
2946
2947 for ip in "${!ip_to_hosts[@]}"; do
2948 echo "$ip${ip_to_hosts[$ip]}"
2949 done | s cedit -e hosts-file-up /etc/hosts
2950 for host in ${hosts[@]}; do
2951 echo $host
2952 done >/p/c/subdir_files/.dsh/group/btrbk
2953 ### end focus on hosts file update ###
2954
2955
2956 # note: note sure if this is a great way to check.
2957 # todo: think about it
2958
2959 if $at_home; then
2960 # possible that in the future we want to create
2961 # a dynamic file here, and then we can move the cat
2962 # command above out of the conditional
2963 rsync -a /p/c/subdir_files/.ssh/config-static ~/.ssh/config
2964 else
2965 for host in ${!portfw_ips[@]}; do
2966 ipsuf=${portfw_ips[$host]}
2967 cat <<EOF
2968 Host ${host}
2969 Port $((2200 + ipsuf))
2970 EOF
2971 done > ~/.ssh/config-dynamic
2972 cat /p/c/subdir_files/.ssh/config-static ~/.ssh/config-dynamic >~/.ssh/config
2973 fi
2974 }
2975
2976 # usage host ipsuf [extrahost]
2977 #
2978 # If the keys already exist and you want new ones, remove them:
2979 # rm /p/c/machine_specific/$host/filesystem/etc/wireguard/hole-{priv,pub}.key
2980 #
2981 # extrahost is a host/cidr that is allowed to go be routed through the
2982 # vpn by this host.
2983 wghole() {
2984 if (( $# < 2 || $# > 3 )); then
2985 e expected 2-3 arg of hostname, ip suffix, and extrahost >&2
2986 return 1
2987 fi
2988 local host ipsuf umask_orig vpn_allowed
2989 host=$1
2990 ipsuf=$2
2991 if [[ $3 ]]; then
2992 extrahost=,$3
2993 fi
2994 for vpn_host in ${!vpn_ips[@]}; do
2995 if [[ $vpn_host == "$host" ]]; then
2996 continue
2997 fi
2998 vpn_allowed+=",10.174.${vpn_ips[$vpn_host]}.2/32"
2999 done
3000 mkdir -p /p/c/machine_specific/$host/filesystem/etc/wireguard
3001 (
3002 cd /p/c/machine_specific/$host/filesystem/etc/wireguard
3003 umask_orig=$(umask)
3004 umask 0077
3005 if [[ ! -s hole-priv.key || ! -s hole-pub.key ]]; then
3006 wg genkey | tee hole-priv.key | wg pubkey > hole-pub.key
3007 fi
3008 cat >wghole.conf <<EOF
3009 [Interface]
3010 # contents hole-priv.key
3011 PrivateKey = $(cat hole-priv.key)
3012 ListenPort = 1194
3013 Address = 10.8.0.$ipsuf/24
3014 # https://dev.to/tangramvision/what-they-don-t-tell-you-about-setting-up-a-wireguard-vpn-1h2g
3015 # ||: makes the systemd service not fail due to the failed command
3016 PostUp = ping -w10 -c1 10.8.0.1 ||:
3017
3018 [Peer]
3019 # li. called wgmail on that server
3020 PublicKey = CTFsje45qLAU44AbX71Vo+xFJ6rt7Cu6+vdMGyWjBjU=
3021 AllowedIPs = 10.8.0.0/24$vpn_allowed$extrahost
3022 Endpoint = 72.14.176.105:1194
3023 PersistentKeepalive = 25
3024 EOF
3025 umask $umask_orig
3026 # old approach. systemd seems to work fine and cleaner.
3027 rm -f ../network/interfaces.d/wghole
3028 cedit -q $host /p/c/machine_specific/li/filesystem/etc/wireguard/wgmail.conf <<EOF || [[ $? == 1 ]]
3029 [Peer]
3030 PublicKey = $(cat hole-pub.key)
3031 AllowedIPs = 10.8.0.$ipsuf/32,10.174.${vpn_ips[$host]}.2/32
3032 EOF
3033 )
3034 }
3035
3036 # sudo maybe
3037 #
3038 # passes on any initial -* args to sudo.
3039 sudm() {
3040 local arg
3041 local -a sudo_opts
3042 for arg; do
3043 if [[ $arg == -* ]]; then
3044 sudo_opts+=("$arg")
3045 shift
3046 else
3047 break
3048 fi
3049 done
3050 if [[ $EUID == 0 ]]; then
3051 "$@"
3052 else
3053 sudo "${sudo_opts[@]}" "$@"
3054 fi
3055 }
3056
3057 mns-setup() {
3058 local ns
3059 ns=$1
3060 sudm mkdir -p /root/mount_namespaces
3061 if ! sudm mountpoint /root/mount_namespaces >/dev/null; then
3062 m sudm mount --bind /root/mount_namespaces /root/mount_namespaces
3063 fi
3064 m sudm mount --make-private /root/mount_namespaces
3065 if ! sudm test -e /root/mount_namespaces/$ns; then
3066 m sudm touch /root/mount_namespaces/$ns
3067 fi
3068 if ! sudm mountpoint /root/mount_namespaces/$ns >/dev/null; then
3069 m sudm unshare --propagation slave --mount=/root/mount_namespaces/$ns /bin/true
3070 fi
3071
3072 }
3073
3074 mns() { # mount namespace
3075 local ns
3076 ns=$1
3077 shift
3078 mns-setup $ns
3079 m sudm -E /usr/bin/nsenter --mount=/root/mount_namespaces/$ns "$@"
3080 }
3081
3082 mnsd() { # mount namespace + systemd namespace
3083 local ns unit
3084 ns=$1
3085 unit=$2
3086 shift 2
3087
3088 mns-setup $ns
3089
3090 pid=$(servicepid $unit)
3091 tmpf=$(mktemp --tmpdir $unit.XXXXXXXXXX)
3092 export -p >$tmpf
3093 printf "%s " "${@@Q}" >>$tmpf
3094 echo >>$tmpf
3095
3096 m sudo nsenter -t $pid -n --mount=/root/mount_namespaces/$ns sudo -u $USER -i bash -c ". $tmpf & sleep 1; rm $tmpf"
3097 }
3098
3099
3100 mnsr() { # mns run
3101 local ns=$1
3102 shift
3103 mns $ns sudo -u iank -E env "PATH=$PATH" "$@"
3104 }
3105
3106 mnsnonetr() {
3107 ns=$1
3108 lomh
3109 if ! s ip netns list | grep -Fx nonet &>/dev/null; then
3110 s ip netns add nonet
3111 fi
3112 mns $ns --net=/var/run/netns/nonet /bin/bash
3113 lomh
3114 }
3115
3116 mnsnonet() {
3117 ns=$1
3118 lomh
3119 if ! s ip netns list | grep -Fx nonet &>/dev/null; then
3120 s ip netns add nonet
3121 fi
3122 mns $ns --net=/var/run/netns/nonet sudo -E -u iank /bin/bash
3123 lomh
3124 }
3125
3126
3127 lom() {
3128 # l = the loopback device
3129 local l base
3130 # get sudo pass cached right away
3131 if ! sudo -nv 2>/dev/null; then
3132 sudo -v
3133 fi
3134 if [[ $1 == /* ]]; then
3135 base=${1##*/}
3136 fs_file=$1
3137 if mns $base mountpoint -q /mnt/$base; then
3138 return 0
3139 fi
3140 l=$(losetup -j $fs_file | sed -rn 's/^([^ ]+): .*/\1/p' | head -n1 ||:)
3141 if [[ ! $l ]]; then
3142 l=$(sudo losetup -f)
3143 m sudo losetup $l $fs_file
3144 fi
3145 if ! sudo cryptsetup status /dev/mapper/$base &>/dev/null; then
3146 if ! m sudo cryptsetup luksOpen $l $base; then
3147 m sudo losetup -d $l
3148 return 1
3149 fi
3150 fi
3151 m sudo mkdir -p /mnt/$base
3152 m mns $base mount /dev/mapper/$base /mnt/$base
3153 m mns $base chown $USER:$USER /mnt/$base
3154 lomh
3155 else
3156 base=$1
3157 if mns $base mountpoint /mnt/$base &>/dev/null; then
3158 m mns $base umount /mnt/$base
3159 fi
3160 if sudo cryptsetup status /dev/mapper/$base &>/dev/null; then
3161 if ! m sudo cryptsetup luksClose /dev/mapper/$base; then
3162 echo lom: failed cryptsetup luksClose /dev/mapper/$base
3163 return 1
3164 fi
3165 fi
3166 l=$(losetup -l --noheadings | awk '$6 ~ /\/'$base'$/ {print $1}')
3167 if [[ $l ]]; then
3168 m sudo losetup -d $l
3169 else
3170 echo lom: warning: no loopback device found
3171 fi
3172 fi
3173 }
3174
3175 # mu personality. for original, just run mp. for 2, run mp 2.
3176 # this is partly duplicated in mail-setup
3177 mp() {
3178 local dead=false
3179 for s in {1..5}; do
3180 if ! killall mu; then
3181 dead=true
3182 break
3183 fi
3184 sleep 1
3185 done
3186 if ! $dead; then
3187 echo error: mu not dead
3188 m psg mu
3189 return 1
3190 fi
3191 suf=$1
3192 set -- /m/mucache ~/.cache/mu /m/.mu ~/.config/mu
3193 while (($#)); do
3194 target=$1$suf
3195 f=$2
3196 shift 2
3197 if [[ -e $f && ! -L $f ]]; then
3198 m rm -rf $f
3199 fi
3200 m ln -sf -T $target $f
3201 done
3202 }
3203
3204 # maildir enable
3205 mdenable() {
3206 local md dst ln_path src two
3207
3208 two=false
3209 case $1 in
3210 -2) two=true; shift ;;
3211 esac
3212
3213 for md; do
3214 src=
3215 if $two; then
3216 dst=/m/4e2/$md
3217 else
3218 dst=/m/4e/$md
3219 fi
3220
3221 ln_path=/m/md/$md
3222 for d in /m/md/$md /m/4e2/$md; do
3223 if [[ -d $d && ! -L $d ]]; then
3224 src=$d
3225 break
3226 fi
3227 done
3228 if [[ ! $src ]]; then
3229 echo "error: could not find $md" >&2
3230 return 1
3231 fi
3232 m mv -T $src $dst
3233 m ln -sf -T $dst $ln_path
3234 done
3235 }
3236 md2enable() {
3237 mdenable -2 "$@"
3238 }
3239 mddisable() {
3240 local md=$1
3241 dst=/m/md/$md
3242
3243 ### begin copied from mdenable, but different d ###
3244 for d in /m/4e/$md /m/4e2/$md; do
3245 if [[ -d $d && ! -L $d ]]; then
3246 src=$d
3247 break
3248 fi
3249 done
3250 if [[ ! $src ]]; then
3251 echo "error: could not find $md" >&2
3252 return 1
3253 fi
3254 ### end copy from mdenable ###
3255
3256 if [[ -L $dst ]]; then m rm $dst; fi
3257 m mv -T $src $dst
3258 }
3259
3260
3261 mdt() {
3262 markdown "$1" >/tmp/mdtest.html
3263 firefox /tmp/mdtest.html
3264 }
3265
3266 mo() { xset dpms force off; } # monitor off
3267
3268 mpvgpu() {
3269 # seems to be the best gpu decoding on my nvidia 670.
3270 # vlc gets similar or better framerate, but is much darker output on my test movie at least.
3271
3272
3273 case $HOSTNAME in
3274 kd)
3275 echo 0f | sudo tee -a /sys/kernel/debug/dri/0/pstate
3276 ;;
3277 esac
3278 # going back to the default slow clock, and slower fan:
3279 # echo 07 | sudo tee -a /sys/kernel/debug/dri/0/pstate
3280 if [[ $DISPLAY ]]; then
3281 mpv --vo=vdpau --hwdec=auto "$@"
3282 else
3283 # waylandvk seems to work the same
3284 mpv --gpu-context=wayland --hwdec=auto
3285 fi
3286 }
3287
3288 mpvd() {
3289 mpv --profile=d "$@";
3290 }
3291 mpva() {
3292 mpv --profile=a "$@";
3293 }
3294 # mpv for testing video quality, dont scale.
3295 mpvt() {
3296 mpv --video-unscaled "$@";
3297 }
3298
3299 # mpv all media files in . or $1
3300 mpvm() {
3301 local -a extensions arg
3302 # get page source of https://en.wikipedia.org/w/index.php?title=Video_file_format&action=edit
3303 # into /a/x.log, then
3304 # grep '^| *\.' /a/x.log | sed 's/| *//;s/,//g'
3305
3306 # note: to join them together for a regex, do:
3307 # old=; for e in ${extensions[@]/./}; do if [[ ! $old ]]; then old=$e; continue; fi; echo -n "$old|"; old=$e; done; echo $e
3308 extensions=(
3309 .webm
3310 .mkv
3311 .flv
3312 .flv
3313 .vob
3314 .ogv .ogg
3315 .drc
3316 .gif
3317 .gifv
3318 .mng
3319 .avi
3320 .MTS .M2TS .TS
3321 .mov .qt
3322 .wmv
3323 .yuv
3324 .rm
3325 .rmvb
3326 .viv
3327 .asf
3328 .amv
3329 .mp4 .m4p .m4v
3330 .mpg .mp2 .mpeg .mpe .mpv
3331 .mpg .mpeg .m2v
3332 .m4v
3333 .svi
3334 .3gp
3335 .3g2
3336 .mxf
3337 .roq
3338 .nsv
3339 )
3340 arg=("(" -iname "*${extensions[0]}")
3341 for (( i=1 ; i < ${#extensions[@]}; i++ )); do
3342 arg+=(-o -iname "*${extensions[i]}")
3343 done
3344 arg+=(")")
3345 dir=${1:-.}
3346 # debug:
3347 #find $dir "${arg[@]}" -size +200k
3348 find $dir "${arg[@]}" -size +200k -exec mpv --profile=d '{}' +
3349 }
3350 mpvs() {
3351 mpv --profile=s "$@";
3352 }
3353
3354 myirc() {
3355 if [[ ! $1 ]]; then
3356 set -- fsfsys
3357 fi
3358 local -a d
3359 d=( /var/lib/znc/moddata/log/iank/{freenode,libera} )
3360 # use * instead of -r since that does sorted order
3361 ssh root@iankelling.org "for f in ${d[*]}; do cd \$f/#$1; grep '\<iank.*' *; done" | cut --complement -c12-16
3362 }
3363
3364
3365 allmyirc() {
3366 local d
3367 d=/var/lib/znc/moddata/log/iank/freenode
3368 ssh root@iankelling.org "cd $d; find . -mtime -60 -type f -exec grep '\<iank.*' {} +" | sed -r 's,^..([^/]*)/(.{11})(.{5})(.{8}).,\2\4 \1,' | sort
3369 }
3370
3371 # The way pidgin logs with xmpp (maybe related to running cheogram too)
3372 # is that there are sometimes duplicates, and sometimes the a log file
3373 # is for a specific day yet logs messages for subsequent days, and the
3374 # only way to realize that is to notice that the timestamps rolled over
3375 # into a new day, you can't see it in isolation. So, basically, pidgin
3376 # logs are really annoying to read a grep of my messages to find the
3377 # date and time I said when I started and stopped working, so I'm trying
3378 # out a new client: profanity.
3379 mypidgin() {
3380 c /p/c/.purple/logs/jabber/iank@fsf.org/office@conference.fsf.org.chat
3381 for x in *.html; do html2text -o ${x%.html}.txt $x; done;
3382 # shellcheck disable=SC2016 # false positive on ${
3383 grep -A1 ') iank:' ./*.txt \
3384 | sed -r 's/^(.{10})[^ ]*\.txt:\(?([^ ]*)[[:space:]](..). iank:/\1_\2_\3/
3385 s/^[^ ]*\.txt-//
3386 /^--$/d
3387 s/^[^ ]*\.txt:\((.{2}).(.{2}).(.{4}) (.{8}) (.{2})\)?/\3-\1-\2_\4_\5/' \
3388 | sed -n 'x;1d;0~2{G;s/\n/ /;p};${x;p}'
3389 }
3390
3391 # my profanity
3392 #
3393 myprof() {
3394 pushd /home/iank/.local/share/profanity/chatlogs/iank_at_fsf.org/rooms/office_at_conference.fsf.org
3395 logs=(*)
3396 logcount=${#logs[@]}
3397 if (( logcount > 16 )); then
3398 i=$(( logcount - 16 ))
3399 else
3400 i=0
3401 fi
3402 # usually do this on monday, sometimes later
3403 if [[ $(date +%A) == Monday ]]; then
3404 min_date=$(date -d 'monday 2 weeks ago' +%s)
3405 else
3406 min_date=$(date -d 'monday 3 weeks ago' +%s)
3407 fi
3408 for (( ; i < logcount; i++ )); do
3409 log=${logs[$i]}
3410 d=$(date -d "$(head -n1 $log|awk '{print $1}')" +%s)
3411 if (( d < min_date )); then
3412 continue
3413 fi
3414 if awk '$3 == "iank:"' $log | sed -r 's/^(.{10}).(.{8})[^ ]+(.*)/\1_\2\3/' | grep .; then
3415 hr
3416 fi
3417 done
3418 popd
3419 }
3420
3421
3422 # Tail all recent prof logs. Copying from profanity has unwanted line breaks
3423 # especially for links.
3424 profr() {
3425 case $HOSTNAME in
3426 kd)
3427 profr-local
3428 ;;
3429 *)
3430 ssh b8.nz profr-local
3431 ;;
3432 esac
3433 }
3434
3435 profr-local() {
3436 local d0 d1
3437 local -a files
3438 d0="$(date +%Y_%m_%d).log"
3439 d1="$(date -d '1 day ago' +%Y_%m_%d).log"
3440 ngset
3441 files=(/d/p/profanity/chatlogs/iank_at_fsf.org/{*,rooms/*}/{$d0,$d1})
3442 ngreset
3443 if (( ${#files[@]} > 0 )); then
3444 cat "${files[@]}" | sort | tail -n 40
3445 fi
3446 }
3447
3448
3449 # Tail pms in the last day, for the case where we restart profanity and
3450 # didn't check for pms beforehand. Assume the most recent logs are on kd.
3451 # If that isn't the case, use prof-recent-local
3452 prof-recent() {
3453 case $HOSTNAME in
3454 kd)
3455 prof-recent-local
3456 ;;
3457 *)
3458 ssh b8.nz prof-recent-local
3459 ;;
3460 esac
3461 }
3462 prof-recent-local() {
3463 local d dates date files f
3464 # consider making the day count passed by parameter. note: this works: $(date -d '2 day ago' +%Y_%m_%d)
3465 dates=("$(date +%Y_%m_%d)" "$(date -d '1 day ago' +%Y_%m_%d)" )
3466 for d in /d/p/profanity/chatlogs/iank_at_fsf.org/!(rooms); do
3467 files=()
3468 for date in ${dates[@]}; do
3469 f=$d/$date.log
3470 if [[ -e $f ]]; then
3471 files+=($f)
3472 fi
3473 done
3474 if (( ${#files[@]} >= 1 )); then
3475 cat ${files[@]} | tail
3476 hr
3477 fi
3478 done
3479 }
3480
3481 prof-sort() {
3482 case $HOSTNAME in
3483 kd)
3484 prof-recent-sort
3485 ;;
3486 *)
3487 ssh b8.nz prof-recent-sort
3488 ;;
3489 esac
3490 }
3491
3492 prof-recent-sort() {
3493 local d dates date files f
3494 # consider making the day count passed by parameter. note: this works: $(date -d '2 day ago' +%Y_%m_%d)
3495 dates=("$(date +%Y_%m_%d)" "$(date -d '1 day ago' +%Y_%m_%d)" )
3496 files=()
3497 for d in /d/p/profanity/chatlogs/iank_at_fsf.org/!(rooms); do
3498 for date in ${dates[@]}; do
3499 f=$d/$date.log
3500 if [[ -e $f ]]; then
3501 files+=($f)
3502 fi
3503 done
3504 done
3505 for f in "${files[@]}"; do
3506 sed "s/\$/ $f/" $f
3507 done | sort
3508 }
3509
3510
3511 # usage: debvm DEBIAN_VERSION RAM_MB
3512 debvm() {
3513 local ver ram fname src
3514 ver=$1
3515 ram=${2:-2024}
3516 # * is because it might have -backports in the name. we only expect 1 expansion
3517 fnames=( debian-$ver-*nocloud-"$(dpkg --print-architecture)".qcow2 )
3518 if (( ${#fnames[@]} >= 2 )); then
3519 echo "error: iank: unexpected multiple files"
3520 return 1
3521 fi
3522 fname="${fnames[0]}"
3523 src=/a/opt/roms/$fname
3524 if [[ ! -f $src ]]; then
3525 echo debvm: not found $src, download from eg: https://cloud.debian.org/images/cloud/buster/latest/
3526 return 1
3527 fi
3528 cp -a $src /t
3529 # note, in fai-revm we do this: not sure why, maybe because of br device
3530 # --graphics spice,listen=0.0.0.0
3531 m s virt-install --osinfo debian11 --rng /dev/urandom -n deb${ver}tmp --import -r $ram --vcpus 2 --disk /t/$fname --graphics spice
3532 # note: to ssh into this machine will require host key generation: ssh-keygen -A
3533
3534 # random: for cvs2git on gnu www, use debian 10. I could use trisquel,
3535 # but happen to want to try out the debian cloud images. the upstream
3536 # requires python2 and hasn't really changed since the version in d10.
3537 #
3538 # apt install cvs2git cvs
3539 # # 7G was not enough
3540 # mount -o mode=1777,nosuid,nodev,size=34G -t tmpfs tmpfs /tmp
3541 # cvs2git --encoding utf_8 --fallback-encoding ascii --dumpfile=dump www-rsync/www |& tee /tmp/l
3542 ## www-rsync is an rsynced copy of the cvsfrom savannah
3543 }
3544
3545 mygajim() {
3546 local time time_sec time_pretty days
3547 days=${1:-16}
3548 sqlite3 -separator ' ' /p/c/subdir_files/.local/share/gajim/logs.db "select time, message from logs where contact_name = 'iank' and jid_id = 17;" | while read -r time l; do
3549 case $time in
3550 16*) : ;;
3551 *) continue ;;
3552 esac
3553 if ! time_pretty=$(date +%F.%R -d @$time); then
3554 echo bad time: $time
3555 return 1
3556 fi
3557 echo $time_pretty "$l"
3558 time_sec=${time%%.*}
3559 # only look at the last 18 days. generally just use this for timesheet.
3560 if (( time_sec < EPOCHSECONDS - 60 * 60 * 24 * days )); then break; fi
3561 done
3562 }
3563
3564 allmygajim() {
3565 sqlite3 -separator ' ' /p/c/subdir_files/.local/share/gajim/logs.db "select time, message from logs where contact_name = 'iank'" | less
3566 }
3567
3568 gajlogs() {
3569 sqlite3 -separator ' ' /p/c/subdir_files/.local/share/gajim/logs.db "select time, message from logs" | less
3570 }
3571
3572
3573 net-dev-info() {
3574 e "lspci -nnk|gr -iA2 net"
3575 lspci -nnk|gr -iA2 net
3576 hr
3577 e "s lshw -C network"
3578 hr
3579 sudo lshw -C network
3580 }
3581
3582 nk() {
3583 ser stop NetworkManager
3584 ser disable NetworkManager
3585 ser stop NetworkManager-wait-online.service
3586 ser disable NetworkManager-wait-online.service
3587 ser stop dnsmasq
3588 sudo resolvconf -d NetworkManager
3589 # ser start dnsmasq
3590 sudo ifup br0
3591 }
3592 ngo() {
3593 sudo ifdown br0
3594 ser start NetworkManager
3595 sleep 4
3596 sudo nmtui-connect
3597 }
3598
3599 otp() {
3600 oathtool --totp -b "$*" | xclip -selection clipboard
3601 }
3602 # run cmd and copy output
3603 j() {
3604 "$@" |& pee "xclip -r -selection clipboard" cat
3605 }
3606
3607 # xorg copy. copy text piped into command
3608 xc() {
3609 xclip -r -selection clipboard
3610 }
3611 # echo copy
3612 ec() {
3613 pee "xclip -r -selection clipboard" cat
3614 }
3615
3616 pakaraoke() {
3617 # from http://askubuntu.com/questions/456021/remove-vocals-from-mp3-and-get-only-instrumentals
3618 pactl load-module module-ladspa-sink sink_name=Karaoke master=alsa_output.usb-Audioengine_Audioengine_D1-00.analog-stereo plugin=karaoke_1409 label=karaoke control=-30
3619 }
3620
3621 pfind() { #find *$1* in $PATH
3622 [[ $# != 1 ]] && { echo requires 1 argument; return 1; }
3623 local pathArray
3624 IFS=: pathArray=($PATH); unset IFS
3625 find "${pathArray[@]}" -iname "*$1*"
3626 }
3627
3628 pick-trash() {
3629 # trash-restore lists everything that has been trashed at or below CWD
3630 # This picks out files just in CWD, not subdirectories,
3631 # which also match grep $1, usually use $1 for a time string
3632 # which you get from running restore-trash once first
3633 local name x ask
3634 local nth=1
3635 # last condition is to not ask again for ones we skipped
3636 while name="$( echo | restore-trash | gr "$PWD/[^/]\+$" | gr "$1" )" \
3637 && [[ $name ]] && (( $(wc -l <<<"$name") >= nth )); do
3638 name="$(echo "$name" | head -n $nth | tail -n 1 )"
3639 read -r -p "$name [Y/n] " ask
3640 if [[ ! $ask || $ask == [Yy] ]]; then
3641 x=$( echo "$name" | gr -o "^\s*[0-9]*" )
3642 echo $x | restore-trash > /dev/null
3643 elif [[ $ask == [Nn] ]]; then
3644 nth=$((nth+1))
3645 else
3646 return
3647 fi
3648 done
3649 }
3650
3651
3652 pub() {
3653 rld /a/h/_site/ li:/var/www/iankelling.org/html
3654 }
3655
3656
3657 pumpa() {
3658 # fixes the menu bar in xmonad. this won\'t be needed when xmonad
3659 # packages catches up on some changes in future (this is written in
3660 # 4/2017)
3661 #
3662 # geekosaur: so youll want to upgrade to xmonad 0.13 or else use a
3663 # locally modified XMonad.Hooks.ManageDocks that doesnt set the
3664 # work area; turns out it\'s impossible to set correctly if you are
3665 # not a fully EWMH compliant desktop environment
3666 #
3667 # geekosaur: chrome shows one failure mode, qt/kde another, other
3668 # gtk apps a third, ... I came up with a setting that works for me
3669 # locally but apparently doesnt work for others, so we joined the
3670 # other tiling window managers in giving up on setting it at all
3671 #
3672 xprop -root -remove _NET_WORKAREA
3673 command pumpa & r
3674 }
3675
3676 # reviewboard, used at my old job
3677 #rbpipe() { rbt post -o --diff-filename=- "$@"; }
3678 #rbp() { rbt post -o "$@"; }
3679
3680 rebr() {
3681 sudo ifdown br0
3682 sudo ifup br0
3683 }
3684
3685
3686 r2e() { command r2e -d /p/c/rss2email.json -c /p/c/rss2email.cfg "$@"; }
3687 # only run on MAIL_HOST. simpler to keep this on one system.
3688 r2eadd() { # usage: name url
3689 # initial setup of rss2email:
3690 # r2e new r2e@iankelling.org
3691 # that initializes files, and sets default email.
3692 # symlink to the config doesnt work, so I copied it to /p/c
3693 # and then use cli option to specify explicit path.
3694 # Only option changed from default config is to set
3695 # force-from = True
3696 #
3697 # or else for a few feeds, the from address is set by the feed, and
3698 # if I fail delivery, then I send a bounce message to that from
3699 # address, which makes me be a spammer.
3700
3701 r2e add $1 "$2" $1@r2e.iankelling.org
3702 # get up to date and dont send old entries now:
3703 r2e run --no-send $1
3704 }
3705
3706 rspicy() { # usage: HOST DOMAIN
3707 # connect to spice vm remote host. use vspicy for local host
3708 local port
3709 # shellcheck disable=SC2087
3710 port=$(ssh $1<<EOF
3711 sudo virsh dumpxml $2|grep "<graphics.*type='spice'" | \
3712 sed -rn "s/.*port='([0-9]+).*/\1/p"
3713 EOF
3714 )
3715 if [[ $port ]]; then
3716 spicy -h $1 -p $port
3717 else
3718 echo "error: no port found. check that the domain is running."
3719 fi
3720 }
3721
3722
3723 scssl() {
3724 # s gem install scss-lint
3725 pushd /a/opt/thoughtbot-guides
3726 git pull --stat
3727 popd
3728 scss-lint -c /a/opt/thoughtbot-guides/style/sass/.scss-lint.yml "$@"
3729 }
3730
3731 skbrc() {
3732 sk -e 2120,245 /b/ds/brc /b/ds/brc2
3733 }
3734
3735 skaraoke() {
3736 local tmp out
3737 out=${2:-${1%.*}.sh}
3738 tmp=$(mktemp -d)
3739 script -t -c "mpv --no-config --no-resume-playback --no-terminal --no-audio-display '$1'" $tmp/typescript 2>$tmp/timing
3740 # todo, the current sleep seems pretty good, but it
3741 # would be nice to have an empirical measurement, or
3742 # some better wait to sync up.
3743 #
3744 # note: --loop-file=no prevents it from hanging if you have that
3745 # set to inf the mpv config.
3746 # --loop=no prevents it from exit code 3 due to stdin if you
3747 # had it set to inf in mpv config.
3748 #
3749 # args go to mpv, for example --volume=80, 50%
3750 cat >$out <<EOFOUTER
3751 #!/bin/bash
3752 trap "trap - TERM && kill 0" INT TERM ERR; set -e
3753 ( sleep .2; scriptreplay <( cat <<'EOF'
3754 $(cat $tmp/timing)
3755 EOF
3756 ) <( cat <<'EOF'
3757 $(cat $tmp/typescript)
3758 EOF
3759 ))&
3760 base64 -d - <<'EOF'| mpv --loop=no --loop-file=no --no-terminal --no-audio-display "\$@" -
3761 $(base64 "$1")
3762 EOF
3763 kill 0
3764 EOFOUTER
3765 rm -r $tmp
3766 chmod +x $out
3767 }
3768
3769 # ssh meld. usage: host1 host2 file
3770 smeld() {
3771 meld <(ssh $1 cat $3) <(ssh $2 cat $3)
3772 }
3773
3774 # remote file meld
3775 # usage: host file1 file2
3776 rmeld() {
3777 local tmpdir
3778 tmpdir=$(mktemp -d)
3779 scp "$1:$2" "$1:$3" $tmpdir
3780 meld "$tmpdir/${2##*/}" "$tmpdir/${3##*/}"
3781 }
3782
3783
3784 spd() {
3785 PATH=/usr/local/spdhackfix:$PATH command spd "$@"
3786 }
3787
3788 spamf() { # spamtest on FILE
3789 if (( $# != 1 )); then
3790 e spamtest error: expected 1 arg, filename >&2
3791 return 1
3792 fi
3793 sdncmdroot spamassassin sudo -u Debian-exim spamassassin -t --cf='score PYZOR_CHECK 0' <"$1"
3794 }
3795
3796
3797 # mail related
3798 testmail() {
3799 declare -gi _seq; _seq+=1
3800 echo "test body" | m mail -s "test mail from $HOSTNAME, $_seq" "${@:-root@localhost}"
3801 # for testing to send from an external address, you can do for example
3802 # -fian@iank.bid -aFrom:ian@iank.bid web-6fnbs@mail-tester.com
3803 # note in exim, you can retry a deferred message
3804 # s exim -M MSG_ID
3805 # MSG_ID is in /var/log/exim4/mainlog, looks like 1ccdnD-0001nh-EN
3806 }
3807
3808 # to test sieve, use below command. for fsf mail, see offlineimap-sync script
3809 # make modifications, then copy to live file, use -eW to actually modify mailbox
3810 #
3811 # Another option is to use sieve-test SCRIPT MAIL_FILE. note,
3812 # sieve-test doesnt know about envelopes, Im not sure if sieve-filter does.
3813
3814 # sieve with output filter. arg is mailbox, like INBOX.
3815 # This depends on dovecot conf, notably mail_location in /etc/dovecot/conf.d/10-mail.conf
3816
3817 # always run this first, edit the test files, then run the following
3818 testsieve() {
3819 sieve-filter ~/sieve/maintest.sieve ${1:-INBOX} delete 2> >(head; tail) >/tmp/testsieve.log && sed -rn '/^Performed actions:/,/^[^ ]/{/^ /p}' /tmp/testsieve.log | sort | uniq -c
3820 }
3821 runsieve() {
3822 c ~/sieve; cp personal{test,}.sieve; cp lists{test,}.sieve; cp personalend{test,}.sieve
3823 sieve-filter -eWv ~/sieve/maintest.sieve ${1:-INBOX} delete &> /tmp/testsieve.log
3824 sed -r '/^info: filtering:/{h;d};/^info: msgid=$/N;/^info: msgid=.*left message in mailbox [^ ]+$/d;/^info: msgid=/{H;g};/^info: message kept in source mailbox.$/d' /tmp/testsieve.log
3825 }
3826
3827 # usage:
3828 # alertme SUBJECT
3829 # printf "subject\nbody\n" | alertme
3830 alertme() {
3831 if [[ -t 0 ]]; then
3832 exim -t <<EOF
3833 From: alertme@b8.nz
3834 To: alerts@iankelling.org
3835 Subject: $*
3836 EOF
3837 else
3838 read -r sub
3839 { cat <<EOF
3840 From: alertme@b8.nz
3841 To: alerts@iankelling.org
3842 Subject: $sub
3843
3844 EOF
3845 cat
3846 } | exim -t
3847 fi
3848 }
3849 daylertme() {
3850 if [[ -t 0 ]]; then
3851 exim -t <<EOF
3852 From: alertme@b8.nz
3853 To: daylert@iankelling.org
3854 Subject: $*
3855 EOF
3856 else
3857 read -r sub
3858 { cat <<EOF
3859 From: alertme@b8.nz
3860 To: daylert@iankelling.org
3861 Subject: $sub
3862
3863 EOF
3864 cat
3865 } | exim -t
3866 fi
3867 }
3868
3869 # alert when a page goes live.
3870 alert200() {
3871 local quiet url tmpdir
3872 quiet=false
3873 case $1 in
3874 # dont send a diff of the html. some html is not very readable
3875 -q) quiet=true
3876 shift
3877 ;;
3878 esac
3879 url="$1"
3880 tmpdir="$(mktemp -d)"
3881 cd $tmpdir
3882 while true; do
3883 if wget -q "$url"; then
3884 if $quiet; then
3885 echo | daylert 200
3886 else
3887 alertme $tmpdir
3888 fi
3889 fi
3890 sleep $(( 120 + RANDOM % 300 ))
3891 done
3892 }
3893
3894 # alert on changes to a webpage (just the base page that curl gets)
3895 # usage: weblert URL [SUBJECT...]
3896 weblert() {
3897 local u old new quiet
3898 quiet=false
3899 case $1 in
3900 # dont send a diff of the html. some html is not very readable
3901 -q) quiet=true
3902 shift
3903 ;;
3904 esac
3905 u="$1"
3906 shift
3907 subject="${*:-weblert}"
3908 old=$(curl -s "$u") ||:
3909 while true; do
3910 new=$(curl -s "$u") ||:
3911 if [[ $old && $new ]]; then
3912 if [[ $new != "$old" ]]; then
3913 if $quiet; then
3914 echo | daylertme "$subject"
3915 else
3916 diff <(printf "%s\n" "$old") <(printf "%s\n" "$new") | daylertme "$subject" ||:
3917 fi
3918 fi
3919 old="$new"
3920 fi
3921 sleep $(( 60 + RANDOM % 120 ))
3922 done
3923 }
3924
3925 torshell() {
3926 # per man torsocks
3927 # shellcheck disable=SC1090 # expected
3928 source "$(type -p torsocks)" on
3929 }
3930
3931 eless2() {
3932 less /var/log/exim4/nondmain
3933 }
3934
3935
3936 # mail related
3937 testexim() {
3938 # testmail above calls sendmail, which is a link to exim/postfix.
3939 # its docs dont say a way of adding an argument
3940 # to sendmail to turn on debug output. We could make a wrapper, but
3941 # that is a pain. Exim debug args are documented here:
3942 # http://www.exim.org/exim-html-current/doc/html/spec_html/ch-the_exim_command_line.html
3943 #
3944 # http://www.exim.org/exim-html-current/doc/html/spec_html/ch-building_and_installing_exim.html
3945 # note, for exim daemon, you can turn on debug options by
3946 # adding -d, etc to COMMONOPTIONS in
3947 # /etc/default/exim4
3948 #
3949 # to specify recipients other than those in to, cc, bcc, you can use the cli args, eg:
3950 # exim -t 'test@zroe.org, t2@zroe.org' <<'EOF'
3951 #
3952 # -t = get recipient from header
3953 exim -d -t <<EOF
3954 From: ian@iankelling.org
3955 To: submit@b.b8.nz
3956 Subject: testbug1
3957
3958 Package: test
3959 Version:1
3960
3961 This is a test message.
3962 EOF
3963 }
3964
3965 # test bounce exim
3966 testbexim() {
3967 to=$1
3968 exim -d -f '<>' $to <<EOF
3969 From: Mail Delivery System <Mailer-Daemon@gnu.org>
3970 To: $to
3971 Subject: Mail delivery failed: returning message to sender
3972
3973 This message was created automatically by mail delivery software.
3974 EOF
3975
3976 }
3977
3978
3979 # toggle keyboard
3980 tk() {
3981 # based on
3982 # https://askubuntu.com/questions/160945/is-there-a-way-to-disable-a-laptops-internal-keyboard
3983 id=$(xinput --list --id-only 'AT Translated Set 2 keyboard')
3984 if xinput list | grep -F '∼ AT Translated Set 2 keyboard' &>/dev/null; then
3985 echo enabling keyboard
3986 # find the first slave keyboard number, they are all the same in my output.
3987 # if they werent, worst case we would need to save the slave number somewhere
3988 # when it got disabled.
3989 slave=$(xinput list | sed -n 's/.*slave \+keyboard (\([0-9]*\)).*/\1/p' | head -n1)
3990 xinput reattach $id $slave
3991 else
3992 xinput float $id
3993 fi
3994 }
3995
3996 tm() {
3997 # timer in minutes
3998 # --no-config
3999 (sleep "$(calc "$* * 60")" && mpv --no-config --volume 50 /a/bin/data/alarm.mp3) > /dev/null 2>&1 &
4000 }
4001
4002 ## usage: to connect to my main transmission daemon from a different host, run this
4003 trans-remote-route() {
4004 :
4005 }
4006 trg() { transmission-remote-gtk & r; }
4007 # TODO: this wont work transmission.lan doesnt exist
4008 trc() {
4009 # example, set global upload limit to 100 kilobytes:
4010 # trc -u 100
4011 TR_AUTH=":$(jq -r .profiles[0].password ~/.config/transmission-remote-gtk/config.json)" transmission-remote transmission.lan -ne "$@"
4012 }
4013
4014 trysleep() {
4015 retries="$1"
4016 sleepsecs="$2"
4017 shift 2
4018 for (( i=0; i < retries - 1; i++ )); do
4019 if "$@"; then
4020 return 0
4021 fi
4022 sleep $sleepsecs
4023 done
4024 "$@"
4025 }
4026
4027
4028 tu() {
4029 local s
4030 if [[ -e $1 && ! -w $1 || ! -w $(dirname "$1") ]]; then
4031 s=s;
4032 fi
4033 # full path for using in some initial setup steps
4034 $s /a/exe/teeu "$@"
4035 }
4036
4037 # execute exim in its namespace. Useful args like -Mrm
4038 enn() {
4039 local ecmd pid
4040
4041 ecmd="/usr/sbin/exim4 -C /etc/exim4/nn-mainlog.conf"
4042 if ip a show veth1-mail &>/dev/null; then
4043 s $ecmd "$@"
4044 else
4045 sdncmdroot exim4 $ecmd "$@"
4046 fi
4047 }
4048
4049 # get pid of systemd service
4050 servicepid() {
4051 local pid unit dir
4052 unit="$1"
4053 pid=$(systemctl show --property MainPID --value "$unit")
4054 case $pid in
4055 [1-9]*) : ;;
4056 *)
4057
4058 dir=/sys/fs/cgroup/system.slice
4059 if [[ ! -d $dir ]]; then
4060 # t10 and older directory.
4061 dir=/sys/fs/cgroup/systemd/system.slice
4062 fi
4063
4064 # 0 or empty. This file includes the MainPid, so I expect we
4065 # could just get this in the first place, but i don't know if that
4066 # is always the case.
4067 pid=$(head -n1 $dir/${unit%.service}.service/cgroup.procs)
4068 ;;
4069 esac
4070 if [[ $pid ]]; then
4071 printf "%s\n" "$pid"
4072 else
4073 return 1
4074 fi
4075 }
4076
4077 sdnbash() { # systemd namespace bash
4078 local unit pid
4079 if (( $# != 1 )); then
4080 echo $0: error wrong number of args >&2
4081 return 1
4082 fi
4083 unit=$1
4084 pid=$(servicepid $unit)
4085 m sudo nsenter -t $pid -n -m sudo -u $USER -i bash
4086 }
4087
4088 sdnbashroot() { # systemd namespace bash as root
4089 local unit pid
4090 if (( $# != 1 )); then
4091 echo $0: error wrong number of args >&2
4092 return 1
4093 fi
4094 unit=$1
4095 pid=$(servicepid $unit)
4096 m sudo nsenter -t $pid -n -m bash
4097 }
4098
4099
4100 # systemd namespace cmd
4101 # usage: UNIT CMD...
4102 sdncmd() {
4103 local unit pid tmpf
4104 if (( $# <= 1 )); then
4105 echo $0: error wrong number of args >&2
4106 return 1
4107 fi
4108 unit=$1
4109 shift
4110 pid=$(servicepid $unit)
4111 tmpf=$(mktemp --tmpdir $unit.XXXXXXXXXX)
4112 export -p >$tmpf
4113 printf "%s " "${@@Q}" >>$tmpf
4114 echo >>$tmpf
4115 m sudo nsenter -t $pid -n -m sudo -u $USER -i bash -c ". $tmpf & rm $tmpf"
4116 }
4117
4118 sdncmdroot() { # systemd namespace root command
4119 local unit pid
4120 if (( $# < 2 )); then
4121 echo $0: error wrong number of args >&2
4122 return 1
4123 fi
4124 unit=$1
4125 shift
4126 pid=$(servicepid $unit)
4127 m sudo nsenter -t $pid -n -m "$@"
4128 }
4129
4130
4131 # systemd network namespace (not mount) cmd
4132 # usage: UNIT CMD...
4133 sdnncmd() {
4134 local unit pid tmpf
4135 if (( $# <= 1 )); then
4136 echo $0: error wrong number of args >&2
4137 return 1
4138 fi
4139 unit=$1
4140 shift
4141 pid=$(servicepid $unit)
4142 tmpf=$(mktemp --tmpdir $unit.XXXXXXXXXX)
4143 export -p >$tmpf
4144 printf "%s " "${@@Q}" >>$tmpf
4145 echo >>$tmpf
4146 m sudo nsenter -t $pid -n sudo -u $USER -i bash -c ". $tmpf & rm $tmpf"
4147 }
4148
4149
4150 mailnnbash() {
4151 sdnbash mailnn
4152 }
4153
4154 # we use wireguard now, use mailnnbash.
4155 # mailvpnbash() {
4156 # m sudo nsenter -t $(pgrep -f "/usr/sbin/openvpn .* --config /etc/openvpn/.*mail.conf") -n -m sudo -u $USER -i bash
4157 # }
4158
4159 eximbash() {
4160 sdnbashroot exim4
4161 }
4162 spamnn() {
4163 local spamdpid
4164 spamdpid=$(systemctl show --property MainPID --value spamassassin)
4165 m sudo nsenter -t $spamdpid -n -m sudo -u Debian-exim spamassassin "$@"
4166 }
4167 unboundbash() {
4168 sdnbashroot unbound
4169 }
4170
4171 nmtc() {
4172 s nmtui-connect "$@"
4173 }
4174
4175 # check exim and others network namespace
4176 mailnncheck() {
4177 local unit pid ns mailnn spamd_ser
4178
4179 spamd_ser=spamd
4180 if systemctl cat spamassassin &>/dev/null; then
4181 spamd_ser=spamassassin
4182 fi
4183
4184 # mailvpn would belong on the list if using openvpn
4185 for unit in mailnn unbound dovecot $spamd_ser exim4 radicale; do
4186 pid=$(servicepid $unit)
4187 echo debug: unit=$unit pid=$pid
4188 if [[ ! $pid ]]; then
4189 echo failed to find pid for unit=$unit
4190 continue
4191 fi
4192 if ! ns=$(s readlink /proc/$pid/ns/net); then
4193 echo failed to find ns for unit=$unit pid=$pid
4194 continue
4195 fi
4196 if [[ $mailnn ]]; then
4197 if [[ $ns != "$mailnn" ]]; then
4198 echo "$unit ns $ns != $mailnn"
4199 fi
4200 else
4201 mailnn=$ns
4202 fi
4203 done
4204
4205 }
4206
4207
4208 vpncmd() {
4209 sdncmd openvpn-client-tr@client.service "$@"
4210 }
4211 vpni() {
4212 sdncmd openvpn-client-tr@client.service bash
4213 }
4214 vpnbash() {
4215 sdncmdroot openvpn-client-tr@client.service bash
4216 }
4217
4218
4219 vpn() {
4220 if [[ -e /lib/systemd/system/openvpn-client@.service ]]; then
4221 local vpn_service=openvpn-client
4222 else
4223 local vpn_service=openvpn
4224 fi
4225
4226 [[ $1 ]] || { echo need arg; return 1; }
4227 journalctl --since=now --unit=$vpn_service@$1 -f -n0 &
4228 sudo systemctl start $vpn_service@$1
4229 # sometimes the ask-password agent does not work and needs a delay.
4230 sleep .5
4231 # https://bugs.debian.org/cgi-bin/bugreport.cgi?bug=779240
4232 # noticed around 8-2017 after update from around stretch release
4233 # on debian testing, even though the bug is much older.
4234 sudo systemd-tty-ask-password-agent
4235 }
4236
4237 fixu() {
4238 local stats
4239 ls -lad /run/user/1000
4240 stats=$(stat -c%a-%g-%u /run/user/1000)
4241 if [[ $stats != 700-1000-1000 ]]; then
4242 m s chmod 700 /run/user/1000; m s chown iank.iank /run/user/1000
4243 fi
4244 }
4245
4246 # unmute desktop output
4247 um() {
4248 local sink card sedcmd
4249 sink=$(pactl get-default-sink)
4250 if [[ $sink == auto_null ]]; then
4251 # guessing there is just one with an off profile. otherwise we will
4252 # need some other solution, like storing the card identifier that we
4253 # muted with nap. Or, we could so some hakery with
4254 # pactl -f json.
4255 sedcmd='/^[[:space:]]*index:/{s/^[[:space:]]*index://;h};/^[[:space:]]*active profile: <off>$/{g;p;q}'
4256 card=$(pacmd list-cards | sed -n "$sedcmd")
4257 m pacmd set-card-profile "$card" output:analog-stereo
4258 fi
4259
4260 m pactl set-sink-mute @DEFAULT_SINK@ false
4261 rm -f /tmp/ianknap
4262 }
4263
4264 nap() {
4265 local sink card
4266 sink=$(pactl get-default-sink)
4267 card="${sink%.*}"
4268 card="${card/output/card}"
4269 m pacmd set-card-profile "$card" off
4270
4271 # clicking on a link in a browser can cause unmute.
4272 # I don't want that. So, use a stronger form of mute
4273 # than this.
4274 #pactl set-sink-mute @DEFAULT_SINK@ true
4275 touch /tmp/ianknap
4276 }
4277
4278
4279 # systemctl is-enabled / status / cat says nothing, instead theres
4280 # some obscure symlink. paths copied from man systemd.unit.
4281 # possibly also usefull, but incomplete, doesnt show units not loaded in memory:
4282 # seru list-dependencies --reverse --all UNIT
4283 sysd-deps() {
4284 local f
4285 local -a dirs search
4286 ngset
4287
4288 case $1 in
4289 u)
4290 search=(
4291 ~/.config/systemd/user.control/*
4292 $XDG_RUNTIME_DIR/systemd/user.control/*
4293 $XDG_RUNTIME_DIR/systemd/transient/*
4294 $XDG_RUNTIME_DIR/systemd/generator.early/*
4295 ~/.config/systemd/user/*
4296 /etc/systemd/user/*
4297 $XDG_RUNTIME_DIR/systemd/user/*
4298 /run/systemd/user/*
4299 $XDG_RUNTIME_DIR/systemd/generator/*
4300 ~/.local/share/systemd/user/*
4301 /usr/lib/systemd/user/*
4302 $XDG_RUNTIME_DIR/systemd/generator.late/*
4303 )
4304 ;;
4305 *)
4306 search=(
4307 /etc/systemd/system.control/*
4308 /run/systemd/system.control/*
4309 /run/systemd/transient/*
4310 /run/systemd/generator.early/*
4311 /etc/systemd/system/*
4312 /etc/systemd/systemd.attached/*
4313 /run/systemd/system/*
4314 /run/systemd/systemd.attached/*
4315 /run/systemd/generator/*
4316 /lib/systemd/system/*
4317 /run/systemd/generator.late/*
4318 )
4319 ;;
4320 esac
4321 for f in "${search[@]}"; do
4322 [[ -d $f ]] || continue
4323 case $f in
4324 *.requires|*.wants)
4325 dirs+=("$f")
4326 ;;
4327 esac
4328 done
4329 # dirs is just so we write out the directory names, ls does it when there is 2 or more dirs.
4330 case ${#dirs[@]} in
4331 1)
4332 echo "${dirs[0]}:"
4333 ll "${dirs[@]}"
4334 ;;
4335 0) : ;;
4336 *)
4337 ll "${dirs[@]}"
4338 ;;
4339 esac
4340 ngreset
4341 }
4342
4343 fixvpndns() {
4344 local link istls
4345 read -r _ link _ istls < <(resolvectl dnsovertls tunfsf)
4346 case $istls in
4347 yes|no) : ;;
4348 *) echo fixvpndns error: unexpected istls value: $istls >&2; return 1 ;;
4349 esac
4350 s busctl call org.freedesktop.resolve1 /org/freedesktop/resolve1 org.freedesktop.resolve1.Manager SetLinkDNSOverTLS is $link no
4351 }
4352
4353 vpnoff() {
4354 [[ $1 ]] || { echo need arg; return 1; }
4355 if [[ -e /lib/systemd/system/openvpn-client@.service ]]; then
4356 local vpn_service=openvpn-client
4357 else
4358 local vpn_service=openvpn
4359 fi
4360 sudo systemctl stop $vpn_service@$1
4361 }
4362 vpnoffc() { # vpn off client
4363 ser stop openvpn-client-tr@client
4364 }
4365 vpnc() {
4366 local unit
4367 unit=openvpn-client-tr@client
4368 sudo -v
4369 if [[ $(systemctl is-active $unit) != active ]]; then
4370 s systemctl start $unit
4371 sleep 1
4372 fi
4373 }
4374
4375
4376 vspicy() { # usage: VIRSH_DOMAIN
4377 # connect to vms made with virt-install
4378 spicy -p "$(sudo virsh dumpxml "$1"|grep "<graphics.*type='spice'"|\
4379 sed -r "s/.*port='([0-9]+).*/\1/")"
4380 }
4381
4382 wian() {
4383 cat-new-files /m/4e/INBOX/new
4384 }
4385 wakehours() {
4386 local sec
4387 if (( $# != 1 )) ; then
4388 echo wakehours: error: expected 1 arg, got $# >&2
4389 return 1
4390 fi
4391 sec=$(( EPOCHSECONDS - $( date +%s -d $1am ) ))
4392 printf "%d:%02d\n" $(( sec / 60 / 60)) $(( (sec / 60) % 60 ))
4393 }
4394
4395 calvis() { # calendar visualize
4396 install -m 600 /dev/null /tmp/calendar-bytes
4397 while read -r l; do
4398 for char in $l; do
4399 # shellcheck disable=SC2059 # intentional for the hex formatting
4400 printf "\x$(printf "%x" $char)" >>/tmp/calendar-bytes
4401 done
4402 done < <(grep -v '[#-]' /p/calendar-data)
4403 /p/c/proc/calendar/linux-amd64/calendar
4404 }
4405
4406 wtr() { curl wttr.in/boston; }
4407
4408 xevkb() { xev -event keyboard; }
4409
4410 # * misc stuff
4411
4412 vrun() {
4413 printf "running: %s\n" "$*"
4414 "$@"
4415 }
4416
4417 electrum() {
4418 # Running the appimage said fuse was not available, but try
4419 # running the appimage with --appimage-extract, which worked.
4420 # It seems there is no need to backup the wallet, it can be restored
4421 # via the seed onto any computer that needs it.
4422 /a/opt/electrum/squashfs-root/AppRun "$@"
4423
4424
4425 # This was an old way I ran electrum over tor, and seems like I
4426 # imported a bitcoin core wallet.
4427 #
4428 # https://electrum.readthedocs.io/en/latest/tor.html
4429 # https://github.com/spesmilo/electrum-docs/issues/129
4430 # s rsync -ptog --chown bitcoin:bitcoin ~/.Xauthority /var/lib/bitcoind/.Xauthority
4431 # sudo -u bitcoin DISPLAY=$DISPLAY XAUTHORITY=/var/lib/bitcoind/.Xauthority /a/opt/electrum-4.2.1-x86_64.AppImage -p socks5:localhost:9050
4432
4433 }
4434
4435
4436 monero() {
4437 sudo -u bitcoin DISPLAY=$DISPLAY XAUTHORITY=/var/lib/bitcoind/.Xauthority /a/opt/monero-gui-v0.17.3.2/monero-wallet-gui
4438 }
4439
4440
4441 # grep + find
4442 gef() {
4443 faf | grep -E "$@" ||:
4444 rgv "$@"
4445 }
4446
4447 # rg my main files
4448 rgm() {
4449 rg "$@" /p/w.org /a/t.org /a/work.org /b
4450 }
4451
4452 # re all my files more expansively.
4453 # usage [-OPT...] regex space combined
4454 rem() {
4455 local paths
4456 local -a opts
4457 for arg; do
4458 if [[ $arg == -* ]]; then
4459 opts+=("$1")
4460 shift
4461 else
4462 break
4463 fi
4464 done
4465 paths="/p/c /b/"
4466 find $paths -not \( -name .svn -prune -o -name .git -prune \
4467 -o -name .hg -prune -o -name .editor-backups -prune \
4468 -o -name .undo-tree-history -prune \) 2>/dev/null | grep -iP --color=auto -- "$*" ||:
4469 rgv $local_rgv_args -g "!bash_unpublished" "${opts[@]}" -- "$*" $paths /a/work.org ||:
4470 }
4471 reml() { # rem with limit to 5 matches per file
4472 local_rgv_args="-m 5"
4473 rem "$@"
4474 }
4475
4476 rep() {
4477 local paths
4478 paths="/p/c"
4479 find $paths -not \( -name .svn -prune -o -name .git -prune \
4480 -o -name .hg -prune -o -name .editor-backups -prune \
4481 -o -name .undo-tree-history -prune \) 2>/dev/null | grep -iP --color=auto -- "$*" ||:
4482 rgv $local_rgv_args -- "$*" $paths /a/t.org /p/w.org ||:
4483 }
4484 repl() { # rem with limit to 5 matches per file
4485 local local_rgv_args="-m 5"
4486 rem "$@"
4487 }
4488
4489
4490 # re on common fsf files
4491 ref() {
4492 local paths
4493 paths="/f/gluestick /f/brains /f/s /c"
4494 find $paths -not \( -name .svn -prune -o -name .git -prune \
4495 -o -name .hg -prune -o -name .editor-backups -prune \
4496 -o -name .undo-tree-history -prune \) 2>/dev/null | grep -iP --color=auto -- "$*" ||:
4497 rgv -- "$*" $paths /a/work.org ||:
4498 }
4499
4500
4501 # for use in /f/bind
4502 fupzone() {
4503 # shellcheck disable=SC2046 # i want word splitting
4504 ./update-zone $(i s | sed -rn 's/.*db\.(.*)/\1/p')
4505 }
4506
4507 # setup:
4508 # pip3 install linode-cli
4509 # linode-cli
4510 livp9() {
4511 local input ip id tmp
4512 input=$1
4513 if [[ $2 ]]; then
4514 id=$2
4515 ip=$3
4516 else
4517 tmp=$(mktemp)
4518 echo $tmp
4519 linode-cli --json --pretty linodes create --root_pass loxHuceygomGisun | tee $tmp
4520 read -r ip id <<<"$(tail -n+2 $tmp | jq -r '.[0].ipv4[0] , .[0].id')"
4521 for string in $ip $id; do
4522 case $string in
4523 [0-9]*) : ;;
4524 *)
4525 echo "livp9: bad value ip=$ip id=$id input=$input"
4526 return 1
4527 ;;
4528 esac
4529 done
4530 rm $tmp
4531
4532 while true; do
4533 if timeout 4 ssh $ip :; then
4534 break
4535 fi
4536 sleep 3
4537 done
4538 fi
4539 ssh $ip <<EOF
4540 apt-get -qq update
4541 apt-get -qq -y install ffmpeg rsync
4542 mkdir vp9
4543 EOF
4544 m rsync $input $ip:
4545 m ssh $ip ffmpeg -nostdin -hide_banner -loglevel error -i $input -g 192 -vcodec libvpx-vp9 -vf scale=-1:720 -max_muxing_queue_size 9999 -b:v 750K -pass 1 -an -f null /dev/null
4546 m ssh $ip ffmpeg -nostdin -hide_banner -loglevel error -y -i $input -g 192 -vcodec libvpx-vp9 -tile-rows 2 -vf scale=-1:720 -max_muxing_queue_size 9999 -b:v 750K -pass 2 -c:a libvorbis -qscale:a 5 vp9/$input
4547 rsync $ip:vp9/$input vp9
4548 linode-cli linodes delete $id
4549 }
4550
4551 reset-konsole() {
4552 # we also have a file in /a/c/...konsole...
4553 local f=$HOME/.config/konsolerc
4554 setini DefaultProfile profileian.profile "Desktop Entry" $f
4555 setini Favorites profileian.profile "Favorite Profiles" $f
4556 setini ShowMenuBarByDefault false KonsoleWindow $f
4557 setini TabBarPosition Top TabBar $f
4558 }
4559
4560 reset-sakura() {
4561 while read -r k v; do
4562 # shellcheck disable=SC2154
4563 setini $k $v sakura /a/c/subdir_files/.config/sakura/sakura.conf
4564 done <<'EOF'
4565 colorset1_back rgb(33,37,39)
4566 less_questions true
4567 audible_bell No
4568 visible_bell No
4569 disable_numbered_tabswitch true
4570 scroll_lines 10000000
4571 scrollbar true
4572 EOF
4573 }
4574
4575 # make a page of links found in the files $@. redirect output
4576 linkhtml() {
4577 gr -oh 'https?:\/\/(www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_\+.~#?&//=]*)' "$@" | \
4578 rev | sort -u | rev | sed 's,.*,<a href="\0">\0</a><br\>,'
4579 }
4580
4581 reset-xscreensaver() {
4582 # except for spash, i set these by setting gui options in
4583 # xscreensaver-command -demo
4584 # then finding the corresponding option in .xscreensaver
4585 # spash, i happened to notice in .xscreensaver
4586 #
4587 # dpmsOff, monitor doesnt come back on using old free software supported nvidia card
4588 cat > /home/iank/.xscreensaver <<'EOF'
4589 mode: blank
4590 dpmsEnabled: True
4591 dpmsStandby: 0:07:00
4592 dpmsSuspend: 0:08:00
4593 dpmsOff: 0:00:00
4594 timeout: 0:05:00
4595 lock: True
4596 lockTimeout: 0:06:00
4597 splash: False
4598 EOF
4599
4600 }
4601
4602
4603 # very useful, copy directory structure 3 deep. add remove /*/ to change level
4604 # rsync -aivh --exclude '/*/*/*/' -f"+ */" -f"- *" SRC DEST
4605
4606
4607 # * stuff that makes sense to be at the end
4608 if [[ "$SUDOD" ]]; then
4609 # allow failure, for example if we are sudoing into a user with diffferent/lesser permissions.
4610 cd "$SUDOD" ||:
4611 unset SUDOD
4612 elif [[ -d /a ]] && [[ $PWD == "$HOME" ]] && [[ $- == *i* ]]; then
4613 cd /a
4614 OLDPWD=
4615 fi
4616
4617
4618
4619
4620 # for mitmproxy to get a newer python.
4621 # commented until i want to use it because it
4622 # noticably slows bash startup
4623 #
4624
4625 mypyenvinit () {
4626 if [[ $EUID == 0 || ! -e ~/.pyenv/bin ]]; then
4627 echo "error: dont be root. make sure pyenv is installed"
4628 return 1
4629 fi
4630 export PATH="$HOME/.pyenv/bin:$PATH"
4631 eval "$(pyenv init -)"
4632 eval "$(pyenv virtualenv-init -)"
4633 }
4634
4635
4636
4637 # I have the git repo and a release. either one should work.
4638 # I have both because I was trying to solve an issue that
4639 # turned out to be unrelated.
4640 # ARDUINO_PATH=/a/opt/Arduino/build/linux/work
4641
4642 ## i should have documented this...
4643 # based on https://github.com/keyboardio/Kaleidoscope
4644 export KALEIDOSCOPE_DIR=/a/opt/Kaleidoscope
4645
4646 # They want to be added to the start, but i think
4647 # that should be avoided unless we really need it.
4648 path-add --end ~/.npm-global
4649
4650
4651 path-add --end $HOME/.cargo/bin
4652
4653 if type -P rg &>/dev/null; then
4654 # --no-messages because of annoying errors on broken symlinks
4655 # -z = search .gz etc files
4656 # -. = search dotfiles
4657 # -n --no-heading: show files and line numbers together allowing for clicking
4658 rg() {
4659 local path_arg
4660 if [[ ${#@} == 1 ]]; then
4661 path_arg=.
4662 fi
4663
4664 command rg -. -z --no-messages -Lin --no-heading -M 900 --no-ignore-parent --no-ignore-vcs -g '!.git' -g '!auto-save-list' -g '!.savehist' "$@" $path_arg || return $?
4665 }
4666 #fails if not exist. ignore
4667 complete -r rg 2>/dev/null ||:
4668 else
4669 alias rg=grr
4670 fi
4671
4672 # rg with respecting vcs ignore files
4673 rgv() {
4674 local path_arg ret=0
4675 if [[ ${#@} == 1 ]]; then
4676 path_arg=.
4677 fi
4678 # settings that are turned off for pipes, keep them on.
4679 # Found by searching for "terminal" in --help
4680 # --heading
4681 # -n
4682 #
4683 # -. = search dotfiles
4684 # -z = search zipped files
4685 # -i = case insensitive
4686 # -M = max columns
4687 # -n --no-heading: show files and line numbers together allowing for clicking
4688 # --no-messages because of annoying errors on broken symlinks
4689 # --no-ignore-parent because i have /a/.git which ignores almost everything under it.
4690 command rg -n --no-heading -. -z --no-messages -i -M 900 --no-ignore-parent -g '!.git' -g '!auto-save-list' -g '!.savehist' "$@" $path_arg || ret=$?
4691 return $ret
4692 }
4693
4694 amall() {
4695 echo "$(tput setaf 5 2>/dev/null ||:)â–ˆ coresite â–ˆ$(tput sgr0 2>/dev/null||:)"
4696 amfsf "$@"
4697 echo "$(tput setaf 5 2>/dev/null ||:)â–ˆ office â–ˆ$(tput sgr0 2>/dev/null||:)"
4698 amoffice "$@"
4699 }
4700 amallq() { # amall quiet
4701 amfsf "$@"
4702 amoffice "$@"
4703 }
4704 amfsf() {
4705 sedi -r '/alertmanager.url/s/@prom.office/@prom/' ~/.config/amtool/config.yml
4706 amtool "$@"
4707 }
4708 amoffice() {
4709 sedi -r '/alertmanager.url/s/@prom.fsf/@prom.office.fsf/' ~/.config/amtool/config.yml
4710 amtool "$@"
4711 }
4712 amls() {
4713 amall silence query "$@"
4714 }
4715 # amtool silence add
4716 amsa() {
4717 amall silence add "$@"
4718 }
4719 # amtool silence force
4720 amsf() {
4721 amall silence add x!="1"
4722 }
4723 amrmall() {
4724 # note: not sure if quoting of this arg is correct
4725 amfsf silence expire "$(amfsf silence query -q)"
4726 amoffice silence expire "$(amoffice silence query -q)"
4727 }
4728
4729
4730 youtube-dl-update() {
4731 sudo wget https://yt-dl.org/downloads/latest/youtube-dl -O /usr/local/bin/youtube-dl
4732 sudo chmod a+rx /usr/local/bin/youtube-dl
4733 }
4734
4735 # https://github.com/yt-dlp/yt-dlp/wiki/Installation
4736 yt-dlp-update() {
4737 sudo curl -L https://github.com/yt-dlp/yt-dlp/releases/latest/download/yt-dlp -o /usr/local/bin/yt-dlp
4738 sudo chmod a+rx /usr/local/bin/yt-dlp # Make executable
4739 }
4740
4741 mpvyt() {
4742 mpv --ytdl ytdl_path=/usr/local/bin/yt-dlp "$@"
4743 }
4744
4745 # taken from default changes to bashrc and bash_profile
4746 path-add --end --ifexists $HOME/.rvm/bin
4747 # also had ruby bin dir, but moved that to environment.sh
4748 # so its included in overall env
4749
4750
4751 # ya, hacky hardcoded hostnames in 2023. we could do better
4752 hssh-update() {
4753 local -a failed_hosts hosts
4754 case $HOSTNAME in
4755 sy|kd)
4756 hosts=(
4757 kd.b8.nz x3.office.fsf.org syw x2.b8.nz
4758 )
4759 ;;
4760 x3)
4761 hosts=(
4762 b8.nz sywg.b8.nz
4763 )
4764 ;;
4765 esac
4766 for host in ${hosts[@]}; do
4767 e $host
4768 if ! scp /b/fai/fai/config/files/usr/local/bin/hssh/IANK root@$host:/usr/local/bin/hssh; then
4769 failed_hosts+=($host)
4770 fi
4771 done
4772 if (( ${#failed_hosts[@]} >= 1 )); then
4773 echo failed_hosts=${failed_hosts[*]}
4774 return 1
4775 fi
4776 }
4777
4778 noi3bar() {
4779 touch /tmp/noi3bar
4780 }
4781 i3bar() {
4782 rm -fv /tmp/noi3bar
4783 }
4784
4785 # example:
4786 # <#part type="image/jpeg" filename="/home/iank/2023-12-24-ski-trip.jpg" disposition=attachment> <#/part>
4787 #
4788 attach-txt() {
4789 local f
4790 for f; do
4791 if [[ ! -s $f ]]; then
4792 e "error: empty or non-existent file $f"
4793 return 1
4794 fi
4795 done
4796 for f; do
4797 echo '<#part type="image/jpeg" filename="'"$(rl "$f")"'" disposition=attachment> <#/part>'
4798 done | ec
4799 }
4800
4801 ctof() {
4802 units "tempC($1)" tempF
4803 }
4804
4805 ftoc() {
4806 units "tempF($1)" tempC
4807 }
4808
4809 # local icecast
4810 localic() {
4811 local mod=false
4812 cedit live /p/c/machine_specific/vps/filesystem/var/lib/bind/db.iankelling.org <<'EOF' || mod=true
4813 live CNAME b8.nz.
4814 EOF
4815 if $mod; then
4816 ip=$(ip r show default | sed -r 's/.*src ([^ ]*).*/\1/' | head -n1)
4817 if [[ ! $ip ]] && timeout 1 ping -c 1 $ip; then
4818 echo "error: failed to get ip: $ip" >&2
4819 exit 1
4820 fi
4821 cat >/p/c/cmc-firewall-data-http <<EOF
4822 http_ip=$ip
4823 EOF
4824 bindpush
4825 wrt-setup
4826 fi
4827 web-conf -e ian@iankelling.org -f 8000 - apache2 live.iankelling.org <<'EOF'
4828 <Location "/fsf.webm">
4829 AuthType Basic
4830 AuthName "basic_auth"
4831 # created with
4832 # htpasswd -c icecast-fsf-htpasswd USERNAME
4833 AuthUserFile "/etc/icecast-fsf-htpasswd"
4834 Require valid-user
4835 </Location>
4836 <Location "/fsf-tech.webm">
4837 AuthType Basic
4838 AuthName "basic_auth"
4839 AuthUserFile "/etc/icecast-fsf-tech-htpasswd"
4840 Require valid-user
4841 </Location>
4842 EOF
4843 s cat /etc/letsencrypt/live/live.iankelling.org/{fullchain,privkey}.pem | s dd of=/etc/icecast2/fullchainpluskey.pem
4844 ser start icecast2
4845 }
4846 # li icecast
4847 liic() {
4848 cedit live /p/c/machine_specific/vps/filesystem/var/lib/bind/db.iankelling.org <<'EOF' || bindpush
4849 live A 72.14.176.105
4850 AAAA 2600:3c00::f03c:91ff:fe6d:baf8
4851 EOF
4852 }
4853 # icecast rm -r
4854 icrmr() {
4855 find /var/icecast -type f -delete
4856 ssh li.b8.nz find /var/icecast -type f -delete
4857 }
4858
4859
4860 # obs screen switching of
4861 obof() {
4862 ls -l /tmp/no-obs-auto-scene-switch
4863 touch /tmp/no-obs-auto-scene-switch
4864 }
4865 # obs screen switching on
4866 obon() {
4867 ls -l /tmp/no-obs-auto-scene-switch
4868 if [[ -e /tmp/no-obs-auto-scene-switch ]]; then
4869 rm -f /tmp/no-obs-auto-scene-switch
4870 fi
4871 }
4872
4873 obs-gen-profiles() {
4874 local p=/p/c/basic/profiles
4875 sed 's/fsf-sysops/fsf-tech/g' $p/fsfsysops/basic.ini >$p/fsftech/basic.ini
4876 sed 's/fsf-sysops/fsf/g' $p/fsfsysops/basic.ini >$p/fsf/basic.ini
4877 }
4878
4879 # terminal clear. like clear, but put the prompt at the bottom,
4880 # useful for obs streaming the bottom half of a terminal window.
4881 tclear() {
4882 for ((i=0; i<COLUMNS; i++)); do
4883 echo
4884 done
4885 }
4886
4887 opensslcertinfo() {
4888 openssl x509 -txt -in "$@"
4889 }
4890
4891 # dsh on btrbk hosts
4892 dsb() {
4893 :
4894 }
4895
4896 # dsh a file and run it
4897 dsa() {
4898 local ret file
4899 if ! parallel -j 10 scp x {}:/tmp <~/.dsh/group/btrbk; then
4900 echo parallel scp failed. dsa returning $ret
4901 fi
4902 dsh -g btrbk
4903 }
4904
4905 # temporary
4906 zmqsend() {
4907 /nocow/t/ffmpeg-release/ffmpeg-7.0.1/tools/zmqsend "$@"
4908 }
4909
4910 ffg() { /nocow/t/ffmpeg-release/ffmpeg-7.0.1/tools/graph2dot -o /tmp/g.tmp && dot -Tpng /tmp/g.tmp -o /tmp/g.png && feh /tmp/g.png; }
4911
4912 firefox-hide-tabs() {
4913
4914 # without this, make tabs smaller by setting browser.uidensity 1 in about:config
4915
4916 profiledir=$1
4917 [[ $1 ]] || return 1
4918 # Related: the sidebery extension is useful.
4919
4920 # This is from
4921 # https://raw.githubusercontent.com/MrOtherGuy/firefox-csshacks/master/chrome/hide_tabs_toolbar.css
4922
4923 ainsl $profiledir/chrome/userChrome.css '#TabsToolbar{ visibility: collapse !important }'
4924
4925 }
4926
4927 # kill lease on cmc
4928 klease() {
4929 local tmpdir ret out
4930 ret=0
4931 out=$(ssh cmc dnsmasq-end-lease "$1" 2>&1) || ret=1
4932 printf "%s\n" "$out"
4933 if [[ $out == *"try diffing"* ]]; then
4934 tmpdir=$(mktemp -d)
4935 m scp cmc:/tmp/dhcp.leases cmc:/tmp/dhcp.leases.iank $tmpdir
4936 m diff $tmpdir/dhcp.leases $tmpdir/dhcp.leases.iank ||:
4937 rm -rf $tmpdir
4938 fi
4939 return $ret
4940 }
4941
4942 # ffs and switch the bash history on this terminal.
4943 # disabled because I don't really need this and
4944 # the history switching is annoying for debugging.
4945 #
4946 # ffs() {
4947 # local last
4948 # last="${*: -1}"
4949 # if [[ $last && $last != -* && $last != sysops ]]; then
4950 # his
4951 # fi
4952 # command ffs "$@"
4953 # }
4954
4955 i3gen() {
4956 /b/ds/i3-sway/gen
4957 }
4958
4959
4960 # insensitive find plus edit
4961 ife() {
4962 local tmps found_count i char file
4963 local -a found_files
4964 local -A button_file
4965 tmps=$(ifn "$@")
4966 mapfile -t found_files <<<"$tmps"
4967 found_count=${#found_files[@]}
4968 if (( ${#found_files[@]} == 1 )); then
4969 m g ${found_files[0]}
4970 else
4971 i=0
4972 for button in {a..z}; do
4973 button_file[$button]="${found_files[$i]}"
4974 echo $button: ${found_files[$i]}
4975 i=$(( i + 1 ))
4976 if (( i >= found_count )); then
4977 break
4978 fi
4979 done
4980 read -rsN1 -t 5 char ||:
4981 file="${button_file[$char]}"
4982
4983 if [[ $file ]]; then
4984 g "$file"
4985 else
4986 echo "no selection"
4987 fi
4988 fi
4989 }
4990
4991 # decrease filesize without losing any noticeable quality. inspired from
4992 # https://gist.github.com/BlueSwordM/86dfcb6ab38a93a524472a0cbe4c4100
4993 ffsencode() {
4994 in="$1"
4995 out="$2"
4996 ffmpeg -i "$in" -c:v libsvtav1 -crf 60 -preset 6 -g 60 -svtav1-params tune=0:enable-overlays=1:scd=1:scm=1 -pix_fmt yuv420p10le -c:a copy "$out"
4997 }
4998
4999 export BASEFILE_DIR=/a/bin/fai-basefiles
5000
5001 #export ANDROID_HOME=/a/opt/android-home
5002 # https://f-droid.org/en/docs/Installing_the_Server_and_Repo_Tools/
5003 #export USE_SDK_WRAPPER=yes
5004 #PATH=$PATH:$ANDROID_HOME/tools:$ANDROID_HOME/platform-tools
5005
5006 # didnt get drush working, if I did, this seems like the
5007 # only good thing to include for it.
5008 # Include Drush completion.
5009 # if [ -f "/home/ian/.drush/drush.complete.sh" ] ; then
5010 # source /home/ian/.drush/drush.complete.sh
5011 # fi
5012
5013
5014 # best practice
5015 unset IFS
5016
5017 # https://wiki.archlinux.org/index.php/Xinitrc#Autostart_X_at_login
5018 # i added an extra condition as gentoo xorg guide says depending on
5019 # $DISPLAY is fragile.
5020 if [[ ! $DISPLAY && $XDG_VTNR == 1 ]] && shopt -q login_shell && isarch; then
5021 exec startx
5022 fi
5023
5024
5025 # ensure no bad programs appending to this file will have an affect
5026 return 0