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