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