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