add a few tiny scripts
[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 apache-header() {
2000 # First paragraph is to avoid people being confused about why a
2001 # file is apache licensed.
2002 cat <<'EOF'
2003 # I, Ian Kelling, follow the GNU license recommendations at
2004 # https://www.gnu.org/licenses/license-recommendations.en.html. They
2005 # recommend that small programs, < 300 lines, be licensed under the
2006 # Apache License 2.0. This file contains or is part of one or more small
2007 # programs. If a small program grows beyond 300 lines, I plan to switch
2008 # its license to GPL.
2009
2010 # Copyright 2024 Ian Kelling
2011
2012 # Licensed under the Apache License, Version 2.0 (the "License");
2013 # you may not use this file except in compliance with the License.
2014 # You may obtain a copy of the License at
2015
2016 # http://www.apache.org/licenses/LICENSE-2.0
2017
2018 # Unless required by applicable law or agreed to in writing, software
2019 # distributed under the License is distributed on an "AS IS" BASIS,
2020 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
2021 # See the License for the specific language governing permissions and
2022 # limitations under the License.
2023
2024 EOF
2025
2026 }
2027
2028 # apply apache to git tracked bash files + README, except files with A?GPL3 header.
2029 apache-apply-repo() {
2030 for f in $(git ls-files); do
2031 [[ -L $f || ! -f $f ]] && continue
2032 if [[ $f != README ]]; then
2033 if ! grep -n '^#!/bin/bash' $f | grep ^1: &>/dev/null; then continue; fi
2034 if head -n 10 $f | grep 'it under the terms of the GNU General Public License as published by' &>/dev/null; then continue; fi
2035 fi
2036 apache-apply $f
2037 done
2038 }
2039
2040 apache-apply() {
2041 for file; do
2042 if head -n1 "$file"| grep -E '^#!/bin/bash\b' &>/dev/null; then
2043 {
2044 head -n1 "$file"
2045 apache-header
2046 tail -n+2 "$file"
2047 } | sponge "$file"
2048 else
2049 {
2050 apache-header
2051 cat "$file"
2052 } | sponge "$file"
2053 fi
2054 done
2055 }
2056 # strip out the apache license from a file.
2057 apache-strip() {
2058 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
2059 }
2060
2061 chrome() {
2062 if type -p chromium &>/dev/null; then
2063 cmd=chromium
2064 else
2065 cd /
2066 cmd="schroot -c bullseye chromium"
2067 CHROMIUM_FLAGS='--enable-remote-extensions' $cmd & r
2068 fi
2069 }
2070
2071
2072 # do all tee.
2073 # pipe to this, or just type like a shell
2074 # todo: test this
2075 dat() {
2076 tee >(ssh frodo.b8.nz) >(ssh x2) >(ssh tp.b8.nz) >(ssh kw) >(ssh tp.b8.nz)
2077 }
2078 da() { # do all
2079 local host
2080 for host in x2 kw tp.b8.nz x3.b8.nz frodo.b8.nz; do
2081 ssh $host "$@"
2082 done
2083 }
2084
2085
2086 debian_pick_mirror () {
2087 # netselect-apt finds a fast mirror.
2088 # but we need to replace the mirrors ourselves,
2089 # because it doesnt do that. best it can do is
2090 # output a basic sources file
2091 # here we get the server it found, get the main server we use
2092 # then substitute all instances of one for the other in the sources file
2093 # and backup original to /etc/apt/sources.list-original.
2094 # this is idempotent. the only way to identify debian sources is to
2095 # note the original server, so we put it in a comment so we can
2096 # identify it later.
2097 local file
2098 file=$(mktemp -d)/f # safe way to get file name without creating one
2099 sudo netselect-apt -o "$file" || return 1
2100 url=$(grep ^\\w $file | head -n1 | awk '{print $2}')
2101 sudo cp -f /etc/apt/sources.list /etc/apt/sources.list-original
2102 sudo sed -ri "/http.us.debian.org/ s@( *[^ #]+ +)[^ ]+([^#]+).*@\1$url\2# http.us.debian.org@" /etc/apt/sources.list
2103 sudo apt-get update
2104 }
2105 digme() {
2106 digdiff @ns{1,2}.iankelling.org "$@"
2107 }
2108
2109 dup() {
2110 local ran_d
2111 ran_d=false
2112 system-status _
2113 case $PS1 in
2114 *[\ \]]D\ *)
2115 pushd /
2116 /b/ds/distro-begin |& ts || return $?
2117 /b/ds/distro-end |& ts || return $?
2118 popd
2119 ran_d=true
2120 ;;&
2121 *[\ \]]DB\ *)
2122 pushd /
2123 /b/ds/distro-begin |& ts || return $?
2124 popd
2125 ran_d=true
2126 ;;
2127 *[\ \]]DE\ *)
2128 pushd /
2129 /b/ds/distro-end |& ts || return $?
2130 popd
2131 ran_d=true
2132 ;;&
2133 *CONFLINK*)
2134 if ! $ran_d; then
2135 conflink
2136 fi
2137 ;;
2138 esac
2139 system-status _
2140 }
2141
2142 envload() { # load environment from a previous: export > file
2143 local file=${1:-$HOME/.${USER}_env}
2144 eval "$(export | sed 's/^declare -x/export -n/')"
2145 while IFS= read -r line; do
2146 # declare -x makes variables local to a function
2147 eval ${line/#declare -x/export}
2148 done < "$file"
2149 }
2150
2151 failfunc() { asdf a b c; }
2152 failfunc2() { failfunc d e f; }
2153
2154 # one that comes with distros is too old for newer devices
2155 fastboot() {
2156 /a/opt/android-platform-tools/fastboot "$@";
2157 }
2158
2159 kdecd() { /usr/lib/x86_64-linux-gnu/libexec/kdeconnectd; }
2160
2161 bat() {
2162 cat /sys/class/power_supply/BAT0/capacity
2163 }
2164
2165 # List of apps to install/update
2166 # Create from existing manually installed apps by doing
2167 # fdroidcl update
2168 # fdroidcl search -i, then manually removing
2169 # automatically installed/preinstalled apps
2170
2171 #
2172 # # my attempt at recovering from boot loop:
2173 # # in that case, boot to recovery (volume up, home button, power, let go of power after samsun logo)
2174 # # then
2175 # mount /dev/block/mmcblk0p12 /data
2176 # cd /data
2177 # find -iname '*appname*'
2178 # rm -rf FOUND_DIRS
2179 # usually good enough to just rm -rf /data/app/APPNAME
2180 #
2181 # currently broken:
2182 # # causes replicant to crash
2183 # org.quantumbadger.redreader
2184 # org.kde.kdeconnect_tp
2185
2186 # not broke, but wont work without gps
2187 #com.zoffcc.applications.zanavi
2188 # not broke, but not using atm
2189 #com.nutomic.syncthingandroid
2190 # # doesn\'t work on replicant
2191 #net.sourceforge.opencamera
2192 #
2193 fdroid_pkgs=(
2194 net.mullvad.mullvadvpn
2195 org.schabi.newpipe
2196 io.github.subhamtyagi.lastlauncher
2197 io.anuke.mindustry
2198 com.biglybt.android.client
2199 de.marmaro.krt.ffupdater
2200 me.ccrama.redditslide
2201 org.fedorahosted.freeotp
2202 at.bitfire.davdroid
2203 com.alaskalinuxuser.justnotes
2204 com.artifex.mupdf.viewer.app
2205 com.danielkim.soundrecorder
2206 com.fsck.k9
2207 com.ichi2.anki
2208 com.jmstudios.redmoon
2209 com.jmstudios.chibe
2210 org.kde.kdeconnect_tp
2211 com.notecryptpro
2212 com.termux
2213 cz.martykan.forecastie
2214 de.danoeh.antennapod
2215 de.blinkt.openvpn
2216 de.marmaro.krt.ffupdater
2217 eu.siacs.conversations
2218 free.rm.skytube.oss
2219 im.vector.alpha # riot
2220 info.papdt.blackblub
2221 me.tripsit.tripmobile
2222 net.gaast.giggity
2223 net.minetest.minetest
2224 net.osmand.plus
2225 org.isoron.uhabits
2226 org.linphone
2227 org.gnu.icecat
2228 org.smssecure.smssecure
2229 org.yaaic
2230 sh.ftp.rocketninelabs.meditationassistant.opensource
2231 )
2232 # https://forum.xda-developers.com/android/software-hacking/wip-selinux-capable-superuser-t3216394
2233 # for maru,
2234 #me.phh.superuser
2235
2236 fdup() {
2237 local -A installed updated
2238 local p
2239 # tried putting this in go buildscript cronjob,
2240 # but it failed with undefined: os.UserCacheDir. I expect its due to
2241 # an environment variable missing, but its easier just to stick it here.
2242 m go get -u mvdan.cc/fdroidcl || return 1
2243 m fdroidcl update
2244 if fdroidcl search -u | grep ^org.fdroid.fdroid; then
2245 fdroidcl install org.fdroid.fdroid
2246 sleep 5
2247 m fdroidcl update
2248 fi
2249 for p in $(fdroidcl search -i| grep -o "^\S\+"); do
2250 installed[$p]=true
2251 done
2252 for p in $(fdroidcl search -u| grep -o "^\S\+"); do
2253 updated[$p]=false
2254 done
2255 for p in ${fdroid_pkgs[@]}; do
2256 if ! ${installed[$p]:-false}; then
2257 m fdroidcl install $p
2258 # sleeps are just me being paranoid since replicant has a history of crashing when certain apps are installed
2259 sleep 5
2260 fi
2261 done
2262 for p in ${!installed[@]}; do
2263 if ! ${updated[$p]:-true}; then
2264 m fdroidcl install $p
2265 sleep 5
2266 fi
2267 done
2268 }
2269
2270 firefox-default-profile() {
2271 local key value section
2272 key=Default
2273 value=1
2274 section=$1
2275 file=/p/c/subdir_files/.mozilla/firefox/profiles.ini
2276 sed -ri "/^ *$key/d" "$file"
2277 sed -ri "/ *\[$section\]/,/^ *\[[^]]+\]/{/^\s*${key}[[:space:]=]/d};/ *\[$section\]/a $key=$value" "$file"
2278 }
2279 fdhome() { #firefox default home profile
2280 firefox-default-profile Profile0
2281 }
2282
2283 fdwork() {
2284 firefox-default-profile Profile4
2285 }
2286
2287 ff() {
2288 if type -P firefox &>/dev/null; then
2289 firefox "$@"
2290 else
2291 iceweasel "$@"
2292 fi
2293 }
2294
2295 fn() {
2296 firefox -P alt "$@" >/dev/null 2>&1
2297 }
2298
2299
2300 fsdiff () {
2301 local missing=false
2302 local dname="${PWD##*/}"
2303 local m="/a/tmp/$dname-missing"
2304 local d="/a/tmp/$dname-diff"
2305 [[ -e $d ]] && rm "$d"
2306 [[ -e $m ]] && rm "$m"
2307 local msize=0
2308 local fsfile
2309 while read -r line; do
2310 fsfile="$1${line#.}"
2311 if [[ -e "$fsfile" ]]; then
2312 md5diff "$line" "$fsfile" && tee -a "/a/tmp/$dname-diff" <<< "$fsfile $line"
2313 else
2314 missing=true
2315 echo "$line" >> "$m"
2316 msize=$((msize + 1))
2317 fi
2318 done < <(find . -type f )
2319 if $missing; then
2320 echo "$m"
2321 (( msize <= 100 )) && cat $m
2322 fi
2323 }
2324 fsdiff-test() {
2325 local tmpd x
2326 # expected output, with different tmp dirs
2327 # /tmp/tmp.HDPbwMqdC9/c/d ./c/d
2328 # /a/tmp/tmp.qLDkYxBYPM-missing
2329 # ./b
2330 tmpd="$(mktemp -d)"
2331 cd "$tmpd"
2332 echo ok > a
2333 echo nok > b
2334 mkdir c
2335 echo ok > c/d
2336 local x
2337 x=$(mktemp -d)
2338 mkdir $x/c
2339 echo different > $x/c/d
2340 echo ok > $x/a
2341 fsdiff $x
2342 rm -r "$x" "$tmpd"
2343 }
2344 rename-test() {
2345 # test whether missing files were renamed, generally for use with fsdiff
2346 # $1 = fsdiff output file, $2 = directory to compare to. pwd = fsdiff dir
2347 # echos non-renamed files
2348 local x y found
2349 unset sums
2350 for x in "$2"/*; do
2351 { sums+=( "$(md5sum < "$x")" ) ; } 2>/dev/null
2352 done
2353 while read -r line; do
2354 { missing_sum=$(md5sum < "$line") ; } 2>/dev/null
2355 renamed=false
2356 for x in "${sums[@]}"; do
2357 if [[ $missing_sum == "$x" ]]; then
2358 renamed=true
2359 break
2360 fi
2361 done
2362 $renamed || echo "$line"
2363 done < "$1"
2364 return 0
2365 }
2366
2367 feh() {
2368 # F = fullscren, z = random, Z = auto zoom
2369 command feh --auto-rotate -FzZ "$@"
2370 }
2371
2372
2373
2374 fw() {
2375 firefox -P default "$@" >/dev/null 2>&1
2376 }
2377
2378 gitian() {
2379 git config user.email ian@iankelling.org
2380 }
2381
2382 # at least in flidas, things rely on gpg being gpg1
2383 gpg() {
2384 if type -P gpg2 &>/dev/null; then
2385 command gpg2 "$@"
2386 else
2387 command gpg "$@"
2388 fi
2389 }
2390
2391 gse() {
2392 local email=iank@fsf.org
2393 git send-email --notes "--envelope-sender=<$email>" \
2394 --suppress-cc=self "$@"
2395 }
2396
2397 gup() { /a/f/gnulib/build-aux/gnupload "$@"; }
2398
2399 dejagnu() { /a/opt/dejagnu/dejagnu "$@"; }
2400
2401 hstatus() {
2402 # do git status on published repos.
2403 c /a/bin/githtml
2404 for x in *; do
2405 cd "$(readlink -f $x)"/..
2406 status=$(i status -s) || pwd
2407 if [[ $status ]]; then
2408 hr
2409 echo $x
2410 printf "%s\n" "$status"
2411 fi
2412 cd /a/bin/githtml
2413 done
2414 }
2415
2416 ## work log
2417 #
2418 # note: database location is specified in ~/.timetrap.yml, currently /p/.timetrap.db
2419 wlog() {
2420 local day i days_back
2421 days_back=${1:-16}
2422 for (( i=days_back; i>=0; i-- )); do
2423 day=$( date +%F -d @$((EPOCHSECONDS - 86400*i )) )
2424 date "+%a %b %d" -d @$((EPOCHSECONDS - 86400*i )) | tr '\n' ' '
2425 /a/opt/timetrap/bin/t d -ftotal -s $day -e $day all -m '^w|lunch$'
2426 done
2427 }
2428 to() { t out -a "$@"; }
2429 ti() { t in -a "$@"; }
2430 tl() {
2431 local in_secs
2432 to "$*"
2433 t s lunch
2434 t in -a "$*"
2435 in_secs="$(date -d "${*//[_.]/ }" +%s)"
2436 m t out -a "$(date +%F.%T -d @$(( in_secs + 60*45 )) )"
2437 t s w
2438 }
2439
2440
2441 # help me focus. opens 2 windows.
2442 focus() {
2443 /p/c/proc/focus/linux-amd64/focus &
2444 watcharb5
2445 kill %%
2446 }
2447
2448
2449 # Display a list of the active window title
2450 # i've been on with 10 second samples going back
2451 # 5 minutes. If I've been on one window for 10 seconds
2452 # or longer, then display the second count.
2453 #
2454 # Press any key to exit.
2455 watcharb5() {
2456 local char ret
2457 killall arbtt-capture &>/dev/null ||:
2458 rm -f ~/.arbtt/capture.log
2459 arbtt-capture --sample-rate=10 &
2460 while true; do
2461 arb5
2462 ret=0
2463 # i first thought to sleep and capture ctrl-c, but it seems we can't
2464 # capture control-c, unless maybe we implement the commands in a
2465 # separate script or maybe add err-cleanup to err. Anyways, this
2466 # method is superior because any single char exits.
2467 read -rsN1 -t 5 char || ret=$?
2468 if (( ret == 142 )) || [[ ! $char ]]; then
2469 # debug
2470 #e ret=$ret char=$char
2471 :
2472 else
2473 killall arbtt-capture ||:
2474 return 0
2475 fi
2476 done
2477
2478 }
2479
2480 arb5() {
2481 local i j l sec blanks line
2482 local -a arbtt_lines
2483 if [[ ! -e ~/.arbtt/capture.log ]]; then
2484 sleep 5
2485 fi
2486 blanks=$(( LINES - 34 ))
2487 for (( i=0; i < blanks; i++ )); do
2488 echo
2489 done
2490
2491 {
2492 i=0
2493 j=0
2494 # https://stackoverflow.com/questions/56486272/how-to-concat-multiple-fields-to-same-line-with-jq
2495 arbtt_lines=$(arbtt-dump -l 30 -t json | \
2496 jq -r '.[] | [ ( .inactive / 1000 | floor ) , ( .windows[] | select (.active == true) |.title) ] | @tsv' | tac)
2497 for line in "${arbtt_lines[@]}"; do
2498 read -r sec l <<<"$line"
2499 if (( j >= LINES )); then
2500 break
2501 fi
2502 if (( i % 6 == 0 && i >= 2 )); then
2503 j=$(( j + 1 ))
2504 echo "## $(( i / 6 + 1 )) ##"
2505 fi
2506 if (( sec > 10 )); then
2507 printf "%3d %s\n" $sec "$l" | sed -r "s/^(.{$COLUMNS}).*/\1/"
2508 else
2509 printf " %s\n" "$l" | sed -r "s/^(.{$COLUMNS}).*/\1/"
2510 fi
2511 i=$(( i + 1 ))
2512 j=$(( j + 1 ))
2513 done
2514 while (( j < 34 && j < LINES )); do
2515 echo
2516 j=$(( j + 1 ))
2517 done
2518 } | tac
2519 }
2520
2521 arbttlog() {
2522 # from the log, show only the currently active window, and the number of
2523 # seconds of input inactivity.
2524 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}' ; }
2525
2526 idea() {
2527 /a/opt/idea-IC-163.7743.44/bin/idea.sh "$@" & r
2528 }
2529
2530 ilogs-local() {
2531 d=/var/lib/znc/moddata/log/iank/
2532 for n in freenode libera; do
2533 cd $d/$n
2534 hr
2535 for x in "#$1/"*; do
2536 base=${x##*/}
2537 files=()
2538 for f in $@; do
2539 tmp=\#$f/$base
2540 if [[ -e $tmp ]]; then
2541 files+=(\#$f/$base)
2542 fi
2543 done
2544 sed \"s/^./${base%log}/\" ${files[@]}|sort -n
2545 hr
2546 done
2547 done
2548 }
2549 ilogs() {
2550 sl root@iankelling.org ilogs-local "$@"
2551 }
2552
2553
2554 ilog-local() {
2555 local d chan
2556 chan="$1"
2557 d=/var/lib/znc/moddata/log/iank/
2558 for n in freenode libera; do
2559 if [[ ! -d $d$n/"$chan" ]]; then
2560 continue
2561 fi
2562 cd $d$n/"$chan"
2563 hr
2564 for x in *; do
2565 echo $x; sed "s/^./${x%log}/" $x; hr;
2566 done
2567 done
2568 }
2569 ilog() {
2570 local chan
2571 chan="${1:-#fsfsys}"
2572 # use * instead of -r since that does sorted order
2573 sl root@iankelling.org ilog-local "$chan" | less +G
2574 }
2575
2576 o() {
2577 if type gio &> /dev/null ; then
2578 gio open "$@"
2579 elif type gvfs-open &> /dev/null ; then
2580 gvfs-open "$@"
2581 else
2582 xdg-open "$@"
2583 fi
2584 # another alternative is run-mailcap
2585 }
2586 ccomp xdg-open o
2587
2588 # jfilter() {
2589 # grep -Evi -e "^(\S+\s+){4}(sudo|sshd|cron)\[\S*:" \
2590 # -e "^(\S+\s+){4}systemd\[\S*: (starting|started) (btrfsmaintstop|dynamicipupdate|spamd dns bug fix cronjob|rss2email)\.*$"
2591 # }
2592 # jtail() {
2593 # journalctl -n 10000 -f "$@" | jfilter
2594 # }
2595 # jr() { journalctl "$@" | jfilter | less ; }
2596 # jrf() { journalctl -n 200 -f "$@" | jfilter; }
2597
2598
2599 ## old version for model01. i need to get that firmware working again.
2600 # kff() { # keyboardio firmware flash. you must hold down the tilde key
2601 # pushd /a/opt/Model01-Firmware
2602 # # if we didn't want this yes hack, then remove "shell read" from
2603 # # /a/opt/Kaleidoscope/etc/makefiles/sketch.mk
2604 # yes $'\n' | VERBOSE=1 make flash
2605 # popd
2606 # }
2607
2608
2609 kff() {
2610 pushd /a/opt/Kaleidoscope/examples/Devices/Keyboardio/Model100
2611 make flash
2612 popd
2613 }
2614
2615 wgkey() {
2616 local umask_orig name
2617 if (( $# != 1 )); then
2618 e expected 1 arg >&2
2619 return 1
2620 fi
2621 name=$1
2622 umask_orig=$(umask)
2623 umask 0077
2624 wg genkey | tee $name-priv.key | wg pubkey > $name-pub.key
2625 umask $umask_orig
2626 }
2627
2628 declare -A vpn_ips
2629 vpn_ips[kd]=2
2630 # note: 1, 4, 5 are occupied by mail wireguard
2631 vpn_ips[x3]=8
2632 vpn_ips[sy]=12
2633 vpn_ips[x2]=13
2634 vpn_ips[kw]=27
2635 vpn_ips[bo]=28
2636 vpn_ips[frodo]=34
2637 vpn_ips[s23b]=49
2638
2639 vpn-ips-update() {
2640 local host ipsuf f files
2641 for host in ${!vpn_ips[@]}; do
2642 ipsuf=${vpn_ips[$host]}
2643 wghole $host $ipsuf
2644 u /a/bin/ds/machine_specific/$host/filesystem/etc/systemd/system/openvpn-client-tr@.service <<EOF
2645 [Unit]
2646 Description=OpenVPN tunnel for %I
2647 After=syslog.target network-online.target
2648 Wants=network-online.target
2649 Documentation=man:openvpn(8)
2650 Documentation=https://community.openvpn.net/openvpn/wiki/Openvpn24ManPage
2651 Documentation=https://community.openvpn.net/openvpn/wiki/HOWTO
2652 Requires=iptables.service
2653
2654 [Service]
2655 Type=notify
2656 RuntimeDirectory=openvpn-client
2657 RuntimeDirectoryMode=0710
2658 WorkingDirectory=/etc/openvpn/client
2659 ExecStart=/usr/sbin/openvpn --suppress-timestamps --nobind --config /etc/openvpn/client/%i.conf
2660 # todo, try reenabling this from the default openvpn,
2661 # it was disabled so we could do bind mounts as a command,
2662 # but now systemd handles it
2663 #CapabilityBoundingSet=CAP_IPC_LOCK CAP_NET_ADMIN CAP_NET_RAW CAP_SETGID CAP_SETUID CAP_SYS_CHROOT CAP_DAC_OVERRIDE
2664 LimitNPROC=10
2665 # DeviceAllow=/dev/null rw
2666 # DeviceAllow=/dev/net/tun rw
2667
2668 # we use .1 to make this be on a different network than kd, so that we can
2669 # talk to transmission on kd from remote host, and still use this
2670 # vpn.
2671 ExecStartPre=/usr/bin/flock -w 20 /tmp/newns.flock /a/bin/newns/newns -n 10.174.$ipsuf start %i
2672 ExecStartPre=/sbin/iptables-restore /a/bin/distro-setup/transmission-firewall/netns.rules
2673 # allow wireguard network to connect
2674 ExecStartPre=/usr/sbin/ip r add 10.8.0.0/24 via 10.174.$ipsuf.1 dev veth1-client
2675 ExecStopPost=/usr/bin/flock -w 20 /tmp/newns.flock /a/bin/newns/newns stop %i
2676 PrivateNetwork=true
2677 BindReadOnlyPaths=/etc/tr-resolv:/run/systemd/resolve:norbind /etc/basic-nsswitch:/etc/resolved-nsswitch:norbind
2678
2679 [Install]
2680 WantedBy=multi-user.target
2681 EOF
2682 done
2683
2684 {
2685 for host in ${!vpn_ips[@]}; do
2686 ipsuf=${vpn_ips[$host]}
2687 cat <<EOF
2688 local-data-ptr: "10.2.0.$ipsuf $host.b8.nz"
2689 EOF
2690 done
2691 } | u /b/ds/ptr-data
2692
2693 {
2694 for host in ${!vpn_ips[@]}; do
2695 ipsuf=${vpn_ips[$host]}
2696 cat <<EOF
2697 $host A 10.2.0.$ipsuf
2698 ${host}wg A 10.8.0.$ipsuf
2699 ${host}vp A 10.5.5.$ipsuf
2700 ${host}tr A 10.174.$ipsuf.2
2701 EOF
2702 done
2703 } | cedit vpn-ips-update /p/c/machine_specific/vps/bind-initial/db.b8.nz ||:
2704
2705
2706 echo checking for stray files:
2707
2708 initial_dir=$PWD
2709 cd /a/bin/ds/machine_specific
2710 ngset
2711 files=( */filesystem/etc/systemd/system/openvpn-client-tr@.service )
2712 ngreset
2713 cd $initial_dir
2714 for f in "${files[@]}"; do
2715 host=${f%%/*}
2716 if [[ ! ${vpn_ips[$host]} ]]; then
2717 e /a/bin/ds/machine_specific/$host/filesystem/etc/systemd/system/openvpn-client-tr@.service
2718 fi
2719 done
2720
2721 cd /p/c/machine_specific
2722 ngset
2723 files=( */filesystem/etc/wireguard/wghole.conf )
2724 ngreset
2725 cd $initial_dir
2726 for f in "${files[@]}"; do
2727 host=${f%%/*}
2728 if [[ ! ${vpn_ips[$host]} ]]; then
2729 e /p/c/machine_specific/$host/filesystem/etc/wireguard/wghole.conf
2730 e cedit -s $host /p/c/machine_specific/li/filesystem/etc/wireguard/wgmail.conf '<<<""'
2731 fi
2732 done
2733 }
2734
2735 # usage host ipsuf [extrahost]
2736 #
2737 # If the keys already exist and you want new ones, remove them:
2738 # rm /p/c/machine_specific/$host/filesystem/etc/wireguard/hole-{priv,pub}.key
2739 #
2740 # extrahost is a host/cidr that is allowed to go be routed through the
2741 # vpn by this host.
2742 wghole() {
2743 if (( $# < 2 || $# > 3 )); then
2744 e expected 2-3 arg of hostname, ip suffix, and extrahost >&2
2745 return 1
2746 fi
2747 local host ipsuf umask_orig vpn_allowed
2748 host=$1
2749 ipsuf=$2
2750 if [[ $3 ]]; then
2751 extrahost=,$3
2752 fi
2753 for vpn_host in ${!vpn_ips[@]}; do
2754 if [[ $vpn_host == "$host" ]]; then
2755 continue
2756 fi
2757 vpn_allowed+=",10.174.${vpn_ips[$vpn_host]}.2/32"
2758 done
2759 mkdir -p /p/c/machine_specific/$host/filesystem/etc/wireguard
2760 (
2761 cd /p/c/machine_specific/$host/filesystem/etc/wireguard
2762 umask_orig=$(umask)
2763 umask 0077
2764 if [[ ! -s hole-priv.key || ! -s hole-pub.key ]]; then
2765 wg genkey | tee hole-priv.key | wg pubkey > hole-pub.key
2766 fi
2767 cat >wghole.conf <<EOF
2768 [Interface]
2769 # contents hole-priv.key
2770 PrivateKey = $(cat hole-priv.key)
2771 ListenPort = 1194
2772 Address = 10.8.0.$ipsuf/24
2773 # https://dev.to/tangramvision/what-they-don-t-tell-you-about-setting-up-a-wireguard-vpn-1h2g
2774 # ||: makes the systemd service not fail due to the failed command
2775 PostUp = ping -w10 -c1 10.8.0.1 ||:
2776
2777 [Peer]
2778 # li. called wgmail on that server
2779 PublicKey = CTFsje45qLAU44AbX71Vo+xFJ6rt7Cu6+vdMGyWjBjU=
2780 AllowedIPs = 10.8.0.0/24$vpn_allowed$extrahost
2781 Endpoint = 72.14.176.105:1194
2782 PersistentKeepalive = 25
2783 EOF
2784 umask $umask_orig
2785 # old approach. systemd seems to work fine and cleaner.
2786 rm -f ../network/interfaces.d/wghole
2787 cedit -q $host /p/c/machine_specific/li/filesystem/etc/wireguard/wgmail.conf <<EOF || [[ $? == 1 ]]
2788 [Peer]
2789 PublicKey = $(cat hole-pub.key)
2790 AllowedIPs = 10.8.0.$ipsuf/32,10.174.${vpn_ips[$host]}.2/32
2791 EOF
2792 )
2793 }
2794
2795
2796 mns() { # mount namespace
2797 ns=$1
2798 shift
2799 s mkdir -p /root/mount_namespaces
2800 if ! sudo mountpoint /root/mount_namespaces >/dev/null; then
2801 m sudo mount --bind /root/mount_namespaces /root/mount_namespaces
2802 fi
2803 m sudo mount --make-private /root/mount_namespaces
2804 if [[ ! -e /root/mount_namespaces/$ns ]]; then
2805 m sudo touch /root/mount_namespaces/$ns
2806 fi
2807 if ! sudo mountpoint /root/mount_namespaces/$ns >/dev/null; then
2808 m sudo unshare --propagation slave --mount=/root/mount_namespaces/$ns /bin/true
2809 fi
2810 m sudo -E /usr/bin/nsenter --mount=/root/mount_namespaces/$ns "$@"
2811 }
2812
2813 mnsd() { # mount namespace + systemd namespace
2814 ns=$1
2815 unit=$2
2816 shift 2
2817
2818 s mkdir -p /root/mount_namespaces
2819 if ! sudo mountpoint /root/mount_namespaces >/dev/null; then
2820 m sudo mount --bind /root/mount_namespaces /root/mount_namespaces
2821 fi
2822 m sudo mount --make-private /root/mount_namespaces
2823 if [[ ! -e /root/mount_namespaces/$ns ]]; then
2824 m sudo touch /root/mount_namespaces/$ns
2825 fi
2826 if ! sudo mountpoint /root/mount_namespaces/$ns >/dev/null; then
2827 m sudo unshare --propagation slave --mount=/root/mount_namespaces/$ns /bin/true
2828 fi
2829
2830 pid=$(servicepid $unit)
2831 tmpf=$(mktemp --tmpdir $unit.XXXXXXXXXX)
2832 export -p >$tmpf
2833 printf "%s " "${@@Q}" >>$tmpf
2834 echo >>$tmpf
2835
2836 m sudo nsenter -t $pid -n --mount=/root/mount_namespaces/$ns sudo -u $USER -i bash -c ". $tmpf & sleep 1; rm $tmpf"
2837 }
2838
2839
2840 mnsr() { # mns run
2841 local ns=$1
2842 shift
2843 mns $ns sudo -u iank -E env "PATH=$PATH" "$@"
2844 }
2845
2846 mnsnonetr() {
2847 ns=$1
2848 lomh
2849 if ! s ip netns list | grep -Fx nonet &>/dev/null; then
2850 s ip netns add nonet
2851 fi
2852 mns $ns --net=/var/run/netns/nonet /bin/bash
2853 lomh
2854 }
2855
2856 mnsnonet() {
2857 ns=$1
2858 lomh
2859 if ! s ip netns list | grep -Fx nonet &>/dev/null; then
2860 s ip netns add nonet
2861 fi
2862 mns $ns --net=/var/run/netns/nonet sudo -E -u iank /bin/bash
2863 lomh
2864 }
2865
2866
2867 lom() {
2868 # l = the loopback device
2869 local l base
2870 # get sudo pass cached right away
2871 if ! sudo -nv 2>/dev/null; then
2872 sudo -v
2873 fi
2874 if [[ $1 == /* ]]; then
2875 base=${1##*/}
2876 fs_file=$1
2877 if mns $base mountpoint -q /mnt/$base; then
2878 return 0
2879 fi
2880 l=$(losetup -j $fs_file | sed -rn 's/^([^ ]+): .*/\1/p' | head -n1 ||:)
2881 if [[ ! $l ]]; then
2882 l=$(sudo losetup -f)
2883 m sudo losetup $l $fs_file
2884 fi
2885 if ! sudo cryptsetup status /dev/mapper/$base &>/dev/null; then
2886 if ! m sudo cryptsetup luksOpen $l $base; then
2887 m sudo losetup -d $l
2888 return 1
2889 fi
2890 fi
2891 m sudo mkdir -p /mnt/$base
2892 m mns $base mount /dev/mapper/$base /mnt/$base
2893 m mns $base chown $USER:$USER /mnt/$base
2894 lomh
2895 else
2896 base=$1
2897 if mns $base mountpoint /mnt/$base &>/dev/null; then
2898 m mns $base umount /mnt/$base
2899 fi
2900 if sudo cryptsetup status /dev/mapper/$base &>/dev/null; then
2901 if ! m sudo cryptsetup luksClose /dev/mapper/$base; then
2902 echo lom: failed cryptsetup luksClose /dev/mapper/$base
2903 return 1
2904 fi
2905 fi
2906 l=$(losetup -l --noheadings | awk '$6 ~ /\/'$base'$/ {print $1}')
2907 if [[ $l ]]; then
2908 m sudo losetup -d $l
2909 else
2910 echo lom: warning: no loopback device found
2911 fi
2912 fi
2913 }
2914
2915 # mu personality. for original, just run mp. for 2, run mp 2.
2916 # this is partly duplicated in mail-setup
2917 mp() {
2918 local dead=false
2919 for s in {1..5}; do
2920 if ! killall mu; then
2921 dead=true
2922 break
2923 fi
2924 sleep 1
2925 done
2926 if ! $dead; then
2927 echo error: mu not dead
2928 m psg mu
2929 return 1
2930 fi
2931 suf=$1
2932 set -- /m/mucache ~/.cache/mu /m/.mu ~/.config/mu
2933 while (($#)); do
2934 target=$1$suf
2935 f=$2
2936 shift 2
2937 if [[ -e $f && ! -L $f ]]; then
2938 m rm -rf $f
2939 fi
2940 m ln -sf -T $target $f
2941 done
2942 }
2943
2944 # maildir enable
2945 mdenable() {
2946 local md dst ln_path src two
2947
2948 two=false
2949 case $1 in
2950 -2) two=true shift ;;
2951 esac
2952
2953 for md; do
2954 src=
2955 if $two; then
2956 dst=/m/4e2/$md
2957 else
2958 dst=/m/4e/$md
2959 fi
2960
2961 ln_path=/m/md/$md
2962 for d in /m/md/$md /m/4e2/$md; do
2963 if [[ -d $d && ! -L $d ]]; then
2964 src=$d
2965 break
2966 fi
2967 done
2968 if [[ ! $src ]]; then
2969 echo "error: could not find $md" >&2
2970 return 1
2971 fi
2972 m mv -T $src $dst
2973 m ln -sf -T $dst $ln_path
2974 done
2975 }
2976 md2enable() {
2977 mdenable -2 "$@"
2978 }
2979 mddisable() {
2980 local md=$1
2981 dst=/m/md/$md
2982
2983 ### begin copied from mdenable, but different d ###
2984 for d in /m/4e/$md /m/4e2/$md; do
2985 if [[ -d $d && ! -L $d ]]; then
2986 src=$d
2987 break
2988 fi
2989 done
2990 if [[ ! $src ]]; then
2991 echo "error: could not find $md" >&2
2992 return 1
2993 fi
2994 ### end copy from mdenable ###
2995
2996 if [[ -L $dst ]]; then m rm $dst; fi
2997 m mv -T $src $dst
2998 }
2999
3000
3001 mdt() {
3002 markdown "$1" >/tmp/mdtest.html
3003 firefox /tmp/mdtest.html
3004 }
3005
3006 mo() { xset dpms force off; } # monitor off
3007
3008 mpvgpu() {
3009 # seems to be the best gpu decoding on my nvidia 670.
3010 # vlc gets similar or better framerate, but is much darker output on my test movie at least.
3011
3012
3013 case $HOSTNAME in
3014 kd)
3015 echo 0f | sudo tee -a /sys/kernel/debug/dri/0/pstate
3016 ;;
3017 esac
3018 # going back to the default slow clock, and slower fan:
3019 # echo 07 | sudo tee -a /sys/kernel/debug/dri/0/pstate
3020 if [[ $DISPLAY ]]; then
3021 mpv --vo=vdpau --hwdec=auto "$@"
3022 else
3023 # waylandvk seems to work the same
3024 mpv --gpu-context=wayland --hwdec=auto
3025 fi
3026 }
3027
3028 mpvd() {
3029 mpv --profile=d "$@";
3030 }
3031 mpva() {
3032 mpv --profile=a "$@";
3033 }
3034 # mpv all media files in . or $1
3035 mpvm() {
3036 local -a extensions arg
3037 # get page source of https://en.wikipedia.org/w/index.php?title=Video_file_format&action=edit
3038 # into /a/x.log, then
3039 # grep '^| *\.' /a/x.log | sed 's/| *//;s/,//g'
3040
3041 # note: to join them together for a regex, do:
3042 # old=; for e in ${extensions[@]/./}; do if [[ ! $old ]]; then old=$e; continue; fi; echo -n "$old|"; old=$e; done; echo $e
3043 extensions=(
3044 .webm
3045 .mkv
3046 .flv
3047 .flv
3048 .vob
3049 .ogv .ogg
3050 .drc
3051 .gif
3052 .gifv
3053 .mng
3054 .avi
3055 .MTS .M2TS .TS
3056 .mov .qt
3057 .wmv
3058 .yuv
3059 .rm
3060 .rmvb
3061 .viv
3062 .asf
3063 .amv
3064 .mp4 .m4p .m4v
3065 .mpg .mp2 .mpeg .mpe .mpv
3066 .mpg .mpeg .m2v
3067 .m4v
3068 .svi
3069 .3gp
3070 .3g2
3071 .mxf
3072 .roq
3073 .nsv
3074 )
3075 arg=("(" -iname "*${extensions[0]}")
3076 for (( i=1 ; i < ${#extensions[@]}; i++ )); do
3077 arg+=(-o -iname "*${extensions[i]}")
3078 done
3079 arg+=(")")
3080 dir=${1:-.}
3081 # debug:
3082 #find $dir "${arg[@]}" -size +200k
3083 find $dir "${arg[@]}" -size +200k -exec mpv --profile=d '{}' +
3084 }
3085 mpvs() {
3086 mpv --profile=s "$@";
3087 }
3088
3089 myirc() {
3090 if [[ ! $1 ]]; then
3091 set -- fsfsys
3092 fi
3093 local -a d
3094 d=( /var/lib/znc/moddata/log/iank/{freenode,libera} )
3095 # use * instead of -r since that does sorted order
3096 ssh root@iankelling.org "for f in ${d[*]}; do cd \$f/#$1; grep '\<iank.*' *; done" | cut --complement -c12-16
3097 }
3098
3099
3100 allmyirc() {
3101 local d
3102 d=/var/lib/znc/moddata/log/iank/freenode
3103 ssh root@iankelling.org "cd $d; find . -mtime -60 -type f -exec grep '\<iank.*' {} +" | sed -r 's,^..([^/]*)/(.{11})(.{5})(.{8}).,\2\4 \1,' | sort
3104 }
3105
3106 # The way pidgin logs with xmpp (maybe related to running cheogram too)
3107 # is that there are sometimes duplicates, and sometimes the a log file
3108 # is for a specific day yet logs messages for subsequent days, and the
3109 # only way to realize that is to notice that the timestamps rolled over
3110 # into a new day, you can't see it in isolation. So, basically, pidgin
3111 # logs are really annoying to read a grep of my messages to find the
3112 # date and time I said when I started and stopped working, so I'm trying
3113 # out a new client: profanity.
3114 mypidgin() {
3115 c /p/c/.purple/logs/jabber/iank@fsf.org/office@conference.fsf.org.chat
3116 for x in *.html; do html2text -o ${x%.html}.txt $x; done;
3117 # shellcheck disable=SC2016 # false positive on ${
3118 grep -A1 ') iank:' ./*.txt \
3119 | sed -r 's/^(.{10})[^ ]*\.txt:\(?([^ ]*)[[:space:]](..). iank:/\1_\2_\3/
3120 s/^[^ ]*\.txt-//
3121 /^--$/d
3122 s/^[^ ]*\.txt:\((.{2}).(.{2}).(.{4}) (.{8}) (.{2})\)?/\3-\1-\2_\4_\5/' \
3123 | sed -n 'x;1d;0~2{G;s/\n/ /;p};${x;p}'
3124 }
3125
3126 # my profanity
3127 #
3128 myprof() {
3129 pushd /home/iank/.local/share/profanity/chatlogs/iank_at_fsf.org/rooms/office_at_conference.fsf.org
3130 logs=(*)
3131 logcount=${#logs[@]}
3132 if (( logcount > 15 )); then
3133 i=$(( logcount - 15 ))
3134 else
3135 i=0
3136 fi
3137 # usually do this on monday, sometimes later
3138 if [[ $(date +%A) == Monday ]]; then
3139 min_date=$(date -d 'monday 2 weeks ago' +%s)
3140 else
3141 min_date=$(date -d 'monday 3 weeks ago' +%s)
3142 fi
3143 for (( ; i < logcount; i++ )); do
3144 log=${logs[$i]}
3145 d=$(date -d "$(head -n1 $log|awk '{print $1}')" +%s)
3146 if (( d < min_date )); then
3147 continue
3148 fi
3149 if awk '$3 == "iank:"' $log | sed -r 's/^(.{10}).(.{8})[^ ]+(.*)/\1_\2\3/' | grep .; then
3150 hr
3151 fi
3152 done
3153 popd
3154 }
3155
3156
3157 # Tail all recent prof logs. Copying from profanity has unwanted line breaks
3158 # especially for links.
3159 profr() {
3160 case $HOSTNAME in
3161 kd)
3162 profr-local
3163 ;;
3164 *)
3165 ssh b8.nz profr-local
3166 ;;
3167 esac
3168 }
3169
3170 profr-local() {
3171 local d0 d1
3172 local -a files
3173 d0="$(date +%Y_%m_%d).log"
3174 d1="$(date -d '1 day ago' +%Y_%m_%d).log"
3175 ngset
3176 files=(/d/p/profanity/chatlogs/iank_at_fsf.org/{*,rooms/*}/{$d0,$d1})
3177 ngreset
3178 if (( ${#files[@]} > 0 )); then
3179 cat "${files[@]}" | sort | tail -n 40
3180 fi
3181 }
3182
3183
3184 # Tail pms in the last day, for the case where we restart profanity and
3185 # didn't check for pms beforehand. Assume the most recent logs are on kd.
3186 # If that isn't the case, use prof-recent-local
3187 prof-recent() {
3188 case $HOSTNAME in
3189 kd)
3190 prof-recent-local
3191 ;;
3192 *)
3193 ssh b8.nz prof-recent-local
3194 ;;
3195 esac
3196 }
3197 prof-recent-local() {
3198 local d dates date files f
3199 # consider making the day count passed by parameter. note: this works: $(date -d '2 day ago' +%Y_%m_%d)
3200 dates=("$(date +%Y_%m_%d)" "$(date -d '1 day ago' +%Y_%m_%d)" )
3201 for d in /d/p/profanity/chatlogs/iank_at_fsf.org/!(rooms); do
3202 files=()
3203 for date in ${dates[@]}; do
3204 f=$d/$date.log
3205 if [[ -e $f ]]; then
3206 files+=($f)
3207 fi
3208 done
3209 if (( ${#files[@]} >= 1 )); then
3210 cat ${files[@]} | tail
3211 hr
3212 fi
3213 done
3214 }
3215
3216 prof-sort() {
3217 case $HOSTNAME in
3218 kd)
3219 prof-recent-sort
3220 ;;
3221 *)
3222 ssh b8.nz prof-recent-sort
3223 ;;
3224 esac
3225 }
3226
3227 prof-recent-sort() {
3228 local d dates date files f
3229 # consider making the day count passed by parameter. note: this works: $(date -d '2 day ago' +%Y_%m_%d)
3230 dates=("$(date +%Y_%m_%d)" "$(date -d '1 day ago' +%Y_%m_%d)" )
3231 files=()
3232 for d in /d/p/profanity/chatlogs/iank_at_fsf.org/!(rooms); do
3233 for date in ${dates[@]}; do
3234 f=$d/$date.log
3235 if [[ -e $f ]]; then
3236 files+=($f)
3237 fi
3238 done
3239 done
3240 for f in "${files[@]}"; do
3241 sed "s/\$/ $f/" $f
3242 done | sort
3243 }
3244
3245
3246 # usage: debvm DEBIAN_VERSION RAM_MB
3247 debvm() {
3248 local ver ram fname src
3249 ver=$1
3250 ram=${2:-2024}
3251 # * is because it might have -backports in the name. we only expect 1 expansion
3252 fnames=( debian-$ver-*nocloud-"$(dpkg --print-architecture)".qcow2 )
3253 if (( ${#fnames[@]} >= 2 )); then
3254 echo "error: iank: unexpected multiple files"
3255 return 1
3256 fi
3257 fname="${fnames[0]}"
3258 src=/a/opt/roms/$fname
3259 if [[ ! -f $src ]]; then
3260 echo debvm: not found $src, download from eg: https://cloud.debian.org/images/cloud/buster/latest/
3261 return 1
3262 fi
3263 cp -a $src /t
3264 # note, in fai-revm we do this: not sure why, maybe because of br device
3265 # --graphics spice,listen=0.0.0.0
3266 m s virt-install --osinfo debian11 --rng /dev/urandom -n deb${ver}tmp --import -r $ram --vcpus 2 --disk /t/$fname --graphics spice
3267 # note: to ssh into this machine will require host key generation: ssh-keygen -A
3268
3269 # random: for cvs2git on gnu www, use debian 10. I could use trisquel,
3270 # but happen to want to try out the debian cloud images. the upstream
3271 # requires python2 and hasn't really changed since the version in d10.
3272 #
3273 # apt install cvs2git cvs
3274 # # 7G was not enough
3275 # mount -o mode=1777,nosuid,nodev,size=34G -t tmpfs tmpfs /tmp
3276 # cvs2git --encoding utf_8 --fallback-encoding ascii --dumpfile=dump www-rsync/www |& tee /tmp/l
3277 ## www-rsync is an rsynced copy of the cvsfrom savannah
3278 }
3279
3280 mygajim() {
3281 local time time_sec time_pretty days
3282 days=${1:-16}
3283 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
3284 case $time in
3285 16*) : ;;
3286 *) continue ;;
3287 esac
3288 if ! time_pretty=$(date +%F.%R -d @$time); then
3289 echo bad time: $time
3290 return 1
3291 fi
3292 echo $time_pretty "$l"
3293 time_sec=${time%%.*}
3294 # only look at the last 18 days. generally just use this for timesheet.
3295 if (( time_sec < EPOCHSECONDS - 60 * 60 * 24 * days )); then break; fi
3296 done
3297 }
3298
3299 allmygajim() {
3300 sqlite3 -separator ' ' /p/c/subdir_files/.local/share/gajim/logs.db "select time, message from logs where contact_name = 'iank'" | less
3301 }
3302
3303 gajlogs() {
3304 sqlite3 -separator ' ' /p/c/subdir_files/.local/share/gajim/logs.db "select time, message from logs" | less
3305 }
3306
3307
3308 net-dev-info() {
3309 e "lspci -nnk|gr -iA2 net"
3310 lspci -nnk|gr -iA2 net
3311 hr
3312 e "s lshw -C network"
3313 hr
3314 sudo lshw -C network
3315 }
3316
3317 nk() {
3318 ser stop NetworkManager
3319 ser disable NetworkManager
3320 ser stop NetworkManager-wait-online.service
3321 ser disable NetworkManager-wait-online.service
3322 ser stop dnsmasq
3323 sudo resolvconf -d NetworkManager
3324 # ser start dnsmasq
3325 sudo ifup br0
3326 }
3327 ngo() {
3328 sudo ifdown br0
3329 ser start NetworkManager
3330 sleep 4
3331 sudo nmtui-connect
3332 }
3333
3334 otp() {
3335 oathtool --totp -b "$*" | xclip -selection clipboard
3336 }
3337 # run cmd and copy output
3338 j() {
3339 "$@" |& pee "xclip -r -selection clipboard" cat
3340 }
3341
3342 # xorg copy. copy text piped into command
3343 xc() {
3344 xclip -r -selection clipboard
3345 }
3346 # echo copy
3347 ec() {
3348 pee "xclip -r -selection clipboard" cat
3349 }
3350
3351 pakaraoke() {
3352 # from http://askubuntu.com/questions/456021/remove-vocals-from-mp3-and-get-only-instrumentals
3353 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
3354 }
3355
3356 pfind() { #find *$1* in $PATH
3357 [[ $# != 1 ]] && { echo requires 1 argument; return 1; }
3358 local pathArray
3359 IFS=: pathArray=($PATH); unset IFS
3360 find "${pathArray[@]}" -iname "*$1*"
3361 }
3362
3363 pick-trash() {
3364 # trash-restore lists everything that has been trashed at or below CWD
3365 # This picks out files just in CWD, not subdirectories,
3366 # which also match grep $1, usually use $1 for a time string
3367 # which you get from running restore-trash once first
3368 local name x ask
3369 local nth=1
3370 # last condition is to not ask again for ones we skipped
3371 while name="$( echo | restore-trash | gr "$PWD/[^/]\+$" | gr "$1" )" \
3372 && [[ $name ]] && (( $(wc -l <<<"$name") >= nth )); do
3373 name="$(echo "$name" | head -n $nth | tail -n 1 )"
3374 read -r -p "$name [Y/n] " ask
3375 if [[ ! $ask || $ask == [Yy] ]]; then
3376 x=$( echo "$name" | gr -o "^\s*[0-9]*" )
3377 echo $x | restore-trash > /dev/null
3378 elif [[ $ask == [Nn] ]]; then
3379 nth=$((nth+1))
3380 else
3381 return
3382 fi
3383 done
3384 }
3385
3386
3387 pub() {
3388 rld /a/h/_site/ li:/var/www/iankelling.org/html
3389 }
3390
3391
3392 pumpa() {
3393 # fixes the menu bar in xmonad. this won\'t be needed when xmonad
3394 # packages catches up on some changes in future (this is written in
3395 # 4/2017)
3396 #
3397 # geekosaur: so youll want to upgrade to xmonad 0.13 or else use a
3398 # locally modified XMonad.Hooks.ManageDocks that doesnt set the
3399 # work area; turns out it\'s impossible to set correctly if you are
3400 # not a fully EWMH compliant desktop environment
3401 #
3402 # geekosaur: chrome shows one failure mode, qt/kde another, other
3403 # gtk apps a third, ... I came up with a setting that works for me
3404 # locally but apparently doesnt work for others, so we joined the
3405 # other tiling window managers in giving up on setting it at all
3406 #
3407 xprop -root -remove _NET_WORKAREA
3408 command pumpa & r
3409 }
3410
3411 # reviewboard, used at my old job
3412 #rbpipe() { rbt post -o --diff-filename=- "$@"; }
3413 #rbp() { rbt post -o "$@"; }
3414
3415 rebr() {
3416 sudo ifdown br0
3417 sudo ifup br0
3418 }
3419
3420
3421 r2e() { command r2e -d /p/c/rss2email.json -c /p/c/rss2email.cfg "$@"; }
3422 # only run on MAIL_HOST. simpler to keep this on one system.
3423 r2eadd() { # usage: name url
3424 # initial setup of rss2email:
3425 # r2e new r2e@iankelling.org
3426 # that initializes files, and sets default email.
3427 # symlink to the config doesnt work, so I copied it to /p/c
3428 # and then use cli option to specify explicit path.
3429 # Only option changed from default config is to set
3430 # force-from = True
3431 #
3432 # or else for a few feeds, the from address is set by the feed, and
3433 # if I fail delivery, then I send a bounce message to that from
3434 # address, which makes me be a spammer.
3435
3436 r2e add $1 "$2" $1@r2e.iankelling.org
3437 # get up to date and dont send old entries now:
3438 r2e run --no-send $1
3439 }
3440
3441 rspicy() { # usage: HOST DOMAIN
3442 # connect to spice vm remote host. use vspicy for local host
3443 local port
3444 # shellcheck disable=SC2087
3445 port=$(ssh $1<<EOF
3446 sudo virsh dumpxml $2|grep "<graphics.*type='spice'" | \
3447 sed -rn "s/.*port='([0-9]+).*/\1/p"
3448 EOF
3449 )
3450 if [[ $port ]]; then
3451 spicy -h $1 -p $port
3452 else
3453 echo "error: no port found. check that the domain is running."
3454 fi
3455 }
3456
3457
3458 scssl() {
3459 # s gem install scss-lint
3460 pushd /a/opt/thoughtbot-guides
3461 git pull --stat
3462 popd
3463 scss-lint -c /a/opt/thoughtbot-guides/style/sass/.scss-lint.yml "$@"
3464 }
3465
3466 skbrc() {
3467 sk -e 2120,245 /b/ds/brc /b/ds/brc2
3468 }
3469
3470 skaraoke() {
3471 local tmp out
3472 out=${2:-${1%.*}.sh}
3473 tmp=$(mktemp -d)
3474 script -t -c "mpv --no-config --no-resume-playback --no-terminal --no-audio-display '$1'" $tmp/typescript 2>$tmp/timing
3475 # todo, the current sleep seems pretty good, but it
3476 # would be nice to have an empirical measurement, or
3477 # some better wait to sync up.
3478 #
3479 # note: --loop-file=no prevents it from hanging if you have that
3480 # set to inf the mpv config.
3481 # --loop=no prevents it from exit code 3 due to stdin if you
3482 # had it set to inf in mpv config.
3483 #
3484 # args go to mpv, for example --volume=80, 50%
3485 cat >$out <<EOFOUTER
3486 #!/bin/bash
3487 trap "trap - TERM && kill 0" INT TERM ERR; set -e
3488 ( sleep .2; scriptreplay <( cat <<'EOF'
3489 $(cat $tmp/timing)
3490 EOF
3491 ) <( cat <<'EOF'
3492 $(cat $tmp/typescript)
3493 EOF
3494 ))&
3495 base64 -d - <<'EOF'| mpv --loop=no --loop-file=no --no-terminal --no-audio-display "\$@" -
3496 $(base64 "$1")
3497 EOF
3498 kill 0
3499 EOFOUTER
3500 rm -r $tmp
3501 chmod +x $out
3502 }
3503
3504 smeld() { # ssh meld usage host1 host2 file
3505 meld <(ssh $1 cat $3) <(ssh $2 cat $3)
3506 }
3507
3508 spd() {
3509 PATH=/usr/local/spdhackfix:$PATH command spd "$@"
3510 }
3511
3512 spamf() { # spamtest on FILE
3513 if (( $# != 1 )); then
3514 e spamtest error: expected 1 arg, filename >&2
3515 return 1
3516 fi
3517 sdncmdroot spamassassin sudo -u Debian-exim spamassassin -t --cf='score PYZOR_CHECK 0' <"$1"
3518 }
3519
3520
3521 # mail related
3522 testmail() {
3523 declare -gi _seq; _seq+=1
3524 echo "test body" | m mail -s "test mail from $HOSTNAME, $_seq" "${@:-root@localhost}"
3525 # for testing to send from an external address, you can do for example
3526 # -fian@iank.bid -aFrom:ian@iank.bid web-6fnbs@mail-tester.com
3527 # note in exim, you can retry a deferred message
3528 # s exim -M MSG_ID
3529 # MSG_ID is in /var/log/exim4/mainlog, looks like 1ccdnD-0001nh-EN
3530 }
3531
3532 # to test sieve, use below command. for fsf mail, see offlineimap-sync script
3533 # make modifications, then copy to live file, use -eW to actually modify mailbox
3534 #
3535 # Another option is to use sieve-test SCRIPT MAIL_FILE. note,
3536 # sieve-test doesnt know about envelopes, Im not sure if sieve-filter does.
3537
3538 # sieve with output filter. arg is mailbox, like INBOX.
3539 # This depends on dovecot conf, notably mail_location in /etc/dovecot/conf.d/10-mail.conf
3540
3541 # always run this first, edit the test files, then run the following
3542 testsieve() {
3543 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
3544 }
3545 runsieve() {
3546 c ~/sieve; cp personal{test,}.sieve; cp lists{test,}.sieve; cp personalend{test,}.sieve
3547 sieve-filter -eWv ~/sieve/maintest.sieve ${1:-INBOX} delete &> /tmp/testsieve.log
3548 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
3549 }
3550
3551 # usage:
3552 # alertme SUBJECT
3553 # printf "subject\nbody\n" | alertme
3554 alertme() {
3555 if [[ -t 0 ]]; then
3556 exim -t <<EOF
3557 From: alertme@b8.nz
3558 To: alerts@iankelling.org
3559 Subject: $*
3560 EOF
3561 else
3562 read -r sub
3563 { cat <<EOF
3564 From: alertme@b8.nz
3565 To: alerts@iankelling.org
3566 Subject: $sub
3567
3568 EOF
3569 cat
3570 } | exim -t
3571 fi
3572 }
3573 daylertme() {
3574 if [[ -t 0 ]]; then
3575 exim -t <<EOF
3576 From: alertme@b8.nz
3577 To: daylert@iankelling.org
3578 Subject: $*
3579 EOF
3580 else
3581 read -r sub
3582 { cat <<EOF
3583 From: alertme@b8.nz
3584 To: daylert@iankelling.org
3585 Subject: $sub
3586
3587 EOF
3588 cat
3589 } | exim -t
3590 fi
3591 }
3592
3593 # alert when a page goes live.
3594 alert200() {
3595 local quiet url tmpdir
3596 quiet=false
3597 case $1 in
3598 # dont send a diff of the html. some html is not very readable
3599 -q) quiet=true
3600 shift
3601 ;;
3602 esac
3603 url="$1"
3604 tmpdir="$(mktemp -d)"
3605 cd $tmpdir
3606 while true; do
3607 if wget -q "$url"; then
3608 if $quiet; then
3609 echo | daylert 200
3610 else
3611 alertme $tmpdir
3612 fi
3613 fi
3614 sleep $(( 120 + RANDOM % 300 ))
3615 done
3616 }
3617
3618 # alert on changes to a webpage (just the base page that curl gets)
3619 # usage: weblert URL [SUBJECT...]
3620 weblert() {
3621 local u old new quiet
3622 quiet=false
3623 case $1 in
3624 # dont send a diff of the html. some html is not very readable
3625 -q) quiet=true
3626 shift
3627 ;;
3628 esac
3629 u="$1"
3630 shift
3631 subject="${*:-weblert}"
3632 old=$(curl -s "$u") ||:
3633 while true; do
3634 new=$(curl -s "$u") ||:
3635 if [[ $old && $new ]]; then
3636 if [[ $new != "$old" ]]; then
3637 if $quiet; then
3638 echo | daylertme "$subject"
3639 else
3640 diff <(printf "%s\n" "$old") <(printf "%s\n" "$new") | daylertme "$subject" ||:
3641 fi
3642 fi
3643 old="$new"
3644 fi
3645 sleep $(( 60 + RANDOM % 120 ))
3646 done
3647 }
3648
3649 torshell() {
3650 # per man torsocks
3651 # shellcheck disable=SC1090 # expected
3652 source "$(type -p torsocks)" on
3653 }
3654
3655 eless2() {
3656 less /var/log/exim4/mymain
3657 }
3658
3659
3660 # mail related
3661 testexim() {
3662 # testmail above calls sendmail, which is a link to exim/postfix.
3663 # its docs dont say a way of adding an argument
3664 # to sendmail to turn on debug output. We could make a wrapper, but
3665 # that is a pain. Exim debug args are documented here:
3666 # http://www.exim.org/exim-html-current/doc/html/spec_html/ch-the_exim_command_line.html
3667 #
3668 # http://www.exim.org/exim-html-current/doc/html/spec_html/ch-building_and_installing_exim.html
3669 # note, for exim daemon, you can turn on debug options by
3670 # adding -d, etc to COMMONOPTIONS in
3671 # /etc/default/exim4
3672 #
3673 # to specify recipients other than those in to, cc, bcc, you can use the cli args, eg:
3674 # exim -t 'test@zroe.org, t2@zroe.org' <<'EOF'
3675 #
3676 # -t = get recipient from header
3677 exim -d -t <<EOF
3678 From: ian@iankelling.org
3679 To: submit@b.b8.nz
3680 Subject: testbug1
3681
3682 Package: test
3683 Version:1
3684
3685 This is a test message.
3686 EOF
3687 }
3688
3689 # test bounce exim
3690 testbexim() {
3691 to=$1
3692 exim -d -f '<>' $to <<EOF
3693 From: Mail Delivery System <Mailer-Daemon@gnu.org>
3694 To: $to
3695 Subject: Mail delivery failed: returning message to sender
3696
3697 This message was created automatically by mail delivery software.
3698 EOF
3699
3700 }
3701
3702
3703 # toggle keyboard
3704 tk() {
3705 # based on
3706 # https://askubuntu.com/questions/160945/is-there-a-way-to-disable-a-laptops-internal-keyboard
3707 id=$(xinput --list --id-only 'AT Translated Set 2 keyboard')
3708 if xinput list | grep -F '∼ AT Translated Set 2 keyboard' &>/dev/null; then
3709 echo enabling keyboard
3710 # find the first slave keyboard number, they are all the same in my output.
3711 # if they werent, worst case we would need to save the slave number somewhere
3712 # when it got disabled.
3713 slave=$(xinput list | sed -n 's/.*slave \+keyboard (\([0-9]*\)).*/\1/p' | head -n1)
3714 xinput reattach $id $slave
3715 else
3716 xinput float $id
3717 fi
3718 }
3719
3720 tm() {
3721 # timer in minutes
3722 # --no-config
3723 (sleep "$(calc "$* * 60")" && mpv --no-config --volume 50 /a/bin/data/alarm.mp3) > /dev/null 2>&1 &
3724 }
3725
3726 ## usage: to connect to my main transmission daemon from a different host, run this
3727 trans-remote-route() {
3728 :
3729 }
3730 trg() { transmission-remote-gtk & r; }
3731 # TODO: this wont work transmission.lan doesnt exist
3732 trc() {
3733 # example, set global upload limit to 100 kilobytes:
3734 # trc -u 100
3735 TR_AUTH=":$(jq -r .profiles[0].password ~/.config/transmission-remote-gtk/config.json)" transmission-remote transmission.lan -ne "$@"
3736 }
3737
3738 trysleep() {
3739 retries="$1"
3740 sleepsecs="$2"
3741 shift 2
3742 for (( i=0; i < retries - 1; i++ )); do
3743 if "$@"; then
3744 return 0
3745 fi
3746 sleep $sleepsecs
3747 done
3748 "$@"
3749 }
3750
3751
3752 tu() {
3753 local s
3754 if [[ -e $1 && ! -w $1 || ! -w $(dirname "$1") ]]; then
3755 s=s;
3756 fi
3757 # full path for using in some initial setup steps
3758 $s /a/exe/teeu "$@"
3759 }
3760
3761 # execute exim in its namespace. Useful args like -Mrm
3762 enn() {
3763 local ecmd pid
3764
3765 ecmd="/usr/sbin/exim4 -C /etc/exim4/my.conf"
3766 if ip a show veth1-mail &>/dev/null; then
3767 s $ecmd "$@"
3768 else
3769 sdncmdroot exim4 $ecmd "$@"
3770 fi
3771 }
3772
3773 # get pid of systemd service
3774 servicepid() {
3775 local pid unit dir
3776 unit="$1"
3777 pid=$(systemctl show --property MainPID --value "$unit")
3778 case $pid in
3779 [1-9]*) : ;;
3780 *)
3781
3782 dir=/sys/fs/cgroup/system.slice
3783 if [[ ! -d $dir ]]; then
3784 # t10 and older directory.
3785 dir=/sys/fs/cgroup/systemd/system.slice
3786 fi
3787
3788 # 0 or empty. This file includes the MainPid, so I expect we
3789 # could just get this in the first place, but i don't know if that
3790 # is always the case.
3791 pid=$(head -n1 $dir/${unit%.service}.service/cgroup.procs)
3792 ;;
3793 esac
3794 if [[ $pid ]]; then
3795 printf "%s\n" "$pid"
3796 else
3797 return 1
3798 fi
3799 }
3800
3801 sdnbash() { # systemd namespace bash
3802 local unit pid
3803 if (( $# != 1 )); then
3804 echo $0: error wrong number of args >&2
3805 return 1
3806 fi
3807 unit=$1
3808 pid=$(servicepid $unit)
3809 m sudo nsenter -t $pid -n -m sudo -u $USER -i bash
3810 }
3811
3812 sdnbashroot() { # systemd namespace bash as root
3813 local unit pid
3814 if (( $# != 1 )); then
3815 echo $0: error wrong number of args >&2
3816 return 1
3817 fi
3818 unit=$1
3819 pid=$(servicepid $unit)
3820 m sudo nsenter -t $pid -n -m bash
3821 }
3822
3823
3824 # systemd namespace cmd
3825 # usage: UNIT CMD...
3826 sdncmd() {
3827 local unit pid tmpf
3828 if (( $# <= 1 )); then
3829 echo $0: error wrong number of args >&2
3830 return 1
3831 fi
3832 unit=$1
3833 shift
3834 pid=$(servicepid $unit)
3835 tmpf=$(mktemp --tmpdir $unit.XXXXXXXXXX)
3836 export -p >$tmpf
3837 printf "%s " "${@@Q}" >>$tmpf
3838 echo >>$tmpf
3839 m sudo nsenter -t $pid -n -m sudo -u $USER -i bash -c ". $tmpf & rm $tmpf"
3840 }
3841
3842 sdncmdroot() { # systemd namespace root command
3843 local unit pid
3844 if (( $# < 2 )); then
3845 echo $0: error wrong number of args >&2
3846 return 1
3847 fi
3848 unit=$1
3849 shift
3850 pid=$(servicepid $unit)
3851 m sudo nsenter -t $pid -n -m "$@"
3852 }
3853
3854
3855 mailnnbash() {
3856 sdnbash mailnn
3857 }
3858
3859 # we use wireguard now, use mailnnbash.
3860 # mailvpnbash() {
3861 # m sudo nsenter -t $(pgrep -f "/usr/sbin/openvpn .* --config /etc/openvpn/.*mail.conf") -n -m sudo -u $USER -i bash
3862 # }
3863
3864 eximbash() {
3865 sdnbashroot exim4
3866 }
3867 spamnn() {
3868 local spamdpid
3869 spamdpid=$(systemctl show --property MainPID --value spamassassin)
3870 m sudo nsenter -t $spamdpid -n -m sudo -u Debian-exim spamassassin "$@"
3871 }
3872 unboundbash() {
3873 sdnbashroot unbound
3874 }
3875
3876 nmtc() {
3877 s nmtui-connect "$@"
3878 }
3879
3880 mailnncheck() {
3881 local unit pid ns mailnn
3882 # mailvpn would belong on the list if using openvpn
3883 for unit in mailnn unbound dovecot spamassassin exim4 radicale; do
3884 pid=$(servicepid $unit)
3885 echo debug: unit=$unit pid=$pid
3886 if [[ ! $pid ]]; then
3887 echo failed to find pid for unit=$unit
3888 continue
3889 fi
3890 if ! ns=$(s readlink /proc/$pid/ns/net); then
3891 echo failed to find ns for unit=$unit pid=$pid
3892 continue
3893 fi
3894 if [[ $mailnn ]]; then
3895 if [[ $ns != "$mailnn" ]]; then
3896 echo "$unit ns $ns != $mailnn"
3897 fi
3898 else
3899 mailnn=$ns
3900 fi
3901 done
3902
3903 }
3904
3905
3906 vpncmd() {
3907 sdncmd openvpn-client-tr@client.service "$@"
3908 }
3909 vpni() {
3910 sdncmd openvpn-client-tr@client.service bash
3911 }
3912 vpnbash() {
3913 sdncmdroot openvpn-client-tr@client.service bash
3914 }
3915
3916
3917 vpn() {
3918 if [[ -e /lib/systemd/system/openvpn-client@.service ]]; then
3919 local vpn_service=openvpn-client
3920 else
3921 local vpn_service=openvpn
3922 fi
3923
3924 [[ $1 ]] || { echo need arg; return 1; }
3925 journalctl --unit=$vpn_service@$1 -f -n0 &
3926 # sometimes the journal doesnt open until after the vpn output
3927 # has happened. hoping this fixes that.
3928 sleep 1
3929 sudo systemctl start $vpn_service@$1
3930 # sometimes the ask-password agent does not work and needs a delay.
3931 sleep .5
3932 # https://bugs.debian.org/cgi-bin/bugreport.cgi?bug=779240
3933 # noticed around 8-2017 after update from around stretch release
3934 # on debian testing, even though the bug is much older.
3935 sudo systemd-tty-ask-password-agent
3936 }
3937
3938 fixu() {
3939 local stats
3940 ls -lad /run/user/1000
3941 stats=$(stat -c%a-%g-%u /run/user/1000)
3942 if [[ $stats != 700-1000-1000 ]]; then
3943 m s chmod 700 /run/user/1000; m s chown iank.iank /run/user/1000
3944 fi
3945 }
3946
3947 # unmute
3948 um() {
3949 local sink card
3950 sink=$(pactl get-default-sink)
3951 if [[ $sink == auto_null ]]; then
3952 # guessing there is just one with an off profile. otherwise we will
3953 # need some other solution, like storing the card identifier that we
3954 # muted with nap.
3955 card=$(pacmd list-cards | sed -n '/^[[:space:]]*index:/{s/^[[:space:]]*index://;h};/^[[:space:]]*active profile: <off>$/{g;p;q}')
3956 m pacmd set-card-profile "$card" output:analog-stereo
3957 fi
3958
3959 m pactl set-sink-mute @DEFAULT_SINK@ false
3960 rm -f /tmp/ianknap
3961 }
3962
3963 nap() {
3964 local sink card
3965 sink=$(pactl get-default-sink)
3966 card="${sink%.*}"
3967 card="${card/output/card}"
3968 m pacmd set-card-profile "$card" off
3969
3970 # clicking on a link in a browser can cause unmute.
3971 # I don't want that. So, use a stronger form of mute
3972 # than this.
3973 #pactl set-sink-mute @DEFAULT_SINK@ true
3974 touch /tmp/ianknap
3975 }
3976
3977
3978 # systemctl is-enabled / status / cat says nothing, instead theres
3979 # some obscure symlink. paths copied from man systemd.unit.
3980 # possibly also usefull, but incomplete, doesnt show units not loaded in memory:
3981 # seru list-dependencies --reverse --all UNIT
3982 sysd-deps() {
3983 local f
3984 local -a dirs search
3985 ngset
3986
3987 case $1 in
3988 u)
3989 search=(
3990 ~/.config/systemd/user.control/*
3991 $XDG_RUNTIME_DIR/systemd/user.control/*
3992 $XDG_RUNTIME_DIR/systemd/transient/*
3993 $XDG_RUNTIME_DIR/systemd/generator.early/*
3994 ~/.config/systemd/user/*
3995 /etc/systemd/user/*
3996 $XDG_RUNTIME_DIR/systemd/user/*
3997 /run/systemd/user/*
3998 $XDG_RUNTIME_DIR/systemd/generator/*
3999 ~/.local/share/systemd/user/*
4000 /usr/lib/systemd/user/*
4001 $XDG_RUNTIME_DIR/systemd/generator.late/*
4002 )
4003 ;;
4004 *)
4005 search=(
4006 /etc/systemd/system.control/*
4007 /run/systemd/system.control/*
4008 /run/systemd/transient/*
4009 /run/systemd/generator.early/*
4010 /etc/systemd/system/*
4011 /etc/systemd/systemd.attached/*
4012 /run/systemd/system/*
4013 /run/systemd/systemd.attached/*
4014 /run/systemd/generator/*
4015 /lib/systemd/system/*
4016 /run/systemd/generator.late/*
4017 )
4018 ;;
4019 esac
4020 for f in "${search[@]}"; do
4021 [[ -d $f ]] || continue
4022 case $f in
4023 *.requires|*.wants)
4024 dirs+=("$f")
4025 ;;
4026 esac
4027 done
4028 # dirs is just so we write out the directory names, ls does it when there is 2 or more dirs.
4029 case ${#dirs[@]} in
4030 1)
4031 echo "${dirs[0]}:"
4032 ll "${dirs[@]}"
4033 ;;
4034 0) : ;;
4035 *)
4036 ll "${dirs[@]}"
4037 ;;
4038 esac
4039 ngreset
4040 }
4041
4042 fixvpndns() {
4043 local link istls
4044 read -r _ link _ istls < <(resolvectl dnsovertls tunfsf)
4045 case $istls in
4046 yes|no) : ;;
4047 *) echo fixvpndns error: unexpected istls value: $istls >&2; return 1 ;;
4048 esac
4049 s busctl call org.freedesktop.resolve1 /org/freedesktop/resolve1 org.freedesktop.resolve1.Manager SetLinkDNSOverTLS is $link no
4050 }
4051
4052 vpnoff() {
4053 [[ $1 ]] || { echo need arg; return 1; }
4054 if [[ -e /lib/systemd/system/openvpn-client@.service ]]; then
4055 local vpn_service=openvpn-client
4056 else
4057 local vpn_service=openvpn
4058 fi
4059 sudo systemctl stop $vpn_service@$1
4060 }
4061 vpnoffc() { # vpn off client
4062 ser stop openvpn-client-tr@client
4063 }
4064 vpnc() {
4065 local unit
4066 unit=openvpn-client-tr@client
4067 sudo -v
4068 if [[ $(systemctl is-active $unit) != active ]]; then
4069 s systemctl start $unit
4070 sleep 1
4071 fi
4072 }
4073
4074
4075 vspicy() { # usage: VIRSH_DOMAIN
4076 # connect to vms made with virt-install
4077 spicy -p "$(sudo virsh dumpxml "$1"|grep "<graphics.*type='spice'"|\
4078 sed -r "s/.*port='([0-9]+).*/\1/")"
4079 }
4080
4081 wian() {
4082 cat-new-files /m/4e/INBOX/new
4083 }
4084 wakehours() {
4085 local sec
4086 if (( $# != 1 )) ; then
4087 echo wakehours: error: expected 1 arg, got $# >&2
4088 return 1
4089 fi
4090 sec=$(( EPOCHSECONDS - $( date +%s -d $1am ) ))
4091 printf "%d:%02d\n" $(( sec / 60 / 60)) $(( (sec / 60) % 60 ))
4092 }
4093
4094 calvis() { # calendar visualize
4095 install -m 600 /dev/null /tmp/calendar-bytes
4096 while read -r l; do
4097 for char in $l; do
4098 # shellcheck disable=SC2059 # intentional for the hex formatting
4099 printf "\x$(printf "%x" $char)" >>/tmp/calendar-bytes
4100 done
4101 done < <(grep -v '[#-]' /p/calendar-data)
4102 /p/c/proc/calendar/linux-amd64/calendar
4103 }
4104
4105 wtr() { curl wttr.in/boston; }
4106
4107 xevkb() { xev -event keyboard; }
4108
4109 # * misc stuff
4110
4111 vrun() {
4112 printf "running: %s\n" "$*"
4113 "$@"
4114 }
4115
4116 electrum() {
4117 # https://electrum.readthedocs.io/en/latest/tor.html
4118 # https://github.com/spesmilo/electrum-docs/issues/129
4119 s rsync -ptog --chown bitcoin:bitcoin ~/.Xauthority /var/lib/bitcoind/.Xauthority
4120 sudo -u bitcoin DISPLAY=$DISPLAY XAUTHORITY=/var/lib/bitcoind/.Xauthority /a/opt/electrum-4.2.1-x86_64.AppImage -p socks5:localhost:9050
4121 }
4122 monero() {
4123 sudo -u bitcoin DISPLAY=$DISPLAY XAUTHORITY=/var/lib/bitcoind/.Xauthority /a/opt/monero-gui-v0.17.3.2/monero-wallet-gui
4124 }
4125
4126
4127 # rg my main files
4128 rgm() {
4129 rg "$@" /p/w.org /a/t.org /a/work.org /b
4130 }
4131
4132 # re all my files more expansively
4133
4134 rem() {
4135 local paths
4136 paths="/p/c /b/"
4137 find $paths -not \( -name .svn -prune -o -name .git -prune \
4138 -o -name .hg -prune -o -name .editor-backups -prune \
4139 -o -name .undo-tree-history -prune \) 2>/dev/null | grep -iP --color=auto -- "$*" ||:
4140 rgv $local_rgv_args -g "!bash_unpublished" -- "$*" $paths /a/work.org ||:
4141 }
4142 reml() { # rem with limit to 5 matches per file
4143 local_rgv_args="-m 5"
4144 rem "$@"
4145 }
4146
4147 rep() {
4148 local paths
4149 paths="/p/c"
4150 find $paths -not \( -name .svn -prune -o -name .git -prune \
4151 -o -name .hg -prune -o -name .editor-backups -prune \
4152 -o -name .undo-tree-history -prune \) 2>/dev/null | grep -iP --color=auto -- "$*" ||:
4153 rgv $local_rgv_args -- "$*" $paths /a/t.org /p/w.org ||:
4154 }
4155 repl() { # rem with limit to 5 matches per file
4156 local local_rgv_args="-m 5"
4157 rem "$@"
4158 }
4159
4160
4161 # re on common fsf files
4162 ref() {
4163 local paths
4164 paths="/f/gluestick /f/brains /f/s /c"
4165 find $paths -not \( -name .svn -prune -o -name .git -prune \
4166 -o -name .hg -prune -o -name .editor-backups -prune \
4167 -o -name .undo-tree-history -prune \) 2>/dev/null | grep -iP --color=auto -- "$*" ||:
4168 rgv -- "$*" $paths /a/work.org ||:
4169 }
4170
4171
4172 # for use in /f/bind
4173 fupzone() {
4174 # shellcheck disable=SC2046 # i want word splitting
4175 ./update-zone $(i s | sed -rn 's/.*db\.(.*)/\1/p')
4176 }
4177
4178 # setup:
4179 # pip3 install linode-cli
4180 # linode-cli
4181 livp9() {
4182 local input ip id tmp
4183 input=$1
4184 if [[ $2 ]]; then
4185 id=$2
4186 ip=$3
4187 else
4188 tmp=$(mktemp)
4189 echo $tmp
4190 linode-cli --json --pretty linodes create --root_pass loxHuceygomGisun | tee $tmp
4191 read -r ip id <<<"$(tail -n+2 $tmp | jq -r '.[0].ipv4[0] , .[0].id')"
4192 for string in $ip $id; do
4193 case $string in
4194 [0-9]*) : ;;
4195 *)
4196 echo "livp9: bad value ip=$ip id=$id input=$input"
4197 return 1
4198 ;;
4199 esac
4200 done
4201 rm $tmp
4202
4203 while true; do
4204 if timeout 4 ssh $ip :; then
4205 break
4206 fi
4207 sleep 3
4208 done
4209 fi
4210 ssh $ip <<EOF
4211 apt-get -qq update
4212 apt-get -qq -y install ffmpeg rsync
4213 mkdir vp9
4214 EOF
4215 m rsync $input $ip:
4216 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
4217 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
4218 rsync $ip:vp9/$input vp9
4219 linode-cli linodes delete $id
4220 }
4221
4222 reset-konsole() {
4223 # we also have a file in /a/c/...konsole...
4224 local f=$HOME/.config/konsolerc
4225 setini DefaultProfile profileian.profile "Desktop Entry" $f
4226 setini Favorites profileian.profile "Favorite Profiles" $f
4227 setini ShowMenuBarByDefault false KonsoleWindow $f
4228 setini TabBarPosition Top TabBar $f
4229 }
4230
4231 reset-sakura() {
4232 while read -r k v; do
4233 # shellcheck disable=SC2154
4234 setini $k $v sakura /a/c/subdir_files/.config/sakura/sakura.conf
4235 done <<'EOF'
4236 colorset1_back rgb(33,37,39)
4237 less_questions true
4238 audible_bell No
4239 visible_bell No
4240 disable_numbered_tabswitch true
4241 scroll_lines 10000000
4242 scrollbar true
4243 EOF
4244 }
4245
4246 # make a page of links found in the files $@. redirect output
4247 linkhtml() {
4248 gr -oh 'https?:\/\/(www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_\+.~#?&//=]*)' "$@" | \
4249 rev | sort -u | rev | sed 's,.*,<a href="\0">\0</a><br\>,'
4250 }
4251
4252 reset-xscreensaver() {
4253 # except for spash, i set these by setting gui options in
4254 # xscreensaver-command -demo
4255 # then finding the corresponding option in .xscreensaver
4256 # spash, i happened to notice in .xscreensaver
4257 #
4258 # dpmsOff, monitor doesnt come back on using old free software supported nvidia card
4259 cat > /home/iank/.xscreensaver <<'EOF'
4260 mode: blank
4261 dpmsEnabled: True
4262 dpmsStandby: 0:07:00
4263 dpmsSuspend: 0:08:00
4264 dpmsOff: 0:00:00
4265 timeout: 0:05:00
4266 lock: True
4267 lockTimeout: 0:06:00
4268 splash: False
4269 EOF
4270
4271 }
4272
4273
4274 # very useful, copy directory structure 3 deep. add remove /*/ to change level
4275 # rsync -aivh --exclude '/*/*/*/' -f"+ */" -f"- *" SRC DEST
4276
4277
4278 # * stuff that makes sense to be at the end
4279 if [[ "$SUDOD" ]]; then
4280 # allow failure, for example if we are sudoing into a user with diffferent/lesser permissions.
4281 cd "$SUDOD" ||:
4282 unset SUDOD
4283 elif [[ -d /a ]] && [[ $PWD == "$HOME" ]] && [[ $- == *i* ]]; then
4284 cd /a
4285 OLDPWD=
4286 fi
4287
4288
4289
4290
4291 # for mitmproxy to get a newer python.
4292 # commented until i want to use it because it
4293 # noticably slows bash startup
4294 #
4295
4296 mypyenvinit () {
4297 if [[ $EUID == 0 || ! -e ~/.pyenv/bin ]]; then
4298 echo "error: dont be root. make sure pyenv is installed"
4299 return 1
4300 fi
4301 export PATH="$HOME/.pyenv/bin:$PATH"
4302 eval "$(pyenv init -)"
4303 eval "$(pyenv virtualenv-init -)"
4304 }
4305
4306
4307
4308 # I have the git repo and a release. either one should work.
4309 # I have both because I was trying to solve an issue that
4310 # turned out to be unrelated.
4311 # ARDUINO_PATH=/a/opt/Arduino/build/linux/work
4312
4313 ## i should have documented this...
4314 # based on https://github.com/keyboardio/Kaleidoscope
4315 export KALEIDOSCOPE_DIR=/a/opt/Kaleidoscope
4316
4317 # They want to be added to the start, but i think
4318 # that should be avoided unless we really need it.
4319 path-add --end ~/.npm-global
4320
4321
4322 path-add --end $HOME/.cargo/bin
4323
4324 if type -P rg &>/dev/null; then
4325 # --no-messages because of annoying errors on broken symlinks
4326 # -z = search .gz etc files
4327 # -. = search dotfiles
4328 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 $?; }
4329 #fails if not exist. ignore
4330 complete -r rg 2>/dev/null ||:
4331 else
4332 alias rg=grr
4333 fi
4334
4335 # rg with respecting vcs ignore files
4336 rgv() {
4337 ret=0
4338 # settings that are turned off for pipes, keep them on.
4339 # Found by searching for "terminal" in --help
4340 # --heading
4341 # -n
4342 #
4343 # -. = search dotfiles
4344 # -z = search zipped files
4345 # -i = case insensitive
4346 # -M = max columns
4347 # --no-messages because of annoying errors on broken symlinks
4348 # --no-ignore-parent because i have /a/.git which ignores almost everything under it.
4349 command rg -n --heading -. -z --no-messages -i -M 900 --no-ignore-parent -g '!.git' -g '!auto-save-list' -g '!.savehist' "$@" || ret=$?
4350 return $ret
4351 }
4352
4353 amall() {
4354 echo "$(tput setaf 5 2>/dev/null ||:)â–ˆ coresite â–ˆ$(tput sgr0 2>/dev/null||:)"
4355 amfsf "$@"
4356 echo "$(tput setaf 5 2>/dev/null ||:)â–ˆ office â–ˆ$(tput sgr0 2>/dev/null||:)"
4357 amoffice "$@"
4358 }
4359 amallq() { # amall quiet
4360 amfsf "$@"
4361 amoffice "$@"
4362 }
4363 amfsf() {
4364 sedi -r '/alertmanager.url/s/@prom.office/@prom/' ~/.config/amtool/config.yml
4365 amtool "$@"
4366 }
4367 amoffice() {
4368 sedi -r '/alertmanager.url/s/@prom.fsf/@prom.office.fsf/' ~/.config/amtool/config.yml
4369 amtool "$@"
4370 }
4371 amls() {
4372 amall silence query "$@"
4373 }
4374 # amtool silence add
4375 amsa() {
4376 amall silence add "$@"
4377 }
4378 # amtool silence force
4379 amsf() {
4380 amall silence add x!="1"
4381 }
4382 amrmall() {
4383 # note: not sure if quoting of this arg is correct
4384 amfsf silence expire "$(amfsf silence query -q)"
4385 amoffice silence expire "$(amoffice silence query -q)"
4386 }
4387
4388
4389 youtube-dl-update() {
4390 sudo wget https://yt-dl.org/downloads/latest/youtube-dl -O /usr/local/bin/youtube-dl
4391 sudo chmod a+rx /usr/local/bin/youtube-dl
4392 }
4393
4394 # https://github.com/yt-dlp/yt-dlp/wiki/Installation
4395 yt-dlp-update() {
4396 sudo curl -L https://github.com/yt-dlp/yt-dlp/releases/latest/download/yt-dlp -o /usr/local/bin/yt-dlp
4397 sudo chmod a+rx /usr/local/bin/yt-dlp # Make executable
4398 }
4399
4400 mpvyt() {
4401 mpv --ytdl ytdl_path=/usr/local/bin/yt-dlp "$@"
4402 }
4403
4404 # taken from default changes to bashrc and bash_profile
4405 path-add --end --ifexists $HOME/.rvm/bin
4406 # also had ruby bin dir, but moved that to environment.sh
4407 # so its included in overall env
4408
4409
4410 # ya, hacky hardcoded hostnames in 2023. we could do better
4411 hssh-update() {
4412 local -a failed_hosts hosts
4413 case $HOSTNAME in
4414 sy|kd)
4415 hosts=(
4416 kd x3.office.fsf.org syw
4417 )
4418 ;;
4419 x3)
4420 hosts=(
4421 b8.nz sywg.b8.nz
4422 )
4423 ;;
4424 esac
4425 for host in ${hosts[@]}; do
4426 e $host
4427 if ! scp /b/fai/fai/config/files/usr/local/bin/hssh/IANK root@$host:/usr/local/bin/hssh; then
4428 failed_hosts+=($host)
4429 fi
4430 done
4431 if (( ${#failed_hosts[@]} >= 1 )); then
4432 echo failed_hosts=${failed_hosts[*]}
4433 return 1
4434 fi
4435 }
4436
4437 noi3bar() {
4438 touch /tmp/noi3bar
4439 }
4440 i3bar() {
4441 rm -fv /tmp/noi3bar
4442 }
4443
4444 # example:
4445 # <#part type="image/jpeg" filename="/home/iank/2023-12-24-ski-trip.jpg" disposition=attachment> <#/part>
4446 #
4447 attach-txt() {
4448 local f
4449 for f; do
4450 if [[ ! -s $f ]]; then
4451 e "error: empty or non-existent file $f"
4452 return 1
4453 fi
4454 done
4455 for f; do
4456 echo '<#part type="image/jpeg" filename="'"$(rl "$f")"'" disposition=attachment> <#/part>'
4457 done | ec
4458 }
4459
4460 ctof() {
4461 units "tempC($1)" tempF
4462 }
4463
4464 ftoc() {
4465 units "tempF($1)" tempC
4466 }
4467
4468 # requires dns/firewall setup first
4469 local-icecast() {
4470 web-conf -e ian@iankelling.org -f 8000 - apache2 live.iankelling.org <<'EOF'
4471 <Location "/fsf.webm">
4472 AuthType Basic
4473 AuthName "basic_auth"
4474 # created with
4475 # htpasswd -c icecast-fsf-htpasswd USERNAME
4476 AuthUserFile "/etc/icecast-fsf-htpasswd"
4477 Require valid-user
4478 </Location>
4479 <Location "/fsf-tech.webm">
4480 AuthType Basic
4481 AuthName "basic_auth"
4482 AuthUserFile "/etc/icecast-fsf-tech-htpasswd"
4483 Require valid-user
4484 </Location>
4485 EOF
4486 }
4487
4488 # obs screen switching of
4489 obof() {
4490 ls -l /tmp/no-obs-auto-scene-switch
4491 touch /tmp/no-obs-auto-scene-switch
4492 }
4493 # obs screen switching on
4494 obon() {
4495 ls -l /tmp/no-obs-auto-scene-switch
4496 if [[ -e /tmp/no-obs-auto-scene-switch ]]; then
4497 rm -f /tmp/no-obs-auto-scene-switch
4498 fi
4499 }
4500
4501 obs-gen-profiles() {
4502 local p=/p/c/basic/profiles
4503 sed 's/fsf-sysops/fsf-tech/g' $p/fsfsysops/basic.ini >$p/fsftech/basic.ini
4504 sed 's/fsf-sysops/fsf/g' $p/fsfsysops/basic.ini >$p/fsf/basic.ini
4505 }
4506
4507 # terminal clear. like clear, but put the prompt at the bottom,
4508 # useful for obs streaming the bottom half of a terminal window.
4509 tclear() {
4510 for ((i=i; i<COLUMNS; i++)); do
4511 echo
4512 done
4513 }
4514
4515
4516 export BASEFILE_DIR=/a/bin/fai-basefiles
4517
4518 #export ANDROID_HOME=/a/opt/android-home
4519 # https://f-droid.org/en/docs/Installing_the_Server_and_Repo_Tools/
4520 #export USE_SDK_WRAPPER=yes
4521 #PATH=$PATH:$ANDROID_HOME/tools:$ANDROID_HOME/platform-tools
4522
4523 # didnt get drush working, if I did, this seems like the
4524 # only good thing to include for it.
4525 # Include Drush completion.
4526 # if [ -f "/home/ian/.drush/drush.complete.sh" ] ; then
4527 # source /home/ian/.drush/drush.complete.sh
4528 # fi
4529
4530
4531 # best practice
4532 unset IFS
4533
4534 # https://wiki.archlinux.org/index.php/Xinitrc#Autostart_X_at_login
4535 # i added an extra condition as gentoo xorg guide says depending on
4536 # $DISPLAY is fragile.
4537 if [[ ! $DISPLAY && $XDG_VTNR == 1 ]] && shopt -q login_shell && isarch; then
4538 exec startx
4539 fi
4540
4541
4542 # ensure no bad programs appending to this file will have an affect
4543 return 0