a28d7b7e970a94cc580735002110a3ea675df1b4
[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 # *** are parts and joins and such, and they make reading hard.
2566 # I probably will want to see them sometimes, just have to
2567 # remove that part.
2568 echo $x; sed "s/^./${x%log}/;/\*\*\*/d" $x; hr;
2569 done
2570 done
2571 }
2572 ilog() {
2573 local chan
2574 chan="${1:-#fsfsys}"
2575 # use * instead of -r since that does sorted order
2576 sl root@iankelling.org ilog-local "$chan" | less +G
2577 }
2578
2579 o() {
2580 if type gio &> /dev/null ; then
2581 gio open "$@"
2582 elif type gvfs-open &> /dev/null ; then
2583 gvfs-open "$@"
2584 else
2585 xdg-open "$@"
2586 fi
2587 # another alternative is run-mailcap
2588 }
2589 ccomp xdg-open o
2590
2591 # jfilter() {
2592 # grep -Evi -e "^(\S+\s+){4}(sudo|sshd|cron)\[\S*:" \
2593 # -e "^(\S+\s+){4}systemd\[\S*: (starting|started) (btrfsmaintstop|dynamicipupdate|spamd dns bug fix cronjob|rss2email)\.*$"
2594 # }
2595 # jtail() {
2596 # journalctl -n 10000 -f "$@" | jfilter
2597 # }
2598 # jr() { journalctl "$@" | jfilter | less ; }
2599 # jrf() { journalctl -n 200 -f "$@" | jfilter; }
2600
2601
2602 ## old version for model01. i need to get that firmware working again.
2603 # kff() { # keyboardio firmware flash. you must hold down the tilde key
2604 # pushd /a/opt/Model01-Firmware
2605 # # if we didn't want this yes hack, then remove "shell read" from
2606 # # /a/opt/Kaleidoscope/etc/makefiles/sketch.mk
2607 # yes $'\n' | VERBOSE=1 make flash
2608 # popd
2609 # }
2610
2611
2612 kff() {
2613 pushd /a/opt/Kaleidoscope/examples/Devices/Keyboardio/Model100
2614 make flash
2615 popd
2616 }
2617
2618 wgkey() {
2619 local umask_orig name
2620 if (( $# != 1 )); then
2621 e expected 1 arg >&2
2622 return 1
2623 fi
2624 name=$1
2625 umask_orig=$(umask)
2626 umask 0077
2627 wg genkey | tee $name-priv.key | wg pubkey > $name-pub.key
2628 umask $umask_orig
2629 }
2630
2631 declare -A vpn_ips
2632 vpn_ips[kd]=2
2633 # note: 1, 4, 5 are occupied by mail wireguard
2634 vpn_ips[x3]=8
2635 vpn_ips[sy]=12
2636 vpn_ips[x2]=13
2637 vpn_ips[kw]=27
2638 vpn_ips[bo]=28
2639 vpn_ips[frodo]=34
2640 vpn_ips[s23b]=49
2641
2642 vpn-ips-update() {
2643 local host ipsuf f files
2644 for host in ${!vpn_ips[@]}; do
2645 ipsuf=${vpn_ips[$host]}
2646 wghole $host $ipsuf
2647 u /a/bin/ds/machine_specific/$host/filesystem/etc/systemd/system/openvpn-client-tr@.service <<EOF
2648 [Unit]
2649 Description=OpenVPN tunnel for %I
2650 After=syslog.target network-online.target
2651 Wants=network-online.target
2652 Documentation=man:openvpn(8)
2653 Documentation=https://community.openvpn.net/openvpn/wiki/Openvpn24ManPage
2654 Documentation=https://community.openvpn.net/openvpn/wiki/HOWTO
2655 Requires=iptables.service
2656
2657 [Service]
2658 Type=notify
2659 RuntimeDirectory=openvpn-client
2660 RuntimeDirectoryMode=0710
2661 WorkingDirectory=/etc/openvpn/client
2662 ExecStart=/usr/sbin/openvpn --suppress-timestamps --nobind --config /etc/openvpn/client/%i.conf
2663 # todo, try reenabling this from the default openvpn,
2664 # it was disabled so we could do bind mounts as a command,
2665 # but now systemd handles it
2666 #CapabilityBoundingSet=CAP_IPC_LOCK CAP_NET_ADMIN CAP_NET_RAW CAP_SETGID CAP_SETUID CAP_SYS_CHROOT CAP_DAC_OVERRIDE
2667 LimitNPROC=10
2668 # DeviceAllow=/dev/null rw
2669 # DeviceAllow=/dev/net/tun rw
2670
2671 # we use .1 to make this be on a different network than kd, so that we can
2672 # talk to transmission on kd from remote host, and still use this
2673 # vpn.
2674 ExecStartPre=/usr/bin/flock -w 20 /tmp/newns.flock /a/bin/newns/newns -n 10.174.$ipsuf start %i
2675 ExecStartPre=/sbin/iptables-restore /a/bin/distro-setup/transmission-firewall/netns.rules
2676 # allow wireguard network to connect
2677 ExecStartPre=/usr/sbin/ip r add 10.8.0.0/24 via 10.174.$ipsuf.1 dev veth1-client
2678 ExecStopPost=/usr/bin/flock -w 20 /tmp/newns.flock /a/bin/newns/newns stop %i
2679 PrivateNetwork=true
2680 BindReadOnlyPaths=/etc/tr-resolv:/run/systemd/resolve:norbind /etc/basic-nsswitch:/etc/resolved-nsswitch:norbind
2681
2682 [Install]
2683 WantedBy=multi-user.target
2684 EOF
2685 done
2686
2687 {
2688 for host in ${!vpn_ips[@]}; do
2689 ipsuf=${vpn_ips[$host]}
2690 cat <<EOF
2691 local-data-ptr: "10.2.0.$ipsuf $host.b8.nz"
2692 EOF
2693 done
2694 } | u /b/ds/ptr-data
2695
2696 {
2697 for host in ${!vpn_ips[@]}; do
2698 ipsuf=${vpn_ips[$host]}
2699 cat <<EOF
2700 $host A 10.2.0.$ipsuf
2701 ${host}wg A 10.8.0.$ipsuf
2702 ${host}vp A 10.5.5.$ipsuf
2703 ${host}tr A 10.174.$ipsuf.2
2704 EOF
2705 done
2706 } | cedit vpn-ips-update /p/c/machine_specific/vps/bind-initial/db.b8.nz ||:
2707
2708
2709 echo checking for stray files:
2710
2711 initial_dir=$PWD
2712 cd /a/bin/ds/machine_specific
2713 ngset
2714 files=( */filesystem/etc/systemd/system/openvpn-client-tr@.service )
2715 ngreset
2716 cd $initial_dir
2717 for f in "${files[@]}"; do
2718 host=${f%%/*}
2719 if [[ ! ${vpn_ips[$host]} ]]; then
2720 e /a/bin/ds/machine_specific/$host/filesystem/etc/systemd/system/openvpn-client-tr@.service
2721 fi
2722 done
2723
2724 cd /p/c/machine_specific
2725 ngset
2726 files=( */filesystem/etc/wireguard/wghole.conf )
2727 ngreset
2728 cd $initial_dir
2729 for f in "${files[@]}"; do
2730 host=${f%%/*}
2731 if [[ ! ${vpn_ips[$host]} ]]; then
2732 e /p/c/machine_specific/$host/filesystem/etc/wireguard/wghole.conf
2733 e cedit -s $host /p/c/machine_specific/li/filesystem/etc/wireguard/wgmail.conf '<<<""'
2734 fi
2735 done
2736 }
2737
2738 # usage host ipsuf [extrahost]
2739 #
2740 # If the keys already exist and you want new ones, remove them:
2741 # rm /p/c/machine_specific/$host/filesystem/etc/wireguard/hole-{priv,pub}.key
2742 #
2743 # extrahost is a host/cidr that is allowed to go be routed through the
2744 # vpn by this host.
2745 wghole() {
2746 if (( $# < 2 || $# > 3 )); then
2747 e expected 2-3 arg of hostname, ip suffix, and extrahost >&2
2748 return 1
2749 fi
2750 local host ipsuf umask_orig vpn_allowed
2751 host=$1
2752 ipsuf=$2
2753 if [[ $3 ]]; then
2754 extrahost=,$3
2755 fi
2756 for vpn_host in ${!vpn_ips[@]}; do
2757 if [[ $vpn_host == "$host" ]]; then
2758 continue
2759 fi
2760 vpn_allowed+=",10.174.${vpn_ips[$vpn_host]}.2/32"
2761 done
2762 mkdir -p /p/c/machine_specific/$host/filesystem/etc/wireguard
2763 (
2764 cd /p/c/machine_specific/$host/filesystem/etc/wireguard
2765 umask_orig=$(umask)
2766 umask 0077
2767 if [[ ! -s hole-priv.key || ! -s hole-pub.key ]]; then
2768 wg genkey | tee hole-priv.key | wg pubkey > hole-pub.key
2769 fi
2770 cat >wghole.conf <<EOF
2771 [Interface]
2772 # contents hole-priv.key
2773 PrivateKey = $(cat hole-priv.key)
2774 ListenPort = 1194
2775 Address = 10.8.0.$ipsuf/24
2776 # https://dev.to/tangramvision/what-they-don-t-tell-you-about-setting-up-a-wireguard-vpn-1h2g
2777 # ||: makes the systemd service not fail due to the failed command
2778 PostUp = ping -w10 -c1 10.8.0.1 ||:
2779
2780 [Peer]
2781 # li. called wgmail on that server
2782 PublicKey = CTFsje45qLAU44AbX71Vo+xFJ6rt7Cu6+vdMGyWjBjU=
2783 AllowedIPs = 10.8.0.0/24$vpn_allowed$extrahost
2784 Endpoint = 72.14.176.105:1194
2785 PersistentKeepalive = 25
2786 EOF
2787 umask $umask_orig
2788 # old approach. systemd seems to work fine and cleaner.
2789 rm -f ../network/interfaces.d/wghole
2790 cedit -q $host /p/c/machine_specific/li/filesystem/etc/wireguard/wgmail.conf <<EOF || [[ $? == 1 ]]
2791 [Peer]
2792 PublicKey = $(cat hole-pub.key)
2793 AllowedIPs = 10.8.0.$ipsuf/32,10.174.${vpn_ips[$host]}.2/32
2794 EOF
2795 )
2796 }
2797
2798
2799 mns() { # mount namespace
2800 ns=$1
2801 shift
2802 s mkdir -p /root/mount_namespaces
2803 if ! sudo mountpoint /root/mount_namespaces >/dev/null; then
2804 m sudo mount --bind /root/mount_namespaces /root/mount_namespaces
2805 fi
2806 m sudo mount --make-private /root/mount_namespaces
2807 if [[ ! -e /root/mount_namespaces/$ns ]]; then
2808 m sudo touch /root/mount_namespaces/$ns
2809 fi
2810 if ! sudo mountpoint /root/mount_namespaces/$ns >/dev/null; then
2811 m sudo unshare --propagation slave --mount=/root/mount_namespaces/$ns /bin/true
2812 fi
2813 m sudo -E /usr/bin/nsenter --mount=/root/mount_namespaces/$ns "$@"
2814 }
2815
2816 mnsd() { # mount namespace + systemd namespace
2817 ns=$1
2818 unit=$2
2819 shift 2
2820
2821 s mkdir -p /root/mount_namespaces
2822 if ! sudo mountpoint /root/mount_namespaces >/dev/null; then
2823 m sudo mount --bind /root/mount_namespaces /root/mount_namespaces
2824 fi
2825 m sudo mount --make-private /root/mount_namespaces
2826 if [[ ! -e /root/mount_namespaces/$ns ]]; then
2827 m sudo touch /root/mount_namespaces/$ns
2828 fi
2829 if ! sudo mountpoint /root/mount_namespaces/$ns >/dev/null; then
2830 m sudo unshare --propagation slave --mount=/root/mount_namespaces/$ns /bin/true
2831 fi
2832
2833 pid=$(servicepid $unit)
2834 tmpf=$(mktemp --tmpdir $unit.XXXXXXXXXX)
2835 export -p >$tmpf
2836 printf "%s " "${@@Q}" >>$tmpf
2837 echo >>$tmpf
2838
2839 m sudo nsenter -t $pid -n --mount=/root/mount_namespaces/$ns sudo -u $USER -i bash -c ". $tmpf & sleep 1; rm $tmpf"
2840 }
2841
2842
2843 mnsr() { # mns run
2844 local ns=$1
2845 shift
2846 mns $ns sudo -u iank -E env "PATH=$PATH" "$@"
2847 }
2848
2849 mnsnonetr() {
2850 ns=$1
2851 lomh
2852 if ! s ip netns list | grep -Fx nonet &>/dev/null; then
2853 s ip netns add nonet
2854 fi
2855 mns $ns --net=/var/run/netns/nonet /bin/bash
2856 lomh
2857 }
2858
2859 mnsnonet() {
2860 ns=$1
2861 lomh
2862 if ! s ip netns list | grep -Fx nonet &>/dev/null; then
2863 s ip netns add nonet
2864 fi
2865 mns $ns --net=/var/run/netns/nonet sudo -E -u iank /bin/bash
2866 lomh
2867 }
2868
2869
2870 lom() {
2871 # l = the loopback device
2872 local l base
2873 # get sudo pass cached right away
2874 if ! sudo -nv 2>/dev/null; then
2875 sudo -v
2876 fi
2877 if [[ $1 == /* ]]; then
2878 base=${1##*/}
2879 fs_file=$1
2880 if mns $base mountpoint -q /mnt/$base; then
2881 return 0
2882 fi
2883 l=$(losetup -j $fs_file | sed -rn 's/^([^ ]+): .*/\1/p' | head -n1 ||:)
2884 if [[ ! $l ]]; then
2885 l=$(sudo losetup -f)
2886 m sudo losetup $l $fs_file
2887 fi
2888 if ! sudo cryptsetup status /dev/mapper/$base &>/dev/null; then
2889 if ! m sudo cryptsetup luksOpen $l $base; then
2890 m sudo losetup -d $l
2891 return 1
2892 fi
2893 fi
2894 m sudo mkdir -p /mnt/$base
2895 m mns $base mount /dev/mapper/$base /mnt/$base
2896 m mns $base chown $USER:$USER /mnt/$base
2897 lomh
2898 else
2899 base=$1
2900 if mns $base mountpoint /mnt/$base &>/dev/null; then
2901 m mns $base umount /mnt/$base
2902 fi
2903 if sudo cryptsetup status /dev/mapper/$base &>/dev/null; then
2904 if ! m sudo cryptsetup luksClose /dev/mapper/$base; then
2905 echo lom: failed cryptsetup luksClose /dev/mapper/$base
2906 return 1
2907 fi
2908 fi
2909 l=$(losetup -l --noheadings | awk '$6 ~ /\/'$base'$/ {print $1}')
2910 if [[ $l ]]; then
2911 m sudo losetup -d $l
2912 else
2913 echo lom: warning: no loopback device found
2914 fi
2915 fi
2916 }
2917
2918 # mu personality. for original, just run mp. for 2, run mp 2.
2919 # this is partly duplicated in mail-setup
2920 mp() {
2921 local dead=false
2922 for s in {1..5}; do
2923 if ! killall mu; then
2924 dead=true
2925 break
2926 fi
2927 sleep 1
2928 done
2929 if ! $dead; then
2930 echo error: mu not dead
2931 m psg mu
2932 return 1
2933 fi
2934 suf=$1
2935 set -- /m/mucache ~/.cache/mu /m/.mu ~/.config/mu
2936 while (($#)); do
2937 target=$1$suf
2938 f=$2
2939 shift 2
2940 if [[ -e $f && ! -L $f ]]; then
2941 m rm -rf $f
2942 fi
2943 m ln -sf -T $target $f
2944 done
2945 }
2946
2947 # maildir enable
2948 mdenable() {
2949 local md dst ln_path src two
2950
2951 two=false
2952 case $1 in
2953 -2) two=true shift ;;
2954 esac
2955
2956 for md; do
2957 src=
2958 if $two; then
2959 dst=/m/4e2/$md
2960 else
2961 dst=/m/4e/$md
2962 fi
2963
2964 ln_path=/m/md/$md
2965 for d in /m/md/$md /m/4e2/$md; do
2966 if [[ -d $d && ! -L $d ]]; then
2967 src=$d
2968 break
2969 fi
2970 done
2971 if [[ ! $src ]]; then
2972 echo "error: could not find $md" >&2
2973 return 1
2974 fi
2975 m mv -T $src $dst
2976 m ln -sf -T $dst $ln_path
2977 done
2978 }
2979 md2enable() {
2980 mdenable -2 "$@"
2981 }
2982 mddisable() {
2983 local md=$1
2984 dst=/m/md/$md
2985
2986 ### begin copied from mdenable, but different d ###
2987 for d in /m/4e/$md /m/4e2/$md; do
2988 if [[ -d $d && ! -L $d ]]; then
2989 src=$d
2990 break
2991 fi
2992 done
2993 if [[ ! $src ]]; then
2994 echo "error: could not find $md" >&2
2995 return 1
2996 fi
2997 ### end copy from mdenable ###
2998
2999 if [[ -L $dst ]]; then m rm $dst; fi
3000 m mv -T $src $dst
3001 }
3002
3003
3004 mdt() {
3005 markdown "$1" >/tmp/mdtest.html
3006 firefox /tmp/mdtest.html
3007 }
3008
3009 mo() { xset dpms force off; } # monitor off
3010
3011 mpvgpu() {
3012 # seems to be the best gpu decoding on my nvidia 670.
3013 # vlc gets similar or better framerate, but is much darker output on my test movie at least.
3014
3015
3016 case $HOSTNAME in
3017 kd)
3018 echo 0f | sudo tee -a /sys/kernel/debug/dri/0/pstate
3019 ;;
3020 esac
3021 # going back to the default slow clock, and slower fan:
3022 # echo 07 | sudo tee -a /sys/kernel/debug/dri/0/pstate
3023 if [[ $DISPLAY ]]; then
3024 mpv --vo=vdpau --hwdec=auto "$@"
3025 else
3026 # waylandvk seems to work the same
3027 mpv --gpu-context=wayland --hwdec=auto
3028 fi
3029 }
3030
3031 mpvd() {
3032 mpv --profile=d "$@";
3033 }
3034 mpva() {
3035 mpv --profile=a "$@";
3036 }
3037 # mpv all media files in . or $1
3038 mpvm() {
3039 local -a extensions arg
3040 # get page source of https://en.wikipedia.org/w/index.php?title=Video_file_format&action=edit
3041 # into /a/x.log, then
3042 # grep '^| *\.' /a/x.log | sed 's/| *//;s/,//g'
3043
3044 # note: to join them together for a regex, do:
3045 # old=; for e in ${extensions[@]/./}; do if [[ ! $old ]]; then old=$e; continue; fi; echo -n "$old|"; old=$e; done; echo $e
3046 extensions=(
3047 .webm
3048 .mkv
3049 .flv
3050 .flv
3051 .vob
3052 .ogv .ogg
3053 .drc
3054 .gif
3055 .gifv
3056 .mng
3057 .avi
3058 .MTS .M2TS .TS
3059 .mov .qt
3060 .wmv
3061 .yuv
3062 .rm
3063 .rmvb
3064 .viv
3065 .asf
3066 .amv
3067 .mp4 .m4p .m4v
3068 .mpg .mp2 .mpeg .mpe .mpv
3069 .mpg .mpeg .m2v
3070 .m4v
3071 .svi
3072 .3gp
3073 .3g2
3074 .mxf
3075 .roq
3076 .nsv
3077 )
3078 arg=("(" -iname "*${extensions[0]}")
3079 for (( i=1 ; i < ${#extensions[@]}; i++ )); do
3080 arg+=(-o -iname "*${extensions[i]}")
3081 done
3082 arg+=(")")
3083 dir=${1:-.}
3084 # debug:
3085 #find $dir "${arg[@]}" -size +200k
3086 find $dir "${arg[@]}" -size +200k -exec mpv --profile=d '{}' +
3087 }
3088 mpvs() {
3089 mpv --profile=s "$@";
3090 }
3091
3092 myirc() {
3093 if [[ ! $1 ]]; then
3094 set -- fsfsys
3095 fi
3096 local -a d
3097 d=( /var/lib/znc/moddata/log/iank/{freenode,libera} )
3098 # use * instead of -r since that does sorted order
3099 ssh root@iankelling.org "for f in ${d[*]}; do cd \$f/#$1; grep '\<iank.*' *; done" | cut --complement -c12-16
3100 }
3101
3102
3103 allmyirc() {
3104 local d
3105 d=/var/lib/znc/moddata/log/iank/freenode
3106 ssh root@iankelling.org "cd $d; find . -mtime -60 -type f -exec grep '\<iank.*' {} +" | sed -r 's,^..([^/]*)/(.{11})(.{5})(.{8}).,\2\4 \1,' | sort
3107 }
3108
3109 # The way pidgin logs with xmpp (maybe related to running cheogram too)
3110 # is that there are sometimes duplicates, and sometimes the a log file
3111 # is for a specific day yet logs messages for subsequent days, and the
3112 # only way to realize that is to notice that the timestamps rolled over
3113 # into a new day, you can't see it in isolation. So, basically, pidgin
3114 # logs are really annoying to read a grep of my messages to find the
3115 # date and time I said when I started and stopped working, so I'm trying
3116 # out a new client: profanity.
3117 mypidgin() {
3118 c /p/c/.purple/logs/jabber/iank@fsf.org/office@conference.fsf.org.chat
3119 for x in *.html; do html2text -o ${x%.html}.txt $x; done;
3120 # shellcheck disable=SC2016 # false positive on ${
3121 grep -A1 ') iank:' ./*.txt \
3122 | sed -r 's/^(.{10})[^ ]*\.txt:\(?([^ ]*)[[:space:]](..). iank:/\1_\2_\3/
3123 s/^[^ ]*\.txt-//
3124 /^--$/d
3125 s/^[^ ]*\.txt:\((.{2}).(.{2}).(.{4}) (.{8}) (.{2})\)?/\3-\1-\2_\4_\5/' \
3126 | sed -n 'x;1d;0~2{G;s/\n/ /;p};${x;p}'
3127 }
3128
3129 # my profanity
3130 #
3131 myprof() {
3132 pushd /home/iank/.local/share/profanity/chatlogs/iank_at_fsf.org/rooms/office_at_conference.fsf.org
3133 logs=(*)
3134 logcount=${#logs[@]}
3135 if (( logcount > 15 )); then
3136 i=$(( logcount - 15 ))
3137 else
3138 i=0
3139 fi
3140 # usually do this on monday, sometimes later
3141 if [[ $(date +%A) == Monday ]]; then
3142 min_date=$(date -d 'monday 2 weeks ago' +%s)
3143 else
3144 min_date=$(date -d 'monday 3 weeks ago' +%s)
3145 fi
3146 for (( ; i < logcount; i++ )); do
3147 log=${logs[$i]}
3148 d=$(date -d "$(head -n1 $log|awk '{print $1}')" +%s)
3149 if (( d < min_date )); then
3150 continue
3151 fi
3152 if awk '$3 == "iank:"' $log | sed -r 's/^(.{10}).(.{8})[^ ]+(.*)/\1_\2\3/' | grep .; then
3153 hr
3154 fi
3155 done
3156 popd
3157 }
3158
3159
3160 # Tail all recent prof logs. Copying from profanity has unwanted line breaks
3161 # especially for links.
3162 profr() {
3163 case $HOSTNAME in
3164 kd)
3165 profr-local
3166 ;;
3167 *)
3168 ssh b8.nz profr-local
3169 ;;
3170 esac
3171 }
3172
3173 profr-local() {
3174 local d0 d1
3175 local -a files
3176 d0="$(date +%Y_%m_%d).log"
3177 d1="$(date -d '1 day ago' +%Y_%m_%d).log"
3178 ngset
3179 files=(/d/p/profanity/chatlogs/iank_at_fsf.org/{*,rooms/*}/{$d0,$d1})
3180 ngreset
3181 if (( ${#files[@]} > 0 )); then
3182 cat "${files[@]}" | sort | tail -n 40
3183 fi
3184 }
3185
3186
3187 # Tail pms in the last day, for the case where we restart profanity and
3188 # didn't check for pms beforehand. Assume the most recent logs are on kd.
3189 # If that isn't the case, use prof-recent-local
3190 prof-recent() {
3191 case $HOSTNAME in
3192 kd)
3193 prof-recent-local
3194 ;;
3195 *)
3196 ssh b8.nz prof-recent-local
3197 ;;
3198 esac
3199 }
3200 prof-recent-local() {
3201 local d dates date files f
3202 # consider making the day count passed by parameter. note: this works: $(date -d '2 day ago' +%Y_%m_%d)
3203 dates=("$(date +%Y_%m_%d)" "$(date -d '1 day ago' +%Y_%m_%d)" )
3204 for d in /d/p/profanity/chatlogs/iank_at_fsf.org/!(rooms); do
3205 files=()
3206 for date in ${dates[@]}; do
3207 f=$d/$date.log
3208 if [[ -e $f ]]; then
3209 files+=($f)
3210 fi
3211 done
3212 if (( ${#files[@]} >= 1 )); then
3213 cat ${files[@]} | tail
3214 hr
3215 fi
3216 done
3217 }
3218
3219 prof-sort() {
3220 case $HOSTNAME in
3221 kd)
3222 prof-recent-sort
3223 ;;
3224 *)
3225 ssh b8.nz prof-recent-sort
3226 ;;
3227 esac
3228 }
3229
3230 prof-recent-sort() {
3231 local d dates date files f
3232 # consider making the day count passed by parameter. note: this works: $(date -d '2 day ago' +%Y_%m_%d)
3233 dates=("$(date +%Y_%m_%d)" "$(date -d '1 day ago' +%Y_%m_%d)" )
3234 files=()
3235 for d in /d/p/profanity/chatlogs/iank_at_fsf.org/!(rooms); do
3236 for date in ${dates[@]}; do
3237 f=$d/$date.log
3238 if [[ -e $f ]]; then
3239 files+=($f)
3240 fi
3241 done
3242 done
3243 for f in "${files[@]}"; do
3244 sed "s/\$/ $f/" $f
3245 done | sort
3246 }
3247
3248
3249 # usage: debvm DEBIAN_VERSION RAM_MB
3250 debvm() {
3251 local ver ram fname src
3252 ver=$1
3253 ram=${2:-2024}
3254 # * is because it might have -backports in the name. we only expect 1 expansion
3255 fnames=( debian-$ver-*nocloud-"$(dpkg --print-architecture)".qcow2 )
3256 if (( ${#fnames[@]} >= 2 )); then
3257 echo "error: iank: unexpected multiple files"
3258 return 1
3259 fi
3260 fname="${fnames[0]}"
3261 src=/a/opt/roms/$fname
3262 if [[ ! -f $src ]]; then
3263 echo debvm: not found $src, download from eg: https://cloud.debian.org/images/cloud/buster/latest/
3264 return 1
3265 fi
3266 cp -a $src /t
3267 # note, in fai-revm we do this: not sure why, maybe because of br device
3268 # --graphics spice,listen=0.0.0.0
3269 m s virt-install --osinfo debian11 --rng /dev/urandom -n deb${ver}tmp --import -r $ram --vcpus 2 --disk /t/$fname --graphics spice
3270 # note: to ssh into this machine will require host key generation: ssh-keygen -A
3271
3272 # random: for cvs2git on gnu www, use debian 10. I could use trisquel,
3273 # but happen to want to try out the debian cloud images. the upstream
3274 # requires python2 and hasn't really changed since the version in d10.
3275 #
3276 # apt install cvs2git cvs
3277 # # 7G was not enough
3278 # mount -o mode=1777,nosuid,nodev,size=34G -t tmpfs tmpfs /tmp
3279 # cvs2git --encoding utf_8 --fallback-encoding ascii --dumpfile=dump www-rsync/www |& tee /tmp/l
3280 ## www-rsync is an rsynced copy of the cvsfrom savannah
3281 }
3282
3283 mygajim() {
3284 local time time_sec time_pretty days
3285 days=${1:-16}
3286 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
3287 case $time in
3288 16*) : ;;
3289 *) continue ;;
3290 esac
3291 if ! time_pretty=$(date +%F.%R -d @$time); then
3292 echo bad time: $time
3293 return 1
3294 fi
3295 echo $time_pretty "$l"
3296 time_sec=${time%%.*}
3297 # only look at the last 18 days. generally just use this for timesheet.
3298 if (( time_sec < EPOCHSECONDS - 60 * 60 * 24 * days )); then break; fi
3299 done
3300 }
3301
3302 allmygajim() {
3303 sqlite3 -separator ' ' /p/c/subdir_files/.local/share/gajim/logs.db "select time, message from logs where contact_name = 'iank'" | less
3304 }
3305
3306 gajlogs() {
3307 sqlite3 -separator ' ' /p/c/subdir_files/.local/share/gajim/logs.db "select time, message from logs" | less
3308 }
3309
3310
3311 net-dev-info() {
3312 e "lspci -nnk|gr -iA2 net"
3313 lspci -nnk|gr -iA2 net
3314 hr
3315 e "s lshw -C network"
3316 hr
3317 sudo lshw -C network
3318 }
3319
3320 nk() {
3321 ser stop NetworkManager
3322 ser disable NetworkManager
3323 ser stop NetworkManager-wait-online.service
3324 ser disable NetworkManager-wait-online.service
3325 ser stop dnsmasq
3326 sudo resolvconf -d NetworkManager
3327 # ser start dnsmasq
3328 sudo ifup br0
3329 }
3330 ngo() {
3331 sudo ifdown br0
3332 ser start NetworkManager
3333 sleep 4
3334 sudo nmtui-connect
3335 }
3336
3337 otp() {
3338 oathtool --totp -b "$*" | xclip -selection clipboard
3339 }
3340 # run cmd and copy output
3341 j() {
3342 "$@" |& pee "xclip -r -selection clipboard" cat
3343 }
3344
3345 # xorg copy. copy text piped into command
3346 xc() {
3347 xclip -r -selection clipboard
3348 }
3349 # echo copy
3350 ec() {
3351 pee "xclip -r -selection clipboard" cat
3352 }
3353
3354 pakaraoke() {
3355 # from http://askubuntu.com/questions/456021/remove-vocals-from-mp3-and-get-only-instrumentals
3356 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
3357 }
3358
3359 pfind() { #find *$1* in $PATH
3360 [[ $# != 1 ]] && { echo requires 1 argument; return 1; }
3361 local pathArray
3362 IFS=: pathArray=($PATH); unset IFS
3363 find "${pathArray[@]}" -iname "*$1*"
3364 }
3365
3366 pick-trash() {
3367 # trash-restore lists everything that has been trashed at or below CWD
3368 # This picks out files just in CWD, not subdirectories,
3369 # which also match grep $1, usually use $1 for a time string
3370 # which you get from running restore-trash once first
3371 local name x ask
3372 local nth=1
3373 # last condition is to not ask again for ones we skipped
3374 while name="$( echo | restore-trash | gr "$PWD/[^/]\+$" | gr "$1" )" \
3375 && [[ $name ]] && (( $(wc -l <<<"$name") >= nth )); do
3376 name="$(echo "$name" | head -n $nth | tail -n 1 )"
3377 read -r -p "$name [Y/n] " ask
3378 if [[ ! $ask || $ask == [Yy] ]]; then
3379 x=$( echo "$name" | gr -o "^\s*[0-9]*" )
3380 echo $x | restore-trash > /dev/null
3381 elif [[ $ask == [Nn] ]]; then
3382 nth=$((nth+1))
3383 else
3384 return
3385 fi
3386 done
3387 }
3388
3389
3390 pub() {
3391 rld /a/h/_site/ li:/var/www/iankelling.org/html
3392 }
3393
3394
3395 pumpa() {
3396 # fixes the menu bar in xmonad. this won\'t be needed when xmonad
3397 # packages catches up on some changes in future (this is written in
3398 # 4/2017)
3399 #
3400 # geekosaur: so youll want to upgrade to xmonad 0.13 or else use a
3401 # locally modified XMonad.Hooks.ManageDocks that doesnt set the
3402 # work area; turns out it\'s impossible to set correctly if you are
3403 # not a fully EWMH compliant desktop environment
3404 #
3405 # geekosaur: chrome shows one failure mode, qt/kde another, other
3406 # gtk apps a third, ... I came up with a setting that works for me
3407 # locally but apparently doesnt work for others, so we joined the
3408 # other tiling window managers in giving up on setting it at all
3409 #
3410 xprop -root -remove _NET_WORKAREA
3411 command pumpa & r
3412 }
3413
3414 # reviewboard, used at my old job
3415 #rbpipe() { rbt post -o --diff-filename=- "$@"; }
3416 #rbp() { rbt post -o "$@"; }
3417
3418 rebr() {
3419 sudo ifdown br0
3420 sudo ifup br0
3421 }
3422
3423
3424 r2e() { command r2e -d /p/c/rss2email.json -c /p/c/rss2email.cfg "$@"; }
3425 # only run on MAIL_HOST. simpler to keep this on one system.
3426 r2eadd() { # usage: name url
3427 # initial setup of rss2email:
3428 # r2e new r2e@iankelling.org
3429 # that initializes files, and sets default email.
3430 # symlink to the config doesnt work, so I copied it to /p/c
3431 # and then use cli option to specify explicit path.
3432 # Only option changed from default config is to set
3433 # force-from = True
3434 #
3435 # or else for a few feeds, the from address is set by the feed, and
3436 # if I fail delivery, then I send a bounce message to that from
3437 # address, which makes me be a spammer.
3438
3439 r2e add $1 "$2" $1@r2e.iankelling.org
3440 # get up to date and dont send old entries now:
3441 r2e run --no-send $1
3442 }
3443
3444 rspicy() { # usage: HOST DOMAIN
3445 # connect to spice vm remote host. use vspicy for local host
3446 local port
3447 # shellcheck disable=SC2087
3448 port=$(ssh $1<<EOF
3449 sudo virsh dumpxml $2|grep "<graphics.*type='spice'" | \
3450 sed -rn "s/.*port='([0-9]+).*/\1/p"
3451 EOF
3452 )
3453 if [[ $port ]]; then
3454 spicy -h $1 -p $port
3455 else
3456 echo "error: no port found. check that the domain is running."
3457 fi
3458 }
3459
3460
3461 scssl() {
3462 # s gem install scss-lint
3463 pushd /a/opt/thoughtbot-guides
3464 git pull --stat
3465 popd
3466 scss-lint -c /a/opt/thoughtbot-guides/style/sass/.scss-lint.yml "$@"
3467 }
3468
3469 skbrc() {
3470 sk -e 2120,245 /b/ds/brc /b/ds/brc2
3471 }
3472
3473 skaraoke() {
3474 local tmp out
3475 out=${2:-${1%.*}.sh}
3476 tmp=$(mktemp -d)
3477 script -t -c "mpv --no-config --no-resume-playback --no-terminal --no-audio-display '$1'" $tmp/typescript 2>$tmp/timing
3478 # todo, the current sleep seems pretty good, but it
3479 # would be nice to have an empirical measurement, or
3480 # some better wait to sync up.
3481 #
3482 # note: --loop-file=no prevents it from hanging if you have that
3483 # set to inf the mpv config.
3484 # --loop=no prevents it from exit code 3 due to stdin if you
3485 # had it set to inf in mpv config.
3486 #
3487 # args go to mpv, for example --volume=80, 50%
3488 cat >$out <<EOFOUTER
3489 #!/bin/bash
3490 trap "trap - TERM && kill 0" INT TERM ERR; set -e
3491 ( sleep .2; scriptreplay <( cat <<'EOF'
3492 $(cat $tmp/timing)
3493 EOF
3494 ) <( cat <<'EOF'
3495 $(cat $tmp/typescript)
3496 EOF
3497 ))&
3498 base64 -d - <<'EOF'| mpv --loop=no --loop-file=no --no-terminal --no-audio-display "\$@" -
3499 $(base64 "$1")
3500 EOF
3501 kill 0
3502 EOFOUTER
3503 rm -r $tmp
3504 chmod +x $out
3505 }
3506
3507 smeld() { # ssh meld usage host1 host2 file
3508 meld <(ssh $1 cat $3) <(ssh $2 cat $3)
3509 }
3510
3511 spd() {
3512 PATH=/usr/local/spdhackfix:$PATH command spd "$@"
3513 }
3514
3515 spamf() { # spamtest on FILE
3516 if (( $# != 1 )); then
3517 e spamtest error: expected 1 arg, filename >&2
3518 return 1
3519 fi
3520 sdncmdroot spamassassin sudo -u Debian-exim spamassassin -t --cf='score PYZOR_CHECK 0' <"$1"
3521 }
3522
3523
3524 # mail related
3525 testmail() {
3526 declare -gi _seq; _seq+=1
3527 echo "test body" | m mail -s "test mail from $HOSTNAME, $_seq" "${@:-root@localhost}"
3528 # for testing to send from an external address, you can do for example
3529 # -fian@iank.bid -aFrom:ian@iank.bid web-6fnbs@mail-tester.com
3530 # note in exim, you can retry a deferred message
3531 # s exim -M MSG_ID
3532 # MSG_ID is in /var/log/exim4/mainlog, looks like 1ccdnD-0001nh-EN
3533 }
3534
3535 # to test sieve, use below command. for fsf mail, see offlineimap-sync script
3536 # make modifications, then copy to live file, use -eW to actually modify mailbox
3537 #
3538 # Another option is to use sieve-test SCRIPT MAIL_FILE. note,
3539 # sieve-test doesnt know about envelopes, Im not sure if sieve-filter does.
3540
3541 # sieve with output filter. arg is mailbox, like INBOX.
3542 # This depends on dovecot conf, notably mail_location in /etc/dovecot/conf.d/10-mail.conf
3543
3544 # always run this first, edit the test files, then run the following
3545 testsieve() {
3546 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
3547 }
3548 runsieve() {
3549 c ~/sieve; cp personal{test,}.sieve; cp lists{test,}.sieve; cp personalend{test,}.sieve
3550 sieve-filter -eWv ~/sieve/maintest.sieve ${1:-INBOX} delete &> /tmp/testsieve.log
3551 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
3552 }
3553
3554 # usage:
3555 # alertme SUBJECT
3556 # printf "subject\nbody\n" | alertme
3557 alertme() {
3558 if [[ -t 0 ]]; then
3559 exim -t <<EOF
3560 From: alertme@b8.nz
3561 To: alerts@iankelling.org
3562 Subject: $*
3563 EOF
3564 else
3565 read -r sub
3566 { cat <<EOF
3567 From: alertme@b8.nz
3568 To: alerts@iankelling.org
3569 Subject: $sub
3570
3571 EOF
3572 cat
3573 } | exim -t
3574 fi
3575 }
3576 daylertme() {
3577 if [[ -t 0 ]]; then
3578 exim -t <<EOF
3579 From: alertme@b8.nz
3580 To: daylert@iankelling.org
3581 Subject: $*
3582 EOF
3583 else
3584 read -r sub
3585 { cat <<EOF
3586 From: alertme@b8.nz
3587 To: daylert@iankelling.org
3588 Subject: $sub
3589
3590 EOF
3591 cat
3592 } | exim -t
3593 fi
3594 }
3595
3596 # alert when a page goes live.
3597 alert200() {
3598 local quiet url tmpdir
3599 quiet=false
3600 case $1 in
3601 # dont send a diff of the html. some html is not very readable
3602 -q) quiet=true
3603 shift
3604 ;;
3605 esac
3606 url="$1"
3607 tmpdir="$(mktemp -d)"
3608 cd $tmpdir
3609 while true; do
3610 if wget -q "$url"; then
3611 if $quiet; then
3612 echo | daylert 200
3613 else
3614 alertme $tmpdir
3615 fi
3616 fi
3617 sleep $(( 120 + RANDOM % 300 ))
3618 done
3619 }
3620
3621 # alert on changes to a webpage (just the base page that curl gets)
3622 # usage: weblert URL [SUBJECT...]
3623 weblert() {
3624 local u old new quiet
3625 quiet=false
3626 case $1 in
3627 # dont send a diff of the html. some html is not very readable
3628 -q) quiet=true
3629 shift
3630 ;;
3631 esac
3632 u="$1"
3633 shift
3634 subject="${*:-weblert}"
3635 old=$(curl -s "$u") ||:
3636 while true; do
3637 new=$(curl -s "$u") ||:
3638 if [[ $old && $new ]]; then
3639 if [[ $new != "$old" ]]; then
3640 if $quiet; then
3641 echo | daylertme "$subject"
3642 else
3643 diff <(printf "%s\n" "$old") <(printf "%s\n" "$new") | daylertme "$subject" ||:
3644 fi
3645 fi
3646 old="$new"
3647 fi
3648 sleep $(( 60 + RANDOM % 120 ))
3649 done
3650 }
3651
3652 torshell() {
3653 # per man torsocks
3654 # shellcheck disable=SC1090 # expected
3655 source "$(type -p torsocks)" on
3656 }
3657
3658 eless2() {
3659 less /var/log/exim4/mymain
3660 }
3661
3662
3663 # mail related
3664 testexim() {
3665 # testmail above calls sendmail, which is a link to exim/postfix.
3666 # its docs dont say a way of adding an argument
3667 # to sendmail to turn on debug output. We could make a wrapper, but
3668 # that is a pain. Exim debug args are documented here:
3669 # http://www.exim.org/exim-html-current/doc/html/spec_html/ch-the_exim_command_line.html
3670 #
3671 # http://www.exim.org/exim-html-current/doc/html/spec_html/ch-building_and_installing_exim.html
3672 # note, for exim daemon, you can turn on debug options by
3673 # adding -d, etc to COMMONOPTIONS in
3674 # /etc/default/exim4
3675 #
3676 # to specify recipients other than those in to, cc, bcc, you can use the cli args, eg:
3677 # exim -t 'test@zroe.org, t2@zroe.org' <<'EOF'
3678 #
3679 # -t = get recipient from header
3680 exim -d -t <<EOF
3681 From: ian@iankelling.org
3682 To: submit@b.b8.nz
3683 Subject: testbug1
3684
3685 Package: test
3686 Version:1
3687
3688 This is a test message.
3689 EOF
3690 }
3691
3692 # test bounce exim
3693 testbexim() {
3694 to=$1
3695 exim -d -f '<>' $to <<EOF
3696 From: Mail Delivery System <Mailer-Daemon@gnu.org>
3697 To: $to
3698 Subject: Mail delivery failed: returning message to sender
3699
3700 This message was created automatically by mail delivery software.
3701 EOF
3702
3703 }
3704
3705
3706 # toggle keyboard
3707 tk() {
3708 # based on
3709 # https://askubuntu.com/questions/160945/is-there-a-way-to-disable-a-laptops-internal-keyboard
3710 id=$(xinput --list --id-only 'AT Translated Set 2 keyboard')
3711 if xinput list | grep -F '∼ AT Translated Set 2 keyboard' &>/dev/null; then
3712 echo enabling keyboard
3713 # find the first slave keyboard number, they are all the same in my output.
3714 # if they werent, worst case we would need to save the slave number somewhere
3715 # when it got disabled.
3716 slave=$(xinput list | sed -n 's/.*slave \+keyboard (\([0-9]*\)).*/\1/p' | head -n1)
3717 xinput reattach $id $slave
3718 else
3719 xinput float $id
3720 fi
3721 }
3722
3723 tm() {
3724 # timer in minutes
3725 # --no-config
3726 (sleep "$(calc "$* * 60")" && mpv --no-config --volume 50 /a/bin/data/alarm.mp3) > /dev/null 2>&1 &
3727 }
3728
3729 ## usage: to connect to my main transmission daemon from a different host, run this
3730 trans-remote-route() {
3731 :
3732 }
3733 trg() { transmission-remote-gtk & r; }
3734 # TODO: this wont work transmission.lan doesnt exist
3735 trc() {
3736 # example, set global upload limit to 100 kilobytes:
3737 # trc -u 100
3738 TR_AUTH=":$(jq -r .profiles[0].password ~/.config/transmission-remote-gtk/config.json)" transmission-remote transmission.lan -ne "$@"
3739 }
3740
3741 trysleep() {
3742 retries="$1"
3743 sleepsecs="$2"
3744 shift 2
3745 for (( i=0; i < retries - 1; i++ )); do
3746 if "$@"; then
3747 return 0
3748 fi
3749 sleep $sleepsecs
3750 done
3751 "$@"
3752 }
3753
3754
3755 tu() {
3756 local s
3757 if [[ -e $1 && ! -w $1 || ! -w $(dirname "$1") ]]; then
3758 s=s;
3759 fi
3760 # full path for using in some initial setup steps
3761 $s /a/exe/teeu "$@"
3762 }
3763
3764 # execute exim in its namespace. Useful args like -Mrm
3765 enn() {
3766 local ecmd pid
3767
3768 ecmd="/usr/sbin/exim4 -C /etc/exim4/my.conf"
3769 if ip a show veth1-mail &>/dev/null; then
3770 s $ecmd "$@"
3771 else
3772 sdncmdroot exim4 $ecmd "$@"
3773 fi
3774 }
3775
3776 # get pid of systemd service
3777 servicepid() {
3778 local pid unit dir
3779 unit="$1"
3780 pid=$(systemctl show --property MainPID --value "$unit")
3781 case $pid in
3782 [1-9]*) : ;;
3783 *)
3784
3785 dir=/sys/fs/cgroup/system.slice
3786 if [[ ! -d $dir ]]; then
3787 # t10 and older directory.
3788 dir=/sys/fs/cgroup/systemd/system.slice
3789 fi
3790
3791 # 0 or empty. This file includes the MainPid, so I expect we
3792 # could just get this in the first place, but i don't know if that
3793 # is always the case.
3794 pid=$(head -n1 $dir/${unit%.service}.service/cgroup.procs)
3795 ;;
3796 esac
3797 if [[ $pid ]]; then
3798 printf "%s\n" "$pid"
3799 else
3800 return 1
3801 fi
3802 }
3803
3804 sdnbash() { # systemd namespace bash
3805 local unit pid
3806 if (( $# != 1 )); then
3807 echo $0: error wrong number of args >&2
3808 return 1
3809 fi
3810 unit=$1
3811 pid=$(servicepid $unit)
3812 m sudo nsenter -t $pid -n -m sudo -u $USER -i bash
3813 }
3814
3815 sdnbashroot() { # systemd namespace bash as root
3816 local unit pid
3817 if (( $# != 1 )); then
3818 echo $0: error wrong number of args >&2
3819 return 1
3820 fi
3821 unit=$1
3822 pid=$(servicepid $unit)
3823 m sudo nsenter -t $pid -n -m bash
3824 }
3825
3826
3827 # systemd namespace cmd
3828 # usage: UNIT CMD...
3829 sdncmd() {
3830 local unit pid tmpf
3831 if (( $# <= 1 )); then
3832 echo $0: error wrong number of args >&2
3833 return 1
3834 fi
3835 unit=$1
3836 shift
3837 pid=$(servicepid $unit)
3838 tmpf=$(mktemp --tmpdir $unit.XXXXXXXXXX)
3839 export -p >$tmpf
3840 printf "%s " "${@@Q}" >>$tmpf
3841 echo >>$tmpf
3842 m sudo nsenter -t $pid -n -m sudo -u $USER -i bash -c ". $tmpf & rm $tmpf"
3843 }
3844
3845 sdncmdroot() { # systemd namespace root command
3846 local unit pid
3847 if (( $# < 2 )); then
3848 echo $0: error wrong number of args >&2
3849 return 1
3850 fi
3851 unit=$1
3852 shift
3853 pid=$(servicepid $unit)
3854 m sudo nsenter -t $pid -n -m "$@"
3855 }
3856
3857
3858 mailnnbash() {
3859 sdnbash mailnn
3860 }
3861
3862 # we use wireguard now, use mailnnbash.
3863 # mailvpnbash() {
3864 # m sudo nsenter -t $(pgrep -f "/usr/sbin/openvpn .* --config /etc/openvpn/.*mail.conf") -n -m sudo -u $USER -i bash
3865 # }
3866
3867 eximbash() {
3868 sdnbashroot exim4
3869 }
3870 spamnn() {
3871 local spamdpid
3872 spamdpid=$(systemctl show --property MainPID --value spamassassin)
3873 m sudo nsenter -t $spamdpid -n -m sudo -u Debian-exim spamassassin "$@"
3874 }
3875 unboundbash() {
3876 sdnbashroot unbound
3877 }
3878
3879 nmtc() {
3880 s nmtui-connect "$@"
3881 }
3882
3883 mailnncheck() {
3884 local unit pid ns mailnn
3885 # mailvpn would belong on the list if using openvpn
3886 for unit in mailnn unbound dovecot spamassassin exim4 radicale; do
3887 pid=$(servicepid $unit)
3888 echo debug: unit=$unit pid=$pid
3889 if [[ ! $pid ]]; then
3890 echo failed to find pid for unit=$unit
3891 continue
3892 fi
3893 if ! ns=$(s readlink /proc/$pid/ns/net); then
3894 echo failed to find ns for unit=$unit pid=$pid
3895 continue
3896 fi
3897 if [[ $mailnn ]]; then
3898 if [[ $ns != "$mailnn" ]]; then
3899 echo "$unit ns $ns != $mailnn"
3900 fi
3901 else
3902 mailnn=$ns
3903 fi
3904 done
3905
3906 }
3907
3908
3909 vpncmd() {
3910 sdncmd openvpn-client-tr@client.service "$@"
3911 }
3912 vpni() {
3913 sdncmd openvpn-client-tr@client.service bash
3914 }
3915 vpnbash() {
3916 sdncmdroot openvpn-client-tr@client.service bash
3917 }
3918
3919
3920 vpn() {
3921 if [[ -e /lib/systemd/system/openvpn-client@.service ]]; then
3922 local vpn_service=openvpn-client
3923 else
3924 local vpn_service=openvpn
3925 fi
3926
3927 [[ $1 ]] || { echo need arg; return 1; }
3928 journalctl --unit=$vpn_service@$1 -f -n0 &
3929 # sometimes the journal doesnt open until after the vpn output
3930 # has happened. hoping this fixes that.
3931 sleep 1
3932 sudo systemctl start $vpn_service@$1
3933 # sometimes the ask-password agent does not work and needs a delay.
3934 sleep .5
3935 # https://bugs.debian.org/cgi-bin/bugreport.cgi?bug=779240
3936 # noticed around 8-2017 after update from around stretch release
3937 # on debian testing, even though the bug is much older.
3938 sudo systemd-tty-ask-password-agent
3939 }
3940
3941 fixu() {
3942 local stats
3943 ls -lad /run/user/1000
3944 stats=$(stat -c%a-%g-%u /run/user/1000)
3945 if [[ $stats != 700-1000-1000 ]]; then
3946 m s chmod 700 /run/user/1000; m s chown iank.iank /run/user/1000
3947 fi
3948 }
3949
3950 # unmute
3951 um() {
3952 local sink card
3953 sink=$(pactl get-default-sink)
3954 if [[ $sink == auto_null ]]; then
3955 # guessing there is just one with an off profile. otherwise we will
3956 # need some other solution, like storing the card identifier that we
3957 # muted with nap.
3958 card=$(pacmd list-cards | sed -n '/^[[:space:]]*index:/{s/^[[:space:]]*index://;h};/^[[:space:]]*active profile: <off>$/{g;p;q}')
3959 m pacmd set-card-profile "$card" output:analog-stereo
3960 fi
3961
3962 m pactl set-sink-mute @DEFAULT_SINK@ false
3963 rm -f /tmp/ianknap
3964 }
3965
3966 nap() {
3967 local sink card
3968 sink=$(pactl get-default-sink)
3969 card="${sink%.*}"
3970 card="${card/output/card}"
3971 m pacmd set-card-profile "$card" off
3972
3973 # clicking on a link in a browser can cause unmute.
3974 # I don't want that. So, use a stronger form of mute
3975 # than this.
3976 #pactl set-sink-mute @DEFAULT_SINK@ true
3977 touch /tmp/ianknap
3978 }
3979
3980
3981 # systemctl is-enabled / status / cat says nothing, instead theres
3982 # some obscure symlink. paths copied from man systemd.unit.
3983 # possibly also usefull, but incomplete, doesnt show units not loaded in memory:
3984 # seru list-dependencies --reverse --all UNIT
3985 sysd-deps() {
3986 local f
3987 local -a dirs search
3988 ngset
3989
3990 case $1 in
3991 u)
3992 search=(
3993 ~/.config/systemd/user.control/*
3994 $XDG_RUNTIME_DIR/systemd/user.control/*
3995 $XDG_RUNTIME_DIR/systemd/transient/*
3996 $XDG_RUNTIME_DIR/systemd/generator.early/*
3997 ~/.config/systemd/user/*
3998 /etc/systemd/user/*
3999 $XDG_RUNTIME_DIR/systemd/user/*
4000 /run/systemd/user/*
4001 $XDG_RUNTIME_DIR/systemd/generator/*
4002 ~/.local/share/systemd/user/*
4003 /usr/lib/systemd/user/*
4004 $XDG_RUNTIME_DIR/systemd/generator.late/*
4005 )
4006 ;;
4007 *)
4008 search=(
4009 /etc/systemd/system.control/*
4010 /run/systemd/system.control/*
4011 /run/systemd/transient/*
4012 /run/systemd/generator.early/*
4013 /etc/systemd/system/*
4014 /etc/systemd/systemd.attached/*
4015 /run/systemd/system/*
4016 /run/systemd/systemd.attached/*
4017 /run/systemd/generator/*
4018 /lib/systemd/system/*
4019 /run/systemd/generator.late/*
4020 )
4021 ;;
4022 esac
4023 for f in "${search[@]}"; do
4024 [[ -d $f ]] || continue
4025 case $f in
4026 *.requires|*.wants)
4027 dirs+=("$f")
4028 ;;
4029 esac
4030 done
4031 # dirs is just so we write out the directory names, ls does it when there is 2 or more dirs.
4032 case ${#dirs[@]} in
4033 1)
4034 echo "${dirs[0]}:"
4035 ll "${dirs[@]}"
4036 ;;
4037 0) : ;;
4038 *)
4039 ll "${dirs[@]}"
4040 ;;
4041 esac
4042 ngreset
4043 }
4044
4045 fixvpndns() {
4046 local link istls
4047 read -r _ link _ istls < <(resolvectl dnsovertls tunfsf)
4048 case $istls in
4049 yes|no) : ;;
4050 *) echo fixvpndns error: unexpected istls value: $istls >&2; return 1 ;;
4051 esac
4052 s busctl call org.freedesktop.resolve1 /org/freedesktop/resolve1 org.freedesktop.resolve1.Manager SetLinkDNSOverTLS is $link no
4053 }
4054
4055 vpnoff() {
4056 [[ $1 ]] || { echo need arg; return 1; }
4057 if [[ -e /lib/systemd/system/openvpn-client@.service ]]; then
4058 local vpn_service=openvpn-client
4059 else
4060 local vpn_service=openvpn
4061 fi
4062 sudo systemctl stop $vpn_service@$1
4063 }
4064 vpnoffc() { # vpn off client
4065 ser stop openvpn-client-tr@client
4066 }
4067 vpnc() {
4068 local unit
4069 unit=openvpn-client-tr@client
4070 sudo -v
4071 if [[ $(systemctl is-active $unit) != active ]]; then
4072 s systemctl start $unit
4073 sleep 1
4074 fi
4075 }
4076
4077
4078 vspicy() { # usage: VIRSH_DOMAIN
4079 # connect to vms made with virt-install
4080 spicy -p "$(sudo virsh dumpxml "$1"|grep "<graphics.*type='spice'"|\
4081 sed -r "s/.*port='([0-9]+).*/\1/")"
4082 }
4083
4084 wian() {
4085 cat-new-files /m/4e/INBOX/new
4086 }
4087 wakehours() {
4088 local sec
4089 if (( $# != 1 )) ; then
4090 echo wakehours: error: expected 1 arg, got $# >&2
4091 return 1
4092 fi
4093 sec=$(( EPOCHSECONDS - $( date +%s -d $1am ) ))
4094 printf "%d:%02d\n" $(( sec / 60 / 60)) $(( (sec / 60) % 60 ))
4095 }
4096
4097 calvis() { # calendar visualize
4098 install -m 600 /dev/null /tmp/calendar-bytes
4099 while read -r l; do
4100 for char in $l; do
4101 # shellcheck disable=SC2059 # intentional for the hex formatting
4102 printf "\x$(printf "%x" $char)" >>/tmp/calendar-bytes
4103 done
4104 done < <(grep -v '[#-]' /p/calendar-data)
4105 /p/c/proc/calendar/linux-amd64/calendar
4106 }
4107
4108 wtr() { curl wttr.in/boston; }
4109
4110 xevkb() { xev -event keyboard; }
4111
4112 # * misc stuff
4113
4114 vrun() {
4115 printf "running: %s\n" "$*"
4116 "$@"
4117 }
4118
4119 electrum() {
4120 # https://electrum.readthedocs.io/en/latest/tor.html
4121 # https://github.com/spesmilo/electrum-docs/issues/129
4122 s rsync -ptog --chown bitcoin:bitcoin ~/.Xauthority /var/lib/bitcoind/.Xauthority
4123 sudo -u bitcoin DISPLAY=$DISPLAY XAUTHORITY=/var/lib/bitcoind/.Xauthority /a/opt/electrum-4.2.1-x86_64.AppImage -p socks5:localhost:9050
4124 }
4125 monero() {
4126 sudo -u bitcoin DISPLAY=$DISPLAY XAUTHORITY=/var/lib/bitcoind/.Xauthority /a/opt/monero-gui-v0.17.3.2/monero-wallet-gui
4127 }
4128
4129
4130 # rg my main files
4131 rgm() {
4132 rg "$@" /p/w.org /a/t.org /a/work.org /b
4133 }
4134
4135 # re all my files more expansively
4136
4137 rem() {
4138 local paths
4139 paths="/p/c /b/"
4140 find $paths -not \( -name .svn -prune -o -name .git -prune \
4141 -o -name .hg -prune -o -name .editor-backups -prune \
4142 -o -name .undo-tree-history -prune \) 2>/dev/null | grep -iP --color=auto -- "$*" ||:
4143 rgv $local_rgv_args -g "!bash_unpublished" -- "$*" $paths /a/work.org ||:
4144 }
4145 reml() { # rem with limit to 5 matches per file
4146 local_rgv_args="-m 5"
4147 rem "$@"
4148 }
4149
4150 rep() {
4151 local paths
4152 paths="/p/c"
4153 find $paths -not \( -name .svn -prune -o -name .git -prune \
4154 -o -name .hg -prune -o -name .editor-backups -prune \
4155 -o -name .undo-tree-history -prune \) 2>/dev/null | grep -iP --color=auto -- "$*" ||:
4156 rgv $local_rgv_args -- "$*" $paths /a/t.org /p/w.org ||:
4157 }
4158 repl() { # rem with limit to 5 matches per file
4159 local local_rgv_args="-m 5"
4160 rem "$@"
4161 }
4162
4163
4164 # re on common fsf files
4165 ref() {
4166 local paths
4167 paths="/f/gluestick /f/brains /f/s /c"
4168 find $paths -not \( -name .svn -prune -o -name .git -prune \
4169 -o -name .hg -prune -o -name .editor-backups -prune \
4170 -o -name .undo-tree-history -prune \) 2>/dev/null | grep -iP --color=auto -- "$*" ||:
4171 rgv -- "$*" $paths /a/work.org ||:
4172 }
4173
4174
4175 # for use in /f/bind
4176 fupzone() {
4177 # shellcheck disable=SC2046 # i want word splitting
4178 ./update-zone $(i s | sed -rn 's/.*db\.(.*)/\1/p')
4179 }
4180
4181 # setup:
4182 # pip3 install linode-cli
4183 # linode-cli
4184 livp9() {
4185 local input ip id tmp
4186 input=$1
4187 if [[ $2 ]]; then
4188 id=$2
4189 ip=$3
4190 else
4191 tmp=$(mktemp)
4192 echo $tmp
4193 linode-cli --json --pretty linodes create --root_pass loxHuceygomGisun | tee $tmp
4194 read -r ip id <<<"$(tail -n+2 $tmp | jq -r '.[0].ipv4[0] , .[0].id')"
4195 for string in $ip $id; do
4196 case $string in
4197 [0-9]*) : ;;
4198 *)
4199 echo "livp9: bad value ip=$ip id=$id input=$input"
4200 return 1
4201 ;;
4202 esac
4203 done
4204 rm $tmp
4205
4206 while true; do
4207 if timeout 4 ssh $ip :; then
4208 break
4209 fi
4210 sleep 3
4211 done
4212 fi
4213 ssh $ip <<EOF
4214 apt-get -qq update
4215 apt-get -qq -y install ffmpeg rsync
4216 mkdir vp9
4217 EOF
4218 m rsync $input $ip:
4219 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
4220 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
4221 rsync $ip:vp9/$input vp9
4222 linode-cli linodes delete $id
4223 }
4224
4225 reset-konsole() {
4226 # we also have a file in /a/c/...konsole...
4227 local f=$HOME/.config/konsolerc
4228 setini DefaultProfile profileian.profile "Desktop Entry" $f
4229 setini Favorites profileian.profile "Favorite Profiles" $f
4230 setini ShowMenuBarByDefault false KonsoleWindow $f
4231 setini TabBarPosition Top TabBar $f
4232 }
4233
4234 reset-sakura() {
4235 while read -r k v; do
4236 # shellcheck disable=SC2154
4237 setini $k $v sakura /a/c/subdir_files/.config/sakura/sakura.conf
4238 done <<'EOF'
4239 colorset1_back rgb(33,37,39)
4240 less_questions true
4241 audible_bell No
4242 visible_bell No
4243 disable_numbered_tabswitch true
4244 scroll_lines 10000000
4245 scrollbar true
4246 EOF
4247 }
4248
4249 # make a page of links found in the files $@. redirect output
4250 linkhtml() {
4251 gr -oh 'https?:\/\/(www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_\+.~#?&//=]*)' "$@" | \
4252 rev | sort -u | rev | sed 's,.*,<a href="\0">\0</a><br\>,'
4253 }
4254
4255 reset-xscreensaver() {
4256 # except for spash, i set these by setting gui options in
4257 # xscreensaver-command -demo
4258 # then finding the corresponding option in .xscreensaver
4259 # spash, i happened to notice in .xscreensaver
4260 #
4261 # dpmsOff, monitor doesnt come back on using old free software supported nvidia card
4262 cat > /home/iank/.xscreensaver <<'EOF'
4263 mode: blank
4264 dpmsEnabled: True
4265 dpmsStandby: 0:07:00
4266 dpmsSuspend: 0:08:00
4267 dpmsOff: 0:00:00
4268 timeout: 0:05:00
4269 lock: True
4270 lockTimeout: 0:06:00
4271 splash: False
4272 EOF
4273
4274 }
4275
4276
4277 # very useful, copy directory structure 3 deep. add remove /*/ to change level
4278 # rsync -aivh --exclude '/*/*/*/' -f"+ */" -f"- *" SRC DEST
4279
4280
4281 # * stuff that makes sense to be at the end
4282 if [[ "$SUDOD" ]]; then
4283 # allow failure, for example if we are sudoing into a user with diffferent/lesser permissions.
4284 cd "$SUDOD" ||:
4285 unset SUDOD
4286 elif [[ -d /a ]] && [[ $PWD == "$HOME" ]] && [[ $- == *i* ]]; then
4287 cd /a
4288 OLDPWD=
4289 fi
4290
4291
4292
4293
4294 # for mitmproxy to get a newer python.
4295 # commented until i want to use it because it
4296 # noticably slows bash startup
4297 #
4298
4299 mypyenvinit () {
4300 if [[ $EUID == 0 || ! -e ~/.pyenv/bin ]]; then
4301 echo "error: dont be root. make sure pyenv is installed"
4302 return 1
4303 fi
4304 export PATH="$HOME/.pyenv/bin:$PATH"
4305 eval "$(pyenv init -)"
4306 eval "$(pyenv virtualenv-init -)"
4307 }
4308
4309
4310
4311 # I have the git repo and a release. either one should work.
4312 # I have both because I was trying to solve an issue that
4313 # turned out to be unrelated.
4314 # ARDUINO_PATH=/a/opt/Arduino/build/linux/work
4315
4316 ## i should have documented this...
4317 # based on https://github.com/keyboardio/Kaleidoscope
4318 export KALEIDOSCOPE_DIR=/a/opt/Kaleidoscope
4319
4320 # They want to be added to the start, but i think
4321 # that should be avoided unless we really need it.
4322 path-add --end ~/.npm-global
4323
4324
4325 path-add --end $HOME/.cargo/bin
4326
4327 if type -P rg &>/dev/null; then
4328 # --no-messages because of annoying errors on broken symlinks
4329 # -z = search .gz etc files
4330 # -. = search dotfiles
4331 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 $?; }
4332 #fails if not exist. ignore
4333 complete -r rg 2>/dev/null ||:
4334 else
4335 alias rg=grr
4336 fi
4337
4338 # rg with respecting vcs ignore files
4339 rgv() {
4340 ret=0
4341 # settings that are turned off for pipes, keep them on.
4342 # Found by searching for "terminal" in --help
4343 # --heading
4344 # -n
4345 #
4346 # -. = search dotfiles
4347 # -z = search zipped files
4348 # -i = case insensitive
4349 # -M = max columns
4350 # --no-messages because of annoying errors on broken symlinks
4351 # --no-ignore-parent because i have /a/.git which ignores almost everything under it.
4352 command rg -n --heading -. -z --no-messages -i -M 900 --no-ignore-parent -g '!.git' -g '!auto-save-list' -g '!.savehist' "$@" || ret=$?
4353 return $ret
4354 }
4355
4356 amall() {
4357 echo "$(tput setaf 5 2>/dev/null ||:)█ coresite █$(tput sgr0 2>/dev/null||:)"
4358 amfsf "$@"
4359 echo "$(tput setaf 5 2>/dev/null ||:)█ office █$(tput sgr0 2>/dev/null||:)"
4360 amoffice "$@"
4361 }
4362 amallq() { # amall quiet
4363 amfsf "$@"
4364 amoffice "$@"
4365 }
4366 amfsf() {
4367 sedi -r '/alertmanager.url/s/@prom.office/@prom/' ~/.config/amtool/config.yml
4368 amtool "$@"
4369 }
4370 amoffice() {
4371 sedi -r '/alertmanager.url/s/@prom.fsf/@prom.office.fsf/' ~/.config/amtool/config.yml
4372 amtool "$@"
4373 }
4374 amls() {
4375 amall silence query "$@"
4376 }
4377 # amtool silence add
4378 amsa() {
4379 amall silence add "$@"
4380 }
4381 # amtool silence force
4382 amsf() {
4383 amall silence add x!="1"
4384 }
4385 amrmall() {
4386 # note: not sure if quoting of this arg is correct
4387 amfsf silence expire "$(amfsf silence query -q)"
4388 amoffice silence expire "$(amoffice silence query -q)"
4389 }
4390
4391
4392 youtube-dl-update() {
4393 sudo wget https://yt-dl.org/downloads/latest/youtube-dl -O /usr/local/bin/youtube-dl
4394 sudo chmod a+rx /usr/local/bin/youtube-dl
4395 }
4396
4397 # https://github.com/yt-dlp/yt-dlp/wiki/Installation
4398 yt-dlp-update() {
4399 sudo curl -L https://github.com/yt-dlp/yt-dlp/releases/latest/download/yt-dlp -o /usr/local/bin/yt-dlp
4400 sudo chmod a+rx /usr/local/bin/yt-dlp # Make executable
4401 }
4402
4403 mpvyt() {
4404 mpv --ytdl ytdl_path=/usr/local/bin/yt-dlp "$@"
4405 }
4406
4407 # taken from default changes to bashrc and bash_profile
4408 path-add --end --ifexists $HOME/.rvm/bin
4409 # also had ruby bin dir, but moved that to environment.sh
4410 # so its included in overall env
4411
4412
4413 # ya, hacky hardcoded hostnames in 2023. we could do better
4414 hssh-update() {
4415 local -a failed_hosts hosts
4416 case $HOSTNAME in
4417 sy|kd)
4418 hosts=(
4419 kd x3.office.fsf.org syw
4420 )
4421 ;;
4422 x3)
4423 hosts=(
4424 b8.nz sywg.b8.nz
4425 )
4426 ;;
4427 esac
4428 for host in ${hosts[@]}; do
4429 e $host
4430 if ! scp /b/fai/fai/config/files/usr/local/bin/hssh/IANK root@$host:/usr/local/bin/hssh; then
4431 failed_hosts+=($host)
4432 fi
4433 done
4434 if (( ${#failed_hosts[@]} >= 1 )); then
4435 echo failed_hosts=${failed_hosts[*]}
4436 return 1
4437 fi
4438 }
4439
4440 noi3bar() {
4441 touch /tmp/noi3bar
4442 }
4443 i3bar() {
4444 rm -fv /tmp/noi3bar
4445 }
4446
4447 # example:
4448 # <#part type="image/jpeg" filename="/home/iank/2023-12-24-ski-trip.jpg" disposition=attachment> <#/part>
4449 #
4450 attach-txt() {
4451 local f
4452 for f; do
4453 if [[ ! -s $f ]]; then
4454 e "error: empty or non-existent file $f"
4455 return 1
4456 fi
4457 done
4458 for f; do
4459 echo '<#part type="image/jpeg" filename="'"$(rl "$f")"'" disposition=attachment> <#/part>'
4460 done | ec
4461 }
4462
4463 ctof() {
4464 units "tempC($1)" tempF
4465 }
4466
4467 ftoc() {
4468 units "tempF($1)" tempC
4469 }
4470
4471 # requires dns/firewall setup first
4472 local-icecast() {
4473 web-conf -e ian@iankelling.org -f 8000 - apache2 live.iankelling.org <<'EOF'
4474 <Location "/fsf.webm">
4475 AuthType Basic
4476 AuthName "basic_auth"
4477 # created with
4478 # htpasswd -c icecast-fsf-htpasswd USERNAME
4479 AuthUserFile "/etc/icecast-fsf-htpasswd"
4480 Require valid-user
4481 </Location>
4482 <Location "/fsf-tech.webm">
4483 AuthType Basic
4484 AuthName "basic_auth"
4485 AuthUserFile "/etc/icecast-fsf-tech-htpasswd"
4486 Require valid-user
4487 </Location>
4488 EOF
4489 }
4490
4491 # obs screen switching of
4492 obof() {
4493 ls -l /tmp/no-obs-auto-scene-switch
4494 touch /tmp/no-obs-auto-scene-switch
4495 }
4496 # obs screen switching on
4497 obon() {
4498 ls -l /tmp/no-obs-auto-scene-switch
4499 if [[ -e /tmp/no-obs-auto-scene-switch ]]; then
4500 rm -f /tmp/no-obs-auto-scene-switch
4501 fi
4502 }
4503
4504 obs-gen-profiles() {
4505 local p=/p/c/basic/profiles
4506 sed 's/fsf-sysops/fsf-tech/g' $p/fsfsysops/basic.ini >$p/fsftech/basic.ini
4507 sed 's/fsf-sysops/fsf/g' $p/fsfsysops/basic.ini >$p/fsf/basic.ini
4508 }
4509
4510 # terminal clear. like clear, but put the prompt at the bottom,
4511 # useful for obs streaming the bottom half of a terminal window.
4512 tclear() {
4513 for ((i=0; i<COLUMNS; i++)); do
4514 echo
4515 done
4516 }
4517
4518
4519 export BASEFILE_DIR=/a/bin/fai-basefiles
4520
4521 #export ANDROID_HOME=/a/opt/android-home
4522 # https://f-droid.org/en/docs/Installing_the_Server_and_Repo_Tools/
4523 #export USE_SDK_WRAPPER=yes
4524 #PATH=$PATH:$ANDROID_HOME/tools:$ANDROID_HOME/platform-tools
4525
4526 # didnt get drush working, if I did, this seems like the
4527 # only good thing to include for it.
4528 # Include Drush completion.
4529 # if [ -f "/home/ian/.drush/drush.complete.sh" ] ; then
4530 # source /home/ian/.drush/drush.complete.sh
4531 # fi
4532
4533
4534 # best practice
4535 unset IFS
4536
4537 # https://wiki.archlinux.org/index.php/Xinitrc#Autostart_X_at_login
4538 # i added an extra condition as gentoo xorg guide says depending on
4539 # $DISPLAY is fragile.
4540 if [[ ! $DISPLAY && $XDG_VTNR == 1 ]] && shopt -q login_shell && isarch; then
4541 exec startx
4542 fi
4543
4544
4545 # ensure no bad programs appending to this file will have an affect
4546 return 0