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