i3 fix, other improvements
[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 chrome() {
2041 if type -p chromium &>/dev/null; then
2042 cmd=chromium
2043 else
2044 cd /
2045 cmd="schroot -c bullseye 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
3037 mns() { # mount namespace
3038 ns=$1
3039 shift
3040 s mkdir -p /root/mount_namespaces
3041 if ! sudo mountpoint /root/mount_namespaces >/dev/null; then
3042 m sudo mount --bind /root/mount_namespaces /root/mount_namespaces
3043 fi
3044 m sudo mount --make-private /root/mount_namespaces
3045 if [[ ! -e /root/mount_namespaces/$ns ]]; then
3046 m sudo touch /root/mount_namespaces/$ns
3047 fi
3048 if ! sudo mountpoint /root/mount_namespaces/$ns >/dev/null; then
3049 m sudo unshare --propagation slave --mount=/root/mount_namespaces/$ns /bin/true
3050 fi
3051 m sudo -E /usr/bin/nsenter --mount=/root/mount_namespaces/$ns "$@"
3052 }
3053
3054 mnsd() { # mount namespace + systemd namespace
3055 ns=$1
3056 unit=$2
3057 shift 2
3058
3059 s mkdir -p /root/mount_namespaces
3060 if ! sudo mountpoint /root/mount_namespaces >/dev/null; then
3061 m sudo mount --bind /root/mount_namespaces /root/mount_namespaces
3062 fi
3063 m sudo mount --make-private /root/mount_namespaces
3064 if [[ ! -e /root/mount_namespaces/$ns ]]; then
3065 m sudo touch /root/mount_namespaces/$ns
3066 fi
3067 if ! sudo mountpoint /root/mount_namespaces/$ns >/dev/null; then
3068 m sudo unshare --propagation slave --mount=/root/mount_namespaces/$ns /bin/true
3069 fi
3070
3071 pid=$(servicepid $unit)
3072 tmpf=$(mktemp --tmpdir $unit.XXXXXXXXXX)
3073 export -p >$tmpf
3074 printf "%s " "${@@Q}" >>$tmpf
3075 echo >>$tmpf
3076
3077 m sudo nsenter -t $pid -n --mount=/root/mount_namespaces/$ns sudo -u $USER -i bash -c ". $tmpf & sleep 1; rm $tmpf"
3078 }
3079
3080
3081 mnsr() { # mns run
3082 local ns=$1
3083 shift
3084 mns $ns sudo -u iank -E env "PATH=$PATH" "$@"
3085 }
3086
3087 mnsnonetr() {
3088 ns=$1
3089 lomh
3090 if ! s ip netns list | grep -Fx nonet &>/dev/null; then
3091 s ip netns add nonet
3092 fi
3093 mns $ns --net=/var/run/netns/nonet /bin/bash
3094 lomh
3095 }
3096
3097 mnsnonet() {
3098 ns=$1
3099 lomh
3100 if ! s ip netns list | grep -Fx nonet &>/dev/null; then
3101 s ip netns add nonet
3102 fi
3103 mns $ns --net=/var/run/netns/nonet sudo -E -u iank /bin/bash
3104 lomh
3105 }
3106
3107
3108 lom() {
3109 # l = the loopback device
3110 local l base
3111 # get sudo pass cached right away
3112 if ! sudo -nv 2>/dev/null; then
3113 sudo -v
3114 fi
3115 if [[ $1 == /* ]]; then
3116 base=${1##*/}
3117 fs_file=$1
3118 if mns $base mountpoint -q /mnt/$base; then
3119 return 0
3120 fi
3121 l=$(losetup -j $fs_file | sed -rn 's/^([^ ]+): .*/\1/p' | head -n1 ||:)
3122 if [[ ! $l ]]; then
3123 l=$(sudo losetup -f)
3124 m sudo losetup $l $fs_file
3125 fi
3126 if ! sudo cryptsetup status /dev/mapper/$base &>/dev/null; then
3127 if ! m sudo cryptsetup luksOpen $l $base; then
3128 m sudo losetup -d $l
3129 return 1
3130 fi
3131 fi
3132 m sudo mkdir -p /mnt/$base
3133 m mns $base mount /dev/mapper/$base /mnt/$base
3134 m mns $base chown $USER:$USER /mnt/$base
3135 lomh
3136 else
3137 base=$1
3138 if mns $base mountpoint /mnt/$base &>/dev/null; then
3139 m mns $base umount /mnt/$base
3140 fi
3141 if sudo cryptsetup status /dev/mapper/$base &>/dev/null; then
3142 if ! m sudo cryptsetup luksClose /dev/mapper/$base; then
3143 echo lom: failed cryptsetup luksClose /dev/mapper/$base
3144 return 1
3145 fi
3146 fi
3147 l=$(losetup -l --noheadings | awk '$6 ~ /\/'$base'$/ {print $1}')
3148 if [[ $l ]]; then
3149 m sudo losetup -d $l
3150 else
3151 echo lom: warning: no loopback device found
3152 fi
3153 fi
3154 }
3155
3156 # mu personality. for original, just run mp. for 2, run mp 2.
3157 # this is partly duplicated in mail-setup
3158 mp() {
3159 local dead=false
3160 for s in {1..5}; do
3161 if ! killall mu; then
3162 dead=true
3163 break
3164 fi
3165 sleep 1
3166 done
3167 if ! $dead; then
3168 echo error: mu not dead
3169 m psg mu
3170 return 1
3171 fi
3172 suf=$1
3173 set -- /m/mucache ~/.cache/mu /m/.mu ~/.config/mu
3174 while (($#)); do
3175 target=$1$suf
3176 f=$2
3177 shift 2
3178 if [[ -e $f && ! -L $f ]]; then
3179 m rm -rf $f
3180 fi
3181 m ln -sf -T $target $f
3182 done
3183 }
3184
3185 # maildir enable
3186 mdenable() {
3187 local md dst ln_path src two
3188
3189 two=false
3190 case $1 in
3191 -2) two=true; shift ;;
3192 esac
3193
3194 for md; do
3195 src=
3196 if $two; then
3197 dst=/m/4e2/$md
3198 else
3199 dst=/m/4e/$md
3200 fi
3201
3202 ln_path=/m/md/$md
3203 for d in /m/md/$md /m/4e2/$md; do
3204 if [[ -d $d && ! -L $d ]]; then
3205 src=$d
3206 break
3207 fi
3208 done
3209 if [[ ! $src ]]; then
3210 echo "error: could not find $md" >&2
3211 return 1
3212 fi
3213 m mv -T $src $dst
3214 m ln -sf -T $dst $ln_path
3215 done
3216 }
3217 md2enable() {
3218 mdenable -2 "$@"
3219 }
3220 mddisable() {
3221 local md=$1
3222 dst=/m/md/$md
3223
3224 ### begin copied from mdenable, but different d ###
3225 for d in /m/4e/$md /m/4e2/$md; do
3226 if [[ -d $d && ! -L $d ]]; then
3227 src=$d
3228 break
3229 fi
3230 done
3231 if [[ ! $src ]]; then
3232 echo "error: could not find $md" >&2
3233 return 1
3234 fi
3235 ### end copy from mdenable ###
3236
3237 if [[ -L $dst ]]; then m rm $dst; fi
3238 m mv -T $src $dst
3239 }
3240
3241
3242 mdt() {
3243 markdown "$1" >/tmp/mdtest.html
3244 firefox /tmp/mdtest.html
3245 }
3246
3247 mo() { xset dpms force off; } # monitor off
3248
3249 mpvgpu() {
3250 # seems to be the best gpu decoding on my nvidia 670.
3251 # vlc gets similar or better framerate, but is much darker output on my test movie at least.
3252
3253
3254 case $HOSTNAME in
3255 kd)
3256 echo 0f | sudo tee -a /sys/kernel/debug/dri/0/pstate
3257 ;;
3258 esac
3259 # going back to the default slow clock, and slower fan:
3260 # echo 07 | sudo tee -a /sys/kernel/debug/dri/0/pstate
3261 if [[ $DISPLAY ]]; then
3262 mpv --vo=vdpau --hwdec=auto "$@"
3263 else
3264 # waylandvk seems to work the same
3265 mpv --gpu-context=wayland --hwdec=auto
3266 fi
3267 }
3268
3269 mpvd() {
3270 mpv --profile=d "$@";
3271 }
3272 mpva() {
3273 mpv --profile=a "$@";
3274 }
3275 # mpv all media files in . or $1
3276 mpvm() {
3277 local -a extensions arg
3278 # get page source of https://en.wikipedia.org/w/index.php?title=Video_file_format&action=edit
3279 # into /a/x.log, then
3280 # grep '^| *\.' /a/x.log | sed 's/| *//;s/,//g'
3281
3282 # note: to join them together for a regex, do:
3283 # old=; for e in ${extensions[@]/./}; do if [[ ! $old ]]; then old=$e; continue; fi; echo -n "$old|"; old=$e; done; echo $e
3284 extensions=(
3285 .webm
3286 .mkv
3287 .flv
3288 .flv
3289 .vob
3290 .ogv .ogg
3291 .drc
3292 .gif
3293 .gifv
3294 .mng
3295 .avi
3296 .MTS .M2TS .TS
3297 .mov .qt
3298 .wmv
3299 .yuv
3300 .rm
3301 .rmvb
3302 .viv
3303 .asf
3304 .amv
3305 .mp4 .m4p .m4v
3306 .mpg .mp2 .mpeg .mpe .mpv
3307 .mpg .mpeg .m2v
3308 .m4v
3309 .svi
3310 .3gp
3311 .3g2
3312 .mxf
3313 .roq
3314 .nsv
3315 )
3316 arg=("(" -iname "*${extensions[0]}")
3317 for (( i=1 ; i < ${#extensions[@]}; i++ )); do
3318 arg+=(-o -iname "*${extensions[i]}")
3319 done
3320 arg+=(")")
3321 dir=${1:-.}
3322 # debug:
3323 #find $dir "${arg[@]}" -size +200k
3324 find $dir "${arg[@]}" -size +200k -exec mpv --profile=d '{}' +
3325 }
3326 mpvs() {
3327 mpv --profile=s "$@";
3328 }
3329
3330 myirc() {
3331 if [[ ! $1 ]]; then
3332 set -- fsfsys
3333 fi
3334 local -a d
3335 d=( /var/lib/znc/moddata/log/iank/{freenode,libera} )
3336 # use * instead of -r since that does sorted order
3337 ssh root@iankelling.org "for f in ${d[*]}; do cd \$f/#$1; grep '\<iank.*' *; done" | cut --complement -c12-16
3338 }
3339
3340
3341 allmyirc() {
3342 local d
3343 d=/var/lib/znc/moddata/log/iank/freenode
3344 ssh root@iankelling.org "cd $d; find . -mtime -60 -type f -exec grep '\<iank.*' {} +" | sed -r 's,^..([^/]*)/(.{11})(.{5})(.{8}).,\2\4 \1,' | sort
3345 }
3346
3347 # The way pidgin logs with xmpp (maybe related to running cheogram too)
3348 # is that there are sometimes duplicates, and sometimes the a log file
3349 # is for a specific day yet logs messages for subsequent days, and the
3350 # only way to realize that is to notice that the timestamps rolled over
3351 # into a new day, you can't see it in isolation. So, basically, pidgin
3352 # logs are really annoying to read a grep of my messages to find the
3353 # date and time I said when I started and stopped working, so I'm trying
3354 # out a new client: profanity.
3355 mypidgin() {
3356 c /p/c/.purple/logs/jabber/iank@fsf.org/office@conference.fsf.org.chat
3357 for x in *.html; do html2text -o ${x%.html}.txt $x; done;
3358 # shellcheck disable=SC2016 # false positive on ${
3359 grep -A1 ') iank:' ./*.txt \
3360 | sed -r 's/^(.{10})[^ ]*\.txt:\(?([^ ]*)[[:space:]](..). iank:/\1_\2_\3/
3361 s/^[^ ]*\.txt-//
3362 /^--$/d
3363 s/^[^ ]*\.txt:\((.{2}).(.{2}).(.{4}) (.{8}) (.{2})\)?/\3-\1-\2_\4_\5/' \
3364 | sed -n 'x;1d;0~2{G;s/\n/ /;p};${x;p}'
3365 }
3366
3367 # my profanity
3368 #
3369 myprof() {
3370 pushd /home/iank/.local/share/profanity/chatlogs/iank_at_fsf.org/rooms/office_at_conference.fsf.org
3371 logs=(*)
3372 logcount=${#logs[@]}
3373 if (( logcount > 16 )); then
3374 i=$(( logcount - 16 ))
3375 else
3376 i=0
3377 fi
3378 # usually do this on monday, sometimes later
3379 if [[ $(date +%A) == Monday ]]; then
3380 min_date=$(date -d 'monday 2 weeks ago' +%s)
3381 else
3382 min_date=$(date -d 'monday 3 weeks ago' +%s)
3383 fi
3384 for (( ; i < logcount; i++ )); do
3385 log=${logs[$i]}
3386 d=$(date -d "$(head -n1 $log|awk '{print $1}')" +%s)
3387 if (( d < min_date )); then
3388 continue
3389 fi
3390 if awk '$3 == "iank:"' $log | sed -r 's/^(.{10}).(.{8})[^ ]+(.*)/\1_\2\3/' | grep .; then
3391 hr
3392 fi
3393 done
3394 popd
3395 }
3396
3397
3398 # Tail all recent prof logs. Copying from profanity has unwanted line breaks
3399 # especially for links.
3400 profr() {
3401 case $HOSTNAME in
3402 kd)
3403 profr-local
3404 ;;
3405 *)
3406 ssh b8.nz profr-local
3407 ;;
3408 esac
3409 }
3410
3411 profr-local() {
3412 local d0 d1
3413 local -a files
3414 d0="$(date +%Y_%m_%d).log"
3415 d1="$(date -d '1 day ago' +%Y_%m_%d).log"
3416 ngset
3417 files=(/d/p/profanity/chatlogs/iank_at_fsf.org/{*,rooms/*}/{$d0,$d1})
3418 ngreset
3419 if (( ${#files[@]} > 0 )); then
3420 cat "${files[@]}" | sort | tail -n 40
3421 fi
3422 }
3423
3424
3425 # Tail pms in the last day, for the case where we restart profanity and
3426 # didn't check for pms beforehand. Assume the most recent logs are on kd.
3427 # If that isn't the case, use prof-recent-local
3428 prof-recent() {
3429 case $HOSTNAME in
3430 kd)
3431 prof-recent-local
3432 ;;
3433 *)
3434 ssh b8.nz prof-recent-local
3435 ;;
3436 esac
3437 }
3438 prof-recent-local() {
3439 local d dates date files f
3440 # consider making the day count passed by parameter. note: this works: $(date -d '2 day ago' +%Y_%m_%d)
3441 dates=("$(date +%Y_%m_%d)" "$(date -d '1 day ago' +%Y_%m_%d)" )
3442 for d in /d/p/profanity/chatlogs/iank_at_fsf.org/!(rooms); do
3443 files=()
3444 for date in ${dates[@]}; do
3445 f=$d/$date.log
3446 if [[ -e $f ]]; then
3447 files+=($f)
3448 fi
3449 done
3450 if (( ${#files[@]} >= 1 )); then
3451 cat ${files[@]} | tail
3452 hr
3453 fi
3454 done
3455 }
3456
3457 prof-sort() {
3458 case $HOSTNAME in
3459 kd)
3460 prof-recent-sort
3461 ;;
3462 *)
3463 ssh b8.nz prof-recent-sort
3464 ;;
3465 esac
3466 }
3467
3468 prof-recent-sort() {
3469 local d dates date files f
3470 # consider making the day count passed by parameter. note: this works: $(date -d '2 day ago' +%Y_%m_%d)
3471 dates=("$(date +%Y_%m_%d)" "$(date -d '1 day ago' +%Y_%m_%d)" )
3472 files=()
3473 for d in /d/p/profanity/chatlogs/iank_at_fsf.org/!(rooms); do
3474 for date in ${dates[@]}; do
3475 f=$d/$date.log
3476 if [[ -e $f ]]; then
3477 files+=($f)
3478 fi
3479 done
3480 done
3481 for f in "${files[@]}"; do
3482 sed "s/\$/ $f/" $f
3483 done | sort
3484 }
3485
3486
3487 # usage: debvm DEBIAN_VERSION RAM_MB
3488 debvm() {
3489 local ver ram fname src
3490 ver=$1
3491 ram=${2:-2024}
3492 # * is because it might have -backports in the name. we only expect 1 expansion
3493 fnames=( debian-$ver-*nocloud-"$(dpkg --print-architecture)".qcow2 )
3494 if (( ${#fnames[@]} >= 2 )); then
3495 echo "error: iank: unexpected multiple files"
3496 return 1
3497 fi
3498 fname="${fnames[0]}"
3499 src=/a/opt/roms/$fname
3500 if [[ ! -f $src ]]; then
3501 echo debvm: not found $src, download from eg: https://cloud.debian.org/images/cloud/buster/latest/
3502 return 1
3503 fi
3504 cp -a $src /t
3505 # note, in fai-revm we do this: not sure why, maybe because of br device
3506 # --graphics spice,listen=0.0.0.0
3507 m s virt-install --osinfo debian11 --rng /dev/urandom -n deb${ver}tmp --import -r $ram --vcpus 2 --disk /t/$fname --graphics spice
3508 # note: to ssh into this machine will require host key generation: ssh-keygen -A
3509
3510 # random: for cvs2git on gnu www, use debian 10. I could use trisquel,
3511 # but happen to want to try out the debian cloud images. the upstream
3512 # requires python2 and hasn't really changed since the version in d10.
3513 #
3514 # apt install cvs2git cvs
3515 # # 7G was not enough
3516 # mount -o mode=1777,nosuid,nodev,size=34G -t tmpfs tmpfs /tmp
3517 # cvs2git --encoding utf_8 --fallback-encoding ascii --dumpfile=dump www-rsync/www |& tee /tmp/l
3518 ## www-rsync is an rsynced copy of the cvsfrom savannah
3519 }
3520
3521 mygajim() {
3522 local time time_sec time_pretty days
3523 days=${1:-16}
3524 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
3525 case $time in
3526 16*) : ;;
3527 *) continue ;;
3528 esac
3529 if ! time_pretty=$(date +%F.%R -d @$time); then
3530 echo bad time: $time
3531 return 1
3532 fi
3533 echo $time_pretty "$l"
3534 time_sec=${time%%.*}
3535 # only look at the last 18 days. generally just use this for timesheet.
3536 if (( time_sec < EPOCHSECONDS - 60 * 60 * 24 * days )); then break; fi
3537 done
3538 }
3539
3540 allmygajim() {
3541 sqlite3 -separator ' ' /p/c/subdir_files/.local/share/gajim/logs.db "select time, message from logs where contact_name = 'iank'" | less
3542 }
3543
3544 gajlogs() {
3545 sqlite3 -separator ' ' /p/c/subdir_files/.local/share/gajim/logs.db "select time, message from logs" | less
3546 }
3547
3548
3549 net-dev-info() {
3550 e "lspci -nnk|gr -iA2 net"
3551 lspci -nnk|gr -iA2 net
3552 hr
3553 e "s lshw -C network"
3554 hr
3555 sudo lshw -C network
3556 }
3557
3558 nk() {
3559 ser stop NetworkManager
3560 ser disable NetworkManager
3561 ser stop NetworkManager-wait-online.service
3562 ser disable NetworkManager-wait-online.service
3563 ser stop dnsmasq
3564 sudo resolvconf -d NetworkManager
3565 # ser start dnsmasq
3566 sudo ifup br0
3567 }
3568 ngo() {
3569 sudo ifdown br0
3570 ser start NetworkManager
3571 sleep 4
3572 sudo nmtui-connect
3573 }
3574
3575 otp() {
3576 oathtool --totp -b "$*" | xclip -selection clipboard
3577 }
3578 # run cmd and copy output
3579 j() {
3580 "$@" |& pee "xclip -r -selection clipboard" cat
3581 }
3582
3583 # xorg copy. copy text piped into command
3584 xc() {
3585 xclip -r -selection clipboard
3586 }
3587 # echo copy
3588 ec() {
3589 pee "xclip -r -selection clipboard" cat
3590 }
3591
3592 pakaraoke() {
3593 # from http://askubuntu.com/questions/456021/remove-vocals-from-mp3-and-get-only-instrumentals
3594 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
3595 }
3596
3597 pfind() { #find *$1* in $PATH
3598 [[ $# != 1 ]] && { echo requires 1 argument; return 1; }
3599 local pathArray
3600 IFS=: pathArray=($PATH); unset IFS
3601 find "${pathArray[@]}" -iname "*$1*"
3602 }
3603
3604 pick-trash() {
3605 # trash-restore lists everything that has been trashed at or below CWD
3606 # This picks out files just in CWD, not subdirectories,
3607 # which also match grep $1, usually use $1 for a time string
3608 # which you get from running restore-trash once first
3609 local name x ask
3610 local nth=1
3611 # last condition is to not ask again for ones we skipped
3612 while name="$( echo | restore-trash | gr "$PWD/[^/]\+$" | gr "$1" )" \
3613 && [[ $name ]] && (( $(wc -l <<<"$name") >= nth )); do
3614 name="$(echo "$name" | head -n $nth | tail -n 1 )"
3615 read -r -p "$name [Y/n] " ask
3616 if [[ ! $ask || $ask == [Yy] ]]; then
3617 x=$( echo "$name" | gr -o "^\s*[0-9]*" )
3618 echo $x | restore-trash > /dev/null
3619 elif [[ $ask == [Nn] ]]; then
3620 nth=$((nth+1))
3621 else
3622 return
3623 fi
3624 done
3625 }
3626
3627
3628 pub() {
3629 rld /a/h/_site/ li:/var/www/iankelling.org/html
3630 }
3631
3632
3633 pumpa() {
3634 # fixes the menu bar in xmonad. this won\'t be needed when xmonad
3635 # packages catches up on some changes in future (this is written in
3636 # 4/2017)
3637 #
3638 # geekosaur: so youll want to upgrade to xmonad 0.13 or else use a
3639 # locally modified XMonad.Hooks.ManageDocks that doesnt set the
3640 # work area; turns out it\'s impossible to set correctly if you are
3641 # not a fully EWMH compliant desktop environment
3642 #
3643 # geekosaur: chrome shows one failure mode, qt/kde another, other
3644 # gtk apps a third, ... I came up with a setting that works for me
3645 # locally but apparently doesnt work for others, so we joined the
3646 # other tiling window managers in giving up on setting it at all
3647 #
3648 xprop -root -remove _NET_WORKAREA
3649 command pumpa & r
3650 }
3651
3652 # reviewboard, used at my old job
3653 #rbpipe() { rbt post -o --diff-filename=- "$@"; }
3654 #rbp() { rbt post -o "$@"; }
3655
3656 rebr() {
3657 sudo ifdown br0
3658 sudo ifup br0
3659 }
3660
3661
3662 r2e() { command r2e -d /p/c/rss2email.json -c /p/c/rss2email.cfg "$@"; }
3663 # only run on MAIL_HOST. simpler to keep this on one system.
3664 r2eadd() { # usage: name url
3665 # initial setup of rss2email:
3666 # r2e new r2e@iankelling.org
3667 # that initializes files, and sets default email.
3668 # symlink to the config doesnt work, so I copied it to /p/c
3669 # and then use cli option to specify explicit path.
3670 # Only option changed from default config is to set
3671 # force-from = True
3672 #
3673 # or else for a few feeds, the from address is set by the feed, and
3674 # if I fail delivery, then I send a bounce message to that from
3675 # address, which makes me be a spammer.
3676
3677 r2e add $1 "$2" $1@r2e.iankelling.org
3678 # get up to date and dont send old entries now:
3679 r2e run --no-send $1
3680 }
3681
3682 rspicy() { # usage: HOST DOMAIN
3683 # connect to spice vm remote host. use vspicy for local host
3684 local port
3685 # shellcheck disable=SC2087
3686 port=$(ssh $1<<EOF
3687 sudo virsh dumpxml $2|grep "<graphics.*type='spice'" | \
3688 sed -rn "s/.*port='([0-9]+).*/\1/p"
3689 EOF
3690 )
3691 if [[ $port ]]; then
3692 spicy -h $1 -p $port
3693 else
3694 echo "error: no port found. check that the domain is running."
3695 fi
3696 }
3697
3698
3699 scssl() {
3700 # s gem install scss-lint
3701 pushd /a/opt/thoughtbot-guides
3702 git pull --stat
3703 popd
3704 scss-lint -c /a/opt/thoughtbot-guides/style/sass/.scss-lint.yml "$@"
3705 }
3706
3707 skbrc() {
3708 sk -e 2120,245 /b/ds/brc /b/ds/brc2
3709 }
3710
3711 skaraoke() {
3712 local tmp out
3713 out=${2:-${1%.*}.sh}
3714 tmp=$(mktemp -d)
3715 script -t -c "mpv --no-config --no-resume-playback --no-terminal --no-audio-display '$1'" $tmp/typescript 2>$tmp/timing
3716 # todo, the current sleep seems pretty good, but it
3717 # would be nice to have an empirical measurement, or
3718 # some better wait to sync up.
3719 #
3720 # note: --loop-file=no prevents it from hanging if you have that
3721 # set to inf the mpv config.
3722 # --loop=no prevents it from exit code 3 due to stdin if you
3723 # had it set to inf in mpv config.
3724 #
3725 # args go to mpv, for example --volume=80, 50%
3726 cat >$out <<EOFOUTER
3727 #!/bin/bash
3728 trap "trap - TERM && kill 0" INT TERM ERR; set -e
3729 ( sleep .2; scriptreplay <( cat <<'EOF'
3730 $(cat $tmp/timing)
3731 EOF
3732 ) <( cat <<'EOF'
3733 $(cat $tmp/typescript)
3734 EOF
3735 ))&
3736 base64 -d - <<'EOF'| mpv --loop=no --loop-file=no --no-terminal --no-audio-display "\$@" -
3737 $(base64 "$1")
3738 EOF
3739 kill 0
3740 EOFOUTER
3741 rm -r $tmp
3742 chmod +x $out
3743 }
3744
3745 # ssh meld. usage: host1 host2 file
3746 smeld() {
3747 meld <(ssh $1 cat $3) <(ssh $2 cat $3)
3748 }
3749
3750 # remote file meld
3751 # usage: host file1 file2
3752 rmeld() {
3753 local tmpdir
3754 tmpdir=$(mktemp -d)
3755 scp "$1:$2" "$1:$3" $tmpdir
3756 meld "$tmpdir/${2##*/}" "$tmpdir/${3##*/}"
3757 }
3758
3759
3760 spd() {
3761 PATH=/usr/local/spdhackfix:$PATH command spd "$@"
3762 }
3763
3764 spamf() { # spamtest on FILE
3765 if (( $# != 1 )); then
3766 e spamtest error: expected 1 arg, filename >&2
3767 return 1
3768 fi
3769 sdncmdroot spamassassin sudo -u Debian-exim spamassassin -t --cf='score PYZOR_CHECK 0' <"$1"
3770 }
3771
3772
3773 # mail related
3774 testmail() {
3775 declare -gi _seq; _seq+=1
3776 echo "test body" | m mail -s "test mail from $HOSTNAME, $_seq" "${@:-root@localhost}"
3777 # for testing to send from an external address, you can do for example
3778 # -fian@iank.bid -aFrom:ian@iank.bid web-6fnbs@mail-tester.com
3779 # note in exim, you can retry a deferred message
3780 # s exim -M MSG_ID
3781 # MSG_ID is in /var/log/exim4/mainlog, looks like 1ccdnD-0001nh-EN
3782 }
3783
3784 # to test sieve, use below command. for fsf mail, see offlineimap-sync script
3785 # make modifications, then copy to live file, use -eW to actually modify mailbox
3786 #
3787 # Another option is to use sieve-test SCRIPT MAIL_FILE. note,
3788 # sieve-test doesnt know about envelopes, Im not sure if sieve-filter does.
3789
3790 # sieve with output filter. arg is mailbox, like INBOX.
3791 # This depends on dovecot conf, notably mail_location in /etc/dovecot/conf.d/10-mail.conf
3792
3793 # always run this first, edit the test files, then run the following
3794 testsieve() {
3795 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
3796 }
3797 runsieve() {
3798 c ~/sieve; cp personal{test,}.sieve; cp lists{test,}.sieve; cp personalend{test,}.sieve
3799 sieve-filter -eWv ~/sieve/maintest.sieve ${1:-INBOX} delete &> /tmp/testsieve.log
3800 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
3801 }
3802
3803 # usage:
3804 # alertme SUBJECT
3805 # printf "subject\nbody\n" | alertme
3806 alertme() {
3807 if [[ -t 0 ]]; then
3808 exim -t <<EOF
3809 From: alertme@b8.nz
3810 To: alerts@iankelling.org
3811 Subject: $*
3812 EOF
3813 else
3814 read -r sub
3815 { cat <<EOF
3816 From: alertme@b8.nz
3817 To: alerts@iankelling.org
3818 Subject: $sub
3819
3820 EOF
3821 cat
3822 } | exim -t
3823 fi
3824 }
3825 daylertme() {
3826 if [[ -t 0 ]]; then
3827 exim -t <<EOF
3828 From: alertme@b8.nz
3829 To: daylert@iankelling.org
3830 Subject: $*
3831 EOF
3832 else
3833 read -r sub
3834 { cat <<EOF
3835 From: alertme@b8.nz
3836 To: daylert@iankelling.org
3837 Subject: $sub
3838
3839 EOF
3840 cat
3841 } | exim -t
3842 fi
3843 }
3844
3845 # alert when a page goes live.
3846 alert200() {
3847 local quiet url tmpdir
3848 quiet=false
3849 case $1 in
3850 # dont send a diff of the html. some html is not very readable
3851 -q) quiet=true
3852 shift
3853 ;;
3854 esac
3855 url="$1"
3856 tmpdir="$(mktemp -d)"
3857 cd $tmpdir
3858 while true; do
3859 if wget -q "$url"; then
3860 if $quiet; then
3861 echo | daylert 200
3862 else
3863 alertme $tmpdir
3864 fi
3865 fi
3866 sleep $(( 120 + RANDOM % 300 ))
3867 done
3868 }
3869
3870 # alert on changes to a webpage (just the base page that curl gets)
3871 # usage: weblert URL [SUBJECT...]
3872 weblert() {
3873 local u old new quiet
3874 quiet=false
3875 case $1 in
3876 # dont send a diff of the html. some html is not very readable
3877 -q) quiet=true
3878 shift
3879 ;;
3880 esac
3881 u="$1"
3882 shift
3883 subject="${*:-weblert}"
3884 old=$(curl -s "$u") ||:
3885 while true; do
3886 new=$(curl -s "$u") ||:
3887 if [[ $old && $new ]]; then
3888 if [[ $new != "$old" ]]; then
3889 if $quiet; then
3890 echo | daylertme "$subject"
3891 else
3892 diff <(printf "%s\n" "$old") <(printf "%s\n" "$new") | daylertme "$subject" ||:
3893 fi
3894 fi
3895 old="$new"
3896 fi
3897 sleep $(( 60 + RANDOM % 120 ))
3898 done
3899 }
3900
3901 torshell() {
3902 # per man torsocks
3903 # shellcheck disable=SC1090 # expected
3904 source "$(type -p torsocks)" on
3905 }
3906
3907 eless2() {
3908 less /var/log/exim4/nondmain
3909 }
3910
3911
3912 # mail related
3913 testexim() {
3914 # testmail above calls sendmail, which is a link to exim/postfix.
3915 # its docs dont say a way of adding an argument
3916 # to sendmail to turn on debug output. We could make a wrapper, but
3917 # that is a pain. Exim debug args are documented here:
3918 # http://www.exim.org/exim-html-current/doc/html/spec_html/ch-the_exim_command_line.html
3919 #
3920 # http://www.exim.org/exim-html-current/doc/html/spec_html/ch-building_and_installing_exim.html
3921 # note, for exim daemon, you can turn on debug options by
3922 # adding -d, etc to COMMONOPTIONS in
3923 # /etc/default/exim4
3924 #
3925 # to specify recipients other than those in to, cc, bcc, you can use the cli args, eg:
3926 # exim -t 'test@zroe.org, t2@zroe.org' <<'EOF'
3927 #
3928 # -t = get recipient from header
3929 exim -d -t <<EOF
3930 From: ian@iankelling.org
3931 To: submit@b.b8.nz
3932 Subject: testbug1
3933
3934 Package: test
3935 Version:1
3936
3937 This is a test message.
3938 EOF
3939 }
3940
3941 # test bounce exim
3942 testbexim() {
3943 to=$1
3944 exim -d -f '<>' $to <<EOF
3945 From: Mail Delivery System <Mailer-Daemon@gnu.org>
3946 To: $to
3947 Subject: Mail delivery failed: returning message to sender
3948
3949 This message was created automatically by mail delivery software.
3950 EOF
3951
3952 }
3953
3954
3955 # toggle keyboard
3956 tk() {
3957 # based on
3958 # https://askubuntu.com/questions/160945/is-there-a-way-to-disable-a-laptops-internal-keyboard
3959 id=$(xinput --list --id-only 'AT Translated Set 2 keyboard')
3960 if xinput list | grep -F '∼ AT Translated Set 2 keyboard' &>/dev/null; then
3961 echo enabling keyboard
3962 # find the first slave keyboard number, they are all the same in my output.
3963 # if they werent, worst case we would need to save the slave number somewhere
3964 # when it got disabled.
3965 slave=$(xinput list | sed -n 's/.*slave \+keyboard (\([0-9]*\)).*/\1/p' | head -n1)
3966 xinput reattach $id $slave
3967 else
3968 xinput float $id
3969 fi
3970 }
3971
3972 tm() {
3973 # timer in minutes
3974 # --no-config
3975 (sleep "$(calc "$* * 60")" && mpv --no-config --volume 50 /a/bin/data/alarm.mp3) > /dev/null 2>&1 &
3976 }
3977
3978 ## usage: to connect to my main transmission daemon from a different host, run this
3979 trans-remote-route() {
3980 :
3981 }
3982 trg() { transmission-remote-gtk & r; }
3983 # TODO: this wont work transmission.lan doesnt exist
3984 trc() {
3985 # example, set global upload limit to 100 kilobytes:
3986 # trc -u 100
3987 TR_AUTH=":$(jq -r .profiles[0].password ~/.config/transmission-remote-gtk/config.json)" transmission-remote transmission.lan -ne "$@"
3988 }
3989
3990 trysleep() {
3991 retries="$1"
3992 sleepsecs="$2"
3993 shift 2
3994 for (( i=0; i < retries - 1; i++ )); do
3995 if "$@"; then
3996 return 0
3997 fi
3998 sleep $sleepsecs
3999 done
4000 "$@"
4001 }
4002
4003
4004 tu() {
4005 local s
4006 if [[ -e $1 && ! -w $1 || ! -w $(dirname "$1") ]]; then
4007 s=s;
4008 fi
4009 # full path for using in some initial setup steps
4010 $s /a/exe/teeu "$@"
4011 }
4012
4013 # execute exim in its namespace. Useful args like -Mrm
4014 enn() {
4015 local ecmd pid
4016
4017 ecmd="/usr/sbin/exim4 -C /etc/exim4/nn-mainlog.conf"
4018 if ip a show veth1-mail &>/dev/null; then
4019 s $ecmd "$@"
4020 else
4021 sdncmdroot exim4 $ecmd "$@"
4022 fi
4023 }
4024
4025 # get pid of systemd service
4026 servicepid() {
4027 local pid unit dir
4028 unit="$1"
4029 pid=$(systemctl show --property MainPID --value "$unit")
4030 case $pid in
4031 [1-9]*) : ;;
4032 *)
4033
4034 dir=/sys/fs/cgroup/system.slice
4035 if [[ ! -d $dir ]]; then
4036 # t10 and older directory.
4037 dir=/sys/fs/cgroup/systemd/system.slice
4038 fi
4039
4040 # 0 or empty. This file includes the MainPid, so I expect we
4041 # could just get this in the first place, but i don't know if that
4042 # is always the case.
4043 pid=$(head -n1 $dir/${unit%.service}.service/cgroup.procs)
4044 ;;
4045 esac
4046 if [[ $pid ]]; then
4047 printf "%s\n" "$pid"
4048 else
4049 return 1
4050 fi
4051 }
4052
4053 sdnbash() { # systemd namespace bash
4054 local unit pid
4055 if (( $# != 1 )); then
4056 echo $0: error wrong number of args >&2
4057 return 1
4058 fi
4059 unit=$1
4060 pid=$(servicepid $unit)
4061 m sudo nsenter -t $pid -n -m sudo -u $USER -i bash
4062 }
4063
4064 sdnbashroot() { # systemd namespace bash as root
4065 local unit pid
4066 if (( $# != 1 )); then
4067 echo $0: error wrong number of args >&2
4068 return 1
4069 fi
4070 unit=$1
4071 pid=$(servicepid $unit)
4072 m sudo nsenter -t $pid -n -m bash
4073 }
4074
4075
4076 # systemd namespace cmd
4077 # usage: UNIT CMD...
4078 sdncmd() {
4079 local unit pid tmpf
4080 if (( $# <= 1 )); then
4081 echo $0: error wrong number of args >&2
4082 return 1
4083 fi
4084 unit=$1
4085 shift
4086 pid=$(servicepid $unit)
4087 tmpf=$(mktemp --tmpdir $unit.XXXXXXXXXX)
4088 export -p >$tmpf
4089 printf "%s " "${@@Q}" >>$tmpf
4090 echo >>$tmpf
4091 m sudo nsenter -t $pid -n -m sudo -u $USER -i bash -c ". $tmpf & rm $tmpf"
4092 }
4093
4094 sdncmdroot() { # systemd namespace root command
4095 local unit pid
4096 if (( $# < 2 )); then
4097 echo $0: error wrong number of args >&2
4098 return 1
4099 fi
4100 unit=$1
4101 shift
4102 pid=$(servicepid $unit)
4103 m sudo nsenter -t $pid -n -m "$@"
4104 }
4105
4106
4107 mailnnbash() {
4108 sdnbash mailnn
4109 }
4110
4111 # we use wireguard now, use mailnnbash.
4112 # mailvpnbash() {
4113 # m sudo nsenter -t $(pgrep -f "/usr/sbin/openvpn .* --config /etc/openvpn/.*mail.conf") -n -m sudo -u $USER -i bash
4114 # }
4115
4116 eximbash() {
4117 sdnbashroot exim4
4118 }
4119 spamnn() {
4120 local spamdpid
4121 spamdpid=$(systemctl show --property MainPID --value spamassassin)
4122 m sudo nsenter -t $spamdpid -n -m sudo -u Debian-exim spamassassin "$@"
4123 }
4124 unboundbash() {
4125 sdnbashroot unbound
4126 }
4127
4128 nmtc() {
4129 s nmtui-connect "$@"
4130 }
4131
4132 # check exim and others network namespace
4133 mailnncheck() {
4134 local unit pid ns mailnn spamd_ser
4135
4136 spamd_ser=spamd
4137 if systemctl cat spamassassin &>/dev/null; then
4138 spamd_ser=spamassassin
4139 fi
4140
4141 # mailvpn would belong on the list if using openvpn
4142 for unit in mailnn unbound dovecot $spamd_ser exim4 radicale; do
4143 pid=$(servicepid $unit)
4144 echo debug: unit=$unit pid=$pid
4145 if [[ ! $pid ]]; then
4146 echo failed to find pid for unit=$unit
4147 continue
4148 fi
4149 if ! ns=$(s readlink /proc/$pid/ns/net); then
4150 echo failed to find ns for unit=$unit pid=$pid
4151 continue
4152 fi
4153 if [[ $mailnn ]]; then
4154 if [[ $ns != "$mailnn" ]]; then
4155 echo "$unit ns $ns != $mailnn"
4156 fi
4157 else
4158 mailnn=$ns
4159 fi
4160 done
4161
4162 }
4163
4164
4165 vpncmd() {
4166 sdncmd openvpn-client-tr@client.service "$@"
4167 }
4168 vpni() {
4169 sdncmd openvpn-client-tr@client.service bash
4170 }
4171 vpnbash() {
4172 sdncmdroot openvpn-client-tr@client.service bash
4173 }
4174
4175
4176 vpn() {
4177 if [[ -e /lib/systemd/system/openvpn-client@.service ]]; then
4178 local vpn_service=openvpn-client
4179 else
4180 local vpn_service=openvpn
4181 fi
4182
4183 [[ $1 ]] || { echo need arg; return 1; }
4184 journalctl --since=now --unit=$vpn_service@$1 -f -n0 &
4185 sudo systemctl start $vpn_service@$1
4186 # sometimes the ask-password agent does not work and needs a delay.
4187 sleep .5
4188 # https://bugs.debian.org/cgi-bin/bugreport.cgi?bug=779240
4189 # noticed around 8-2017 after update from around stretch release
4190 # on debian testing, even though the bug is much older.
4191 sudo systemd-tty-ask-password-agent
4192 }
4193
4194 fixu() {
4195 local stats
4196 ls -lad /run/user/1000
4197 stats=$(stat -c%a-%g-%u /run/user/1000)
4198 if [[ $stats != 700-1000-1000 ]]; then
4199 m s chmod 700 /run/user/1000; m s chown iank.iank /run/user/1000
4200 fi
4201 }
4202
4203 # unmute desktop output
4204 um() {
4205 local sink card sedcmd
4206 sink=$(pactl get-default-sink)
4207 if [[ $sink == auto_null ]]; then
4208 # guessing there is just one with an off profile. otherwise we will
4209 # need some other solution, like storing the card identifier that we
4210 # muted with nap. Or, we could so some hakery with
4211 # pactl -f json.
4212 sedcmd='/^[[:space:]]*index:/{s/^[[:space:]]*index://;h};/^[[:space:]]*active profile: <off>$/{g;p;q}'
4213 card=$(pacmd list-cards | sed -n "$sedcmd")
4214 m pacmd set-card-profile "$card" output:analog-stereo
4215 fi
4216
4217 m pactl set-sink-mute @DEFAULT_SINK@ false
4218 rm -f /tmp/ianknap
4219 }
4220
4221 nap() {
4222 local sink card
4223 sink=$(pactl get-default-sink)
4224 card="${sink%.*}"
4225 card="${card/output/card}"
4226 m pacmd set-card-profile "$card" off
4227
4228 # clicking on a link in a browser can cause unmute.
4229 # I don't want that. So, use a stronger form of mute
4230 # than this.
4231 #pactl set-sink-mute @DEFAULT_SINK@ true
4232 touch /tmp/ianknap
4233 }
4234
4235
4236 # systemctl is-enabled / status / cat says nothing, instead theres
4237 # some obscure symlink. paths copied from man systemd.unit.
4238 # possibly also usefull, but incomplete, doesnt show units not loaded in memory:
4239 # seru list-dependencies --reverse --all UNIT
4240 sysd-deps() {
4241 local f
4242 local -a dirs search
4243 ngset
4244
4245 case $1 in
4246 u)
4247 search=(
4248 ~/.config/systemd/user.control/*
4249 $XDG_RUNTIME_DIR/systemd/user.control/*
4250 $XDG_RUNTIME_DIR/systemd/transient/*
4251 $XDG_RUNTIME_DIR/systemd/generator.early/*
4252 ~/.config/systemd/user/*
4253 /etc/systemd/user/*
4254 $XDG_RUNTIME_DIR/systemd/user/*
4255 /run/systemd/user/*
4256 $XDG_RUNTIME_DIR/systemd/generator/*
4257 ~/.local/share/systemd/user/*
4258 /usr/lib/systemd/user/*
4259 $XDG_RUNTIME_DIR/systemd/generator.late/*
4260 )
4261 ;;
4262 *)
4263 search=(
4264 /etc/systemd/system.control/*
4265 /run/systemd/system.control/*
4266 /run/systemd/transient/*
4267 /run/systemd/generator.early/*
4268 /etc/systemd/system/*
4269 /etc/systemd/systemd.attached/*
4270 /run/systemd/system/*
4271 /run/systemd/systemd.attached/*
4272 /run/systemd/generator/*
4273 /lib/systemd/system/*
4274 /run/systemd/generator.late/*
4275 )
4276 ;;
4277 esac
4278 for f in "${search[@]}"; do
4279 [[ -d $f ]] || continue
4280 case $f in
4281 *.requires|*.wants)
4282 dirs+=("$f")
4283 ;;
4284 esac
4285 done
4286 # dirs is just so we write out the directory names, ls does it when there is 2 or more dirs.
4287 case ${#dirs[@]} in
4288 1)
4289 echo "${dirs[0]}:"
4290 ll "${dirs[@]}"
4291 ;;
4292 0) : ;;
4293 *)
4294 ll "${dirs[@]}"
4295 ;;
4296 esac
4297 ngreset
4298 }
4299
4300 fixvpndns() {
4301 local link istls
4302 read -r _ link _ istls < <(resolvectl dnsovertls tunfsf)
4303 case $istls in
4304 yes|no) : ;;
4305 *) echo fixvpndns error: unexpected istls value: $istls >&2; return 1 ;;
4306 esac
4307 s busctl call org.freedesktop.resolve1 /org/freedesktop/resolve1 org.freedesktop.resolve1.Manager SetLinkDNSOverTLS is $link no
4308 }
4309
4310 vpnoff() {
4311 [[ $1 ]] || { echo need arg; return 1; }
4312 if [[ -e /lib/systemd/system/openvpn-client@.service ]]; then
4313 local vpn_service=openvpn-client
4314 else
4315 local vpn_service=openvpn
4316 fi
4317 sudo systemctl stop $vpn_service@$1
4318 }
4319 vpnoffc() { # vpn off client
4320 ser stop openvpn-client-tr@client
4321 }
4322 vpnc() {
4323 local unit
4324 unit=openvpn-client-tr@client
4325 sudo -v
4326 if [[ $(systemctl is-active $unit) != active ]]; then
4327 s systemctl start $unit
4328 sleep 1
4329 fi
4330 }
4331
4332
4333 vspicy() { # usage: VIRSH_DOMAIN
4334 # connect to vms made with virt-install
4335 spicy -p "$(sudo virsh dumpxml "$1"|grep "<graphics.*type='spice'"|\
4336 sed -r "s/.*port='([0-9]+).*/\1/")"
4337 }
4338
4339 wian() {
4340 cat-new-files /m/4e/INBOX/new
4341 }
4342 wakehours() {
4343 local sec
4344 if (( $# != 1 )) ; then
4345 echo wakehours: error: expected 1 arg, got $# >&2
4346 return 1
4347 fi
4348 sec=$(( EPOCHSECONDS - $( date +%s -d $1am ) ))
4349 printf "%d:%02d\n" $(( sec / 60 / 60)) $(( (sec / 60) % 60 ))
4350 }
4351
4352 calvis() { # calendar visualize
4353 install -m 600 /dev/null /tmp/calendar-bytes
4354 while read -r l; do
4355 for char in $l; do
4356 # shellcheck disable=SC2059 # intentional for the hex formatting
4357 printf "\x$(printf "%x" $char)" >>/tmp/calendar-bytes
4358 done
4359 done < <(grep -v '[#-]' /p/calendar-data)
4360 /p/c/proc/calendar/linux-amd64/calendar
4361 }
4362
4363 wtr() { curl wttr.in/boston; }
4364
4365 xevkb() { xev -event keyboard; }
4366
4367 # * misc stuff
4368
4369 vrun() {
4370 printf "running: %s\n" "$*"
4371 "$@"
4372 }
4373
4374 electrum() {
4375 # Running the appimage said fuse was not available, but try
4376 # running the appimage with --appimage-extract, which worked.
4377 # It seems there is no need to backup the wallet, it can be restored
4378 # via the seed onto any computer that needs it.
4379 /a/opt/electrum/squashfs-root/AppRun "$@"
4380
4381
4382 # This was an old way I ran electrum over tor, and seems like I
4383 # imported a bitcoin core wallet.
4384 #
4385 # https://electrum.readthedocs.io/en/latest/tor.html
4386 # https://github.com/spesmilo/electrum-docs/issues/129
4387 # s rsync -ptog --chown bitcoin:bitcoin ~/.Xauthority /var/lib/bitcoind/.Xauthority
4388 # sudo -u bitcoin DISPLAY=$DISPLAY XAUTHORITY=/var/lib/bitcoind/.Xauthority /a/opt/electrum-4.2.1-x86_64.AppImage -p socks5:localhost:9050
4389
4390 }
4391
4392
4393 monero() {
4394 sudo -u bitcoin DISPLAY=$DISPLAY XAUTHORITY=/var/lib/bitcoind/.Xauthority /a/opt/monero-gui-v0.17.3.2/monero-wallet-gui
4395 }
4396
4397
4398 # grep + find
4399 gef() {
4400 faf | grep -E "$@" ||:
4401 rgv "$@"
4402 }
4403
4404 # rg my main files
4405 rgm() {
4406 rg "$@" /p/w.org /a/t.org /a/work.org /b
4407 }
4408
4409 # re all my files more expansively.
4410 # usage [-OPT...] regex space combined
4411 rem() {
4412 local paths
4413 local -a opts
4414 for arg; do
4415 if [[ $arg == -* ]]; then
4416 opts+=("$1")
4417 shift
4418 else
4419 break
4420 fi
4421 done
4422 paths="/p/c /b/"
4423 find $paths -not \( -name .svn -prune -o -name .git -prune \
4424 -o -name .hg -prune -o -name .editor-backups -prune \
4425 -o -name .undo-tree-history -prune \) 2>/dev/null | grep -iP --color=auto -- "$*" ||:
4426 rgv $local_rgv_args -g "!bash_unpublished" "${opts[@]}" -- "$*" $paths /a/work.org ||:
4427 }
4428 reml() { # rem with limit to 5 matches per file
4429 local_rgv_args="-m 5"
4430 rem "$@"
4431 }
4432
4433 rep() {
4434 local paths
4435 paths="/p/c"
4436 find $paths -not \( -name .svn -prune -o -name .git -prune \
4437 -o -name .hg -prune -o -name .editor-backups -prune \
4438 -o -name .undo-tree-history -prune \) 2>/dev/null | grep -iP --color=auto -- "$*" ||:
4439 rgv $local_rgv_args -- "$*" $paths /a/t.org /p/w.org ||:
4440 }
4441 repl() { # rem with limit to 5 matches per file
4442 local local_rgv_args="-m 5"
4443 rem "$@"
4444 }
4445
4446
4447 # re on common fsf files
4448 ref() {
4449 local paths
4450 paths="/f/gluestick /f/brains /f/s /c"
4451 find $paths -not \( -name .svn -prune -o -name .git -prune \
4452 -o -name .hg -prune -o -name .editor-backups -prune \
4453 -o -name .undo-tree-history -prune \) 2>/dev/null | grep -iP --color=auto -- "$*" ||:
4454 rgv -- "$*" $paths /a/work.org ||:
4455 }
4456
4457
4458 # for use in /f/bind
4459 fupzone() {
4460 # shellcheck disable=SC2046 # i want word splitting
4461 ./update-zone $(i s | sed -rn 's/.*db\.(.*)/\1/p')
4462 }
4463
4464 # setup:
4465 # pip3 install linode-cli
4466 # linode-cli
4467 livp9() {
4468 local input ip id tmp
4469 input=$1
4470 if [[ $2 ]]; then
4471 id=$2
4472 ip=$3
4473 else
4474 tmp=$(mktemp)
4475 echo $tmp
4476 linode-cli --json --pretty linodes create --root_pass loxHuceygomGisun | tee $tmp
4477 read -r ip id <<<"$(tail -n+2 $tmp | jq -r '.[0].ipv4[0] , .[0].id')"
4478 for string in $ip $id; do
4479 case $string in
4480 [0-9]*) : ;;
4481 *)
4482 echo "livp9: bad value ip=$ip id=$id input=$input"
4483 return 1
4484 ;;
4485 esac
4486 done
4487 rm $tmp
4488
4489 while true; do
4490 if timeout 4 ssh $ip :; then
4491 break
4492 fi
4493 sleep 3
4494 done
4495 fi
4496 ssh $ip <<EOF
4497 apt-get -qq update
4498 apt-get -qq -y install ffmpeg rsync
4499 mkdir vp9
4500 EOF
4501 m rsync $input $ip:
4502 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
4503 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
4504 rsync $ip:vp9/$input vp9
4505 linode-cli linodes delete $id
4506 }
4507
4508 reset-konsole() {
4509 # we also have a file in /a/c/...konsole...
4510 local f=$HOME/.config/konsolerc
4511 setini DefaultProfile profileian.profile "Desktop Entry" $f
4512 setini Favorites profileian.profile "Favorite Profiles" $f
4513 setini ShowMenuBarByDefault false KonsoleWindow $f
4514 setini TabBarPosition Top TabBar $f
4515 }
4516
4517 reset-sakura() {
4518 while read -r k v; do
4519 # shellcheck disable=SC2154
4520 setini $k $v sakura /a/c/subdir_files/.config/sakura/sakura.conf
4521 done <<'EOF'
4522 colorset1_back rgb(33,37,39)
4523 less_questions true
4524 audible_bell No
4525 visible_bell No
4526 disable_numbered_tabswitch true
4527 scroll_lines 10000000
4528 scrollbar true
4529 EOF
4530 }
4531
4532 # make a page of links found in the files $@. redirect output
4533 linkhtml() {
4534 gr -oh 'https?:\/\/(www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_\+.~#?&//=]*)' "$@" | \
4535 rev | sort -u | rev | sed 's,.*,<a href="\0">\0</a><br\>,'
4536 }
4537
4538 reset-xscreensaver() {
4539 # except for spash, i set these by setting gui options in
4540 # xscreensaver-command -demo
4541 # then finding the corresponding option in .xscreensaver
4542 # spash, i happened to notice in .xscreensaver
4543 #
4544 # dpmsOff, monitor doesnt come back on using old free software supported nvidia card
4545 cat > /home/iank/.xscreensaver <<'EOF'
4546 mode: blank
4547 dpmsEnabled: True
4548 dpmsStandby: 0:07:00
4549 dpmsSuspend: 0:08:00
4550 dpmsOff: 0:00:00
4551 timeout: 0:05:00
4552 lock: True
4553 lockTimeout: 0:06:00
4554 splash: False
4555 EOF
4556
4557 }
4558
4559
4560 # very useful, copy directory structure 3 deep. add remove /*/ to change level
4561 # rsync -aivh --exclude '/*/*/*/' -f"+ */" -f"- *" SRC DEST
4562
4563
4564 # * stuff that makes sense to be at the end
4565 if [[ "$SUDOD" ]]; then
4566 # allow failure, for example if we are sudoing into a user with diffferent/lesser permissions.
4567 cd "$SUDOD" ||:
4568 unset SUDOD
4569 elif [[ -d /a ]] && [[ $PWD == "$HOME" ]] && [[ $- == *i* ]]; then
4570 cd /a
4571 OLDPWD=
4572 fi
4573
4574
4575
4576
4577 # for mitmproxy to get a newer python.
4578 # commented until i want to use it because it
4579 # noticably slows bash startup
4580 #
4581
4582 mypyenvinit () {
4583 if [[ $EUID == 0 || ! -e ~/.pyenv/bin ]]; then
4584 echo "error: dont be root. make sure pyenv is installed"
4585 return 1
4586 fi
4587 export PATH="$HOME/.pyenv/bin:$PATH"
4588 eval "$(pyenv init -)"
4589 eval "$(pyenv virtualenv-init -)"
4590 }
4591
4592
4593
4594 # I have the git repo and a release. either one should work.
4595 # I have both because I was trying to solve an issue that
4596 # turned out to be unrelated.
4597 # ARDUINO_PATH=/a/opt/Arduino/build/linux/work
4598
4599 ## i should have documented this...
4600 # based on https://github.com/keyboardio/Kaleidoscope
4601 export KALEIDOSCOPE_DIR=/a/opt/Kaleidoscope
4602
4603 # They want to be added to the start, but i think
4604 # that should be avoided unless we really need it.
4605 path-add --end ~/.npm-global
4606
4607
4608 path-add --end $HOME/.cargo/bin
4609
4610 if type -P rg &>/dev/null; then
4611 # --no-messages because of annoying errors on broken symlinks
4612 # -z = search .gz etc files
4613 # -. = search dotfiles
4614 # -n --no-heading: show files and line numbers together allowing for clicking
4615 rg() {
4616 local path_arg
4617 if [[ ${#@} == 1 ]]; then
4618 path_arg=.
4619 fi
4620
4621 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 $?
4622 }
4623 #fails if not exist. ignore
4624 complete -r rg 2>/dev/null ||:
4625 else
4626 alias rg=grr
4627 fi
4628
4629 # rg with respecting vcs ignore files
4630 rgv() {
4631 local path_arg ret=0
4632 if [[ ${#@} == 1 ]]; then
4633 path_arg=.
4634 fi
4635 # settings that are turned off for pipes, keep them on.
4636 # Found by searching for "terminal" in --help
4637 # --heading
4638 # -n
4639 #
4640 # -. = search dotfiles
4641 # -z = search zipped files
4642 # -i = case insensitive
4643 # -M = max columns
4644 # -n --no-heading: show files and line numbers together allowing for clicking
4645 # --no-messages because of annoying errors on broken symlinks
4646 # --no-ignore-parent because i have /a/.git which ignores almost everything under it.
4647 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=$?
4648 return $ret
4649 }
4650
4651 amall() {
4652 echo "$(tput setaf 5 2>/dev/null ||:)â–ˆ coresite â–ˆ$(tput sgr0 2>/dev/null||:)"
4653 amfsf "$@"
4654 echo "$(tput setaf 5 2>/dev/null ||:)â–ˆ office â–ˆ$(tput sgr0 2>/dev/null||:)"
4655 amoffice "$@"
4656 }
4657 amallq() { # amall quiet
4658 amfsf "$@"
4659 amoffice "$@"
4660 }
4661 amfsf() {
4662 sedi -r '/alertmanager.url/s/@prom.office/@prom/' ~/.config/amtool/config.yml
4663 amtool "$@"
4664 }
4665 amoffice() {
4666 sedi -r '/alertmanager.url/s/@prom.fsf/@prom.office.fsf/' ~/.config/amtool/config.yml
4667 amtool "$@"
4668 }
4669 amls() {
4670 amall silence query "$@"
4671 }
4672 # amtool silence add
4673 amsa() {
4674 amall silence add "$@"
4675 }
4676 # amtool silence force
4677 amsf() {
4678 amall silence add x!="1"
4679 }
4680 amrmall() {
4681 # note: not sure if quoting of this arg is correct
4682 amfsf silence expire "$(amfsf silence query -q)"
4683 amoffice silence expire "$(amoffice silence query -q)"
4684 }
4685
4686
4687 youtube-dl-update() {
4688 sudo wget https://yt-dl.org/downloads/latest/youtube-dl -O /usr/local/bin/youtube-dl
4689 sudo chmod a+rx /usr/local/bin/youtube-dl
4690 }
4691
4692 # https://github.com/yt-dlp/yt-dlp/wiki/Installation
4693 yt-dlp-update() {
4694 sudo curl -L https://github.com/yt-dlp/yt-dlp/releases/latest/download/yt-dlp -o /usr/local/bin/yt-dlp
4695 sudo chmod a+rx /usr/local/bin/yt-dlp # Make executable
4696 }
4697
4698 mpvyt() {
4699 mpv --ytdl ytdl_path=/usr/local/bin/yt-dlp "$@"
4700 }
4701
4702 # taken from default changes to bashrc and bash_profile
4703 path-add --end --ifexists $HOME/.rvm/bin
4704 # also had ruby bin dir, but moved that to environment.sh
4705 # so its included in overall env
4706
4707
4708 # ya, hacky hardcoded hostnames in 2023. we could do better
4709 hssh-update() {
4710 local -a failed_hosts hosts
4711 case $HOSTNAME in
4712 sy|kd)
4713 hosts=(
4714 kd.b8.nz x3.office.fsf.org syw x2.b8.nz
4715 )
4716 ;;
4717 x3)
4718 hosts=(
4719 b8.nz sywg.b8.nz
4720 )
4721 ;;
4722 esac
4723 for host in ${hosts[@]}; do
4724 e $host
4725 if ! scp /b/fai/fai/config/files/usr/local/bin/hssh/IANK root@$host:/usr/local/bin/hssh; then
4726 failed_hosts+=($host)
4727 fi
4728 done
4729 if (( ${#failed_hosts[@]} >= 1 )); then
4730 echo failed_hosts=${failed_hosts[*]}
4731 return 1
4732 fi
4733 }
4734
4735 noi3bar() {
4736 touch /tmp/noi3bar
4737 }
4738 i3bar() {
4739 rm -fv /tmp/noi3bar
4740 }
4741
4742 # example:
4743 # <#part type="image/jpeg" filename="/home/iank/2023-12-24-ski-trip.jpg" disposition=attachment> <#/part>
4744 #
4745 attach-txt() {
4746 local f
4747 for f; do
4748 if [[ ! -s $f ]]; then
4749 e "error: empty or non-existent file $f"
4750 return 1
4751 fi
4752 done
4753 for f; do
4754 echo '<#part type="image/jpeg" filename="'"$(rl "$f")"'" disposition=attachment> <#/part>'
4755 done | ec
4756 }
4757
4758 ctof() {
4759 units "tempC($1)" tempF
4760 }
4761
4762 ftoc() {
4763 units "tempF($1)" tempC
4764 }
4765
4766 # local icecast
4767 localic() {
4768 local mod=false
4769 cedit live /p/c/machine_specific/vps/filesystem/var/lib/bind/db.iankelling.org <<'EOF' || mod=true
4770 live CNAME b8.nz.
4771 EOF
4772 if $mod; then
4773 ip=$(ip r show default | sed -r 's/.*src ([^ ]*).*/\1/' | head -n1)
4774 if [[ ! $ip ]] && timeout 1 ping -c 1 $ip; then
4775 echo "error: failed to get ip: $ip" >&2
4776 exit 1
4777 fi
4778 cat >/p/c/cmc-firewall-data-http <<EOF
4779 http_ip=$ip
4780 EOF
4781 bindpush
4782 wrt-setup
4783 fi
4784 web-conf -e ian@iankelling.org -f 8000 - apache2 live.iankelling.org <<'EOF'
4785 <Location "/fsf.webm">
4786 AuthType Basic
4787 AuthName "basic_auth"
4788 # created with
4789 # htpasswd -c icecast-fsf-htpasswd USERNAME
4790 AuthUserFile "/etc/icecast-fsf-htpasswd"
4791 Require valid-user
4792 </Location>
4793 <Location "/fsf-tech.webm">
4794 AuthType Basic
4795 AuthName "basic_auth"
4796 AuthUserFile "/etc/icecast-fsf-tech-htpasswd"
4797 Require valid-user
4798 </Location>
4799 EOF
4800 s cat /etc/letsencrypt/live/live.iankelling.org/{fullchain,privkey}.pem | s dd of=/etc/icecast2/fullchainpluskey.pem
4801 ser start icecast2
4802 }
4803 # li icecast
4804 liic() {
4805 cedit live /p/c/machine_specific/vps/filesystem/var/lib/bind/db.iankelling.org <<'EOF' || bindpush
4806 live A 72.14.176.105
4807 AAAA 2600:3c00::f03c:91ff:fe6d:baf8
4808 EOF
4809 }
4810 # icecast rm -r
4811 icrmr() {
4812 find /var/icecast -type f -delete
4813 ssh li.b8.nz find /var/icecast -type f -delete
4814 }
4815
4816
4817 # obs screen switching of
4818 obof() {
4819 ls -l /tmp/no-obs-auto-scene-switch
4820 touch /tmp/no-obs-auto-scene-switch
4821 }
4822 # obs screen switching on
4823 obon() {
4824 ls -l /tmp/no-obs-auto-scene-switch
4825 if [[ -e /tmp/no-obs-auto-scene-switch ]]; then
4826 rm -f /tmp/no-obs-auto-scene-switch
4827 fi
4828 }
4829
4830 obs-gen-profiles() {
4831 local p=/p/c/basic/profiles
4832 sed 's/fsf-sysops/fsf-tech/g' $p/fsfsysops/basic.ini >$p/fsftech/basic.ini
4833 sed 's/fsf-sysops/fsf/g' $p/fsfsysops/basic.ini >$p/fsf/basic.ini
4834 }
4835
4836 # terminal clear. like clear, but put the prompt at the bottom,
4837 # useful for obs streaming the bottom half of a terminal window.
4838 tclear() {
4839 for ((i=0; i<COLUMNS; i++)); do
4840 echo
4841 done
4842 }
4843
4844 opensslcertinfo() {
4845 openssl x509 -txt -in "$@"
4846 }
4847
4848 # dsh on btrbk hosts
4849 dsb() {
4850 :
4851 }
4852
4853 # dsh a file and run it
4854 dsa() {
4855 local ret file
4856 if ! parallel -j 10 scp x {}:/tmp <~/.dsh/group/btrbk; then
4857 echo parallel scp failed. dsa returning $ret
4858 fi
4859 dsh -g btrbk
4860 }
4861
4862 # temporary
4863 zmqsend() {
4864 /nocow/t/ffmpeg-release/ffmpeg-7.0.1/tools/zmqsend "$@"
4865 }
4866
4867 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; }
4868
4869 firefox-hide-tabs() {
4870
4871 # without this, make tabs smaller by setting browser.uidensity 1 in about:config
4872
4873 profiledir=$1
4874 [[ $1 ]] || return 1
4875 # Related: the sidebery extension is useful.
4876
4877 # This is from
4878 # https://raw.githubusercontent.com/MrOtherGuy/firefox-csshacks/master/chrome/hide_tabs_toolbar.css
4879
4880 ainsl $profiledir/chrome/userChrome.css '#TabsToolbar{ visibility: collapse !important }'
4881
4882 }
4883
4884 # kill lease on cmc
4885 klease() {
4886 local tmpdir ret out
4887 ret=0
4888 out=$(ssh cmc dnsmasq-end-lease "$1" 2>&1) || ret=1
4889 printf "%s\n" "$out"
4890 if [[ $out == *"try diffing"* ]]; then
4891 tmpdir=$(mktemp -d)
4892 m scp cmc:/tmp/dhcp.leases cmc:/tmp/dhcp.leases.iank $tmpdir
4893 m diff $tmpdir/dhcp.leases $tmpdir/dhcp.leases.iank ||:
4894 rm -rf $tmpdir
4895 fi
4896 return $ret
4897 }
4898
4899 # ffs and switch the bash history on this terminal.
4900 # disabled because I don't really need this and
4901 # the history switching is annoying for debugging.
4902 #
4903 # ffs() {
4904 # local last
4905 # last="${*: -1}"
4906 # if [[ $last && $last != -* && $last != sysops ]]; then
4907 # his
4908 # fi
4909 # command ffs "$@"
4910 # }
4911
4912 i3gen() {
4913 /b/ds/i3-sway/gen
4914 }
4915
4916
4917 # insensitive find plus edit
4918 ife() {
4919 local tmps found_count i char file
4920 local -a found_files
4921 local -A button_file
4922 tmps=$(ifn "$@")
4923 mapfile -t found_files <<<"$tmps"
4924 found_count=${#found_files[@]}
4925 if (( ${#found_files[@]} == 1 )); then
4926 m g ${found_files[0]}
4927 else
4928 i=0
4929 for button in {a..z}; do
4930 button_file[$button]="${found_files[$i]}"
4931 echo $button: ${found_files[$i]}
4932 i=$(( i + 1 ))
4933 if (( i >= found_count )); then
4934 break
4935 fi
4936 done
4937 read -rsN1 -t 5 char ||:
4938 file="${button_file[$char]}"
4939
4940 if [[ $file ]]; then
4941 g "$file"
4942 else
4943 echo "no selection"
4944 fi
4945 fi
4946 }
4947
4948
4949 export BASEFILE_DIR=/a/bin/fai-basefiles
4950
4951 #export ANDROID_HOME=/a/opt/android-home
4952 # https://f-droid.org/en/docs/Installing_the_Server_and_Repo_Tools/
4953 #export USE_SDK_WRAPPER=yes
4954 #PATH=$PATH:$ANDROID_HOME/tools:$ANDROID_HOME/platform-tools
4955
4956 # didnt get drush working, if I did, this seems like the
4957 # only good thing to include for it.
4958 # Include Drush completion.
4959 # if [ -f "/home/ian/.drush/drush.complete.sh" ] ; then
4960 # source /home/ian/.drush/drush.complete.sh
4961 # fi
4962
4963
4964 # best practice
4965 unset IFS
4966
4967 # https://wiki.archlinux.org/index.php/Xinitrc#Autostart_X_at_login
4968 # i added an extra condition as gentoo xorg guide says depending on
4969 # $DISPLAY is fragile.
4970 if [[ ! $DISPLAY && $XDG_VTNR == 1 ]] && shopt -q login_shell && isarch; then
4971 exec startx
4972 fi
4973
4974
4975 # ensure no bad programs appending to this file will have an affect
4976 return 0