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