🕘︎ - 2025-06-10
It's not that often I encounter a QR code I want to open on my computer, but when I do it would be nice to simply be able to select the graphic on the screen, and then have the URL open.
Combining Flameshot and zbar-tools in a small shell script allows me to do just that: the script checks for PNG in the clipboard, and if there is none, launches Flameshot. After that zbarimg
is run to extract the URL, which is then opened in the default browser:
#!/bin/sh
# open-qr-code - script to interpret a qr-code in the clipboard and
# open it.
#
# Depends on: xclip, zbar-tools, libnotify-bin, flameshot.
# 1) Save png from clipboard in file:
CAPTURE=0
if ! xclip -o -t TARGETS -selection clipboard | grep --quiet image/png; then
# 0) If there is no image in clipboard, launch flameshot to pick one:
sed -i 's/showDesktopNotification=true/showDesktopNotification=false/' ~/.config/flameshot/flameshot.ini
flameshot gui --clipboard --accept-on-select
sed -i 's/showDesktopNotification=false/showDesktopNotification=true/' ~/.config/flameshot/flameshot.ini
CAPTURE=1
fi
if xclip -o -t TARGETS -selection clipboard | grep --quiet image/png; then
IMAGE=$(/bin/mktemp --tmpdir qr-codeXXXXXX.png)
xclip -t image/png -selection clipboard -o > "$IMAGE"
# 2) Run QR-code detector:
if CONTENT=$(zbarimg --quiet "$IMAGE"); then
# 3) If it is a URL, open it in a browser:
URL=$(echo "$CONTENT" | sed 's/^QR-Code://')
if echo "$URL" | grep --quiet '^http'; then
notify-send --icon /usr/share/icons/Moka/256x256@2x/apps/qr-creator.png "Open QR code" "Opened $URL"
x-www-browser --new-window "$URL" &
else
notify-send --icon /usr/share/icons/Moka/256x256@2x/apps/qr-creator.png "Open QR code" "QR code contained non-url: '$URL'"
fi
else
notify-send --icon /usr/share/icons/Moka/256x256@2x/apps/qr-creator.png "Open QR code" "Did not find a QR code in the image"
fi
if [ $CAPTURE = 1 ]; then
echo "" | xclip -in -selection clipboard
fi
else
notify-send --icon /usr/share/icons/Moka/256x256@2x/apps/qr-creator.png "Open QR code" "No image/png in clipboard"
fi
🕘︎ - 2025-06-09
Recently I was editing a Markdown file and I was converting it to HTML with pandoc for every edit.
Re-running the command quickly became tedious - I could use some kind of framework for editing Markdown or something elaborate, but what I really wanted was just "run a command when a file changes".
So, watchfile
was born - it basically just wraps inotifywait
from the inotify-tools package:
#!/bin/sh
# watchfile FILE COMMAND - run a command whenever a file changes.
FILE=$1
shift
while inotifywait --quiet --quiet "$FILE"; do
echo -n "Running '$*' ... "
"$@"
echo "done."
done
🕣︎ - 2025-06-08
One of the small, handy shell scripts I use all the time is called dl
. If called with arguments, URLs, it will try downloading each argument using yt-dlp
.
That's pretty mundane, basically just easier to spell. But here is the twist: if called without arguments, dl
examines the clipboard, looking for a URL, and downloads it if found, then the selection.
So often I will see a link to a video, copy the address and then open a shell, type dl
and away we go - skipping the "paste the URl"-step.
Small improvement, but very handy.
P.S. It also retries.
#!/bin/bash
# dl - wrapper to download with yt-dlp from clipboard to ~/video.
#
# Copyright (C) 2020-2025, by Adam Sjøgren Under the GPLv2.
set -o pipefail
cd ~/video || exit 10
MAX_RETRIES=5
ARGS=( "$@" )
if [ "$*" == "" ]; then
ARGS[0]=$(xsel --output --clipboard)
if [[ ${ARGS[0]} != http* ]]; then
# If not the clipboard, maybe the mouse:
ARGS[0]=$(xsel --output)
echo "dl: clipboard is empty, using string from the mouse"
else
# Clear clipboard if we got a URL from it, so we are ready for
# the next, possibly from the mouse:
echo "dl: using string from the clipboard"
# This doesn't work, at least not with Emacs:
# xsel --clear --clipboard
# so do this instead:
echo -n "" | xsel --input --clipboard
fi
fi
i=0
for url in "${ARGS[@]}"; do
if [[ "$url" != http* ]]; then
echo "'$url' doesn't look like a URL"
exit 10
fi
url=$(echo "$url" | sed 's/drtv\/episode\//drtv\/se\//')
url=$(echo "$url" | sed 's/drtv\/program\//drtv\/se\//')
if [ $i -gt 0 ]; then
echo ""
fi
FILE=$url
LAST_ERROR=1
TRY=1
echo "Downloading $url ..."
while ((LAST_ERROR != 0 && TRY < MAX_RETRIES)); do
if [ $TRY -gt 1 ]; then
echo "Trying again ($LAST_ERROR, $TRY)"
fi
yt-dlp --force-ipv4 --color always --print 'before_dl:[36m%(title)s[0m' --print-to-file before_dl:title "/tmp/dl-out-$$.txt" "$url"
LAST_ERROR=$?
FILE=$(cat /tmp/dl-out-$$.txt)
rm /tmp/dl-out-$$.txt
TRY=$((TRY + 1))
done
if [ $LAST_ERROR != 0 ]; then
echo "Failed dl $url"
else
TITLE=$(ffprobe "$FILE" 2>&1 | grep ' title ' | cut -d: -f2-)
if [ "$TITLE" == "" ]; then
TITLE="$FILE"
fi
if [ "$TITLE" == "" ]; then
TITLE="$url"
fi
notify-send --icon /usr/share/pixmaps/terminal-tango.svg --expire-time 2000 'dl' "${TITLE} downloaded"
echo "Done."
fi
i=$((i+1))
done
🕒︎ - 2025-05-29
A couple of days ago, LLM generated text was discussed on The Unix Heritage Society mailing list, and this exchange caught my eye:
an LLM is pretty much just a much-fancier and better-automated descendant of Mark V Shaney: https://en.wikipedia.org/wiki/Mark_V._Shaney
I am glad someone has finally pointed that out.
I followed the Wikipedia link and learned that "Mark V. Shaney" was the name used to post markov chain generated usenet articles, made at Bell Labs back in the 1980's.
I wasn't on usenet in 1984, but I do have an archive, so let me look up the Mark V. Shaney articles I can find:
The Wikipedia entry has this information, under Examples:
Other quotations from Mark's Usenet posts are:[3]
- "I spent an interesting evening recently with a grain of salt." (Alternatively reported as "While at a conference a few weeks back, I spent an interesting evening with a grain of salt."[4][5])
- "I hope that there are sour apples in every bushel."[6][7] (see also sour grapes)
Interestingly we can solve the "alternatively reported" issue by going to the source, which is the very first article in the archive, 2980@alice.UUCP, where it says:
When I meet someone on a professional basis, I want them to shave their
arms. While at a conference a few weeks back, I spent an interesting
evening with a grain of salt. I wouldn't take them seriously! This
brings me back to the brash people who dare others to do so or not. I
love a good flame argument, probably more than anyone...
(my emphasis).
The second, sour apples quote from the Wikipedia entry is from the article 3062@alice.UUCP, where it is the closing line.
The (almost) full article shown in the Wikipedia Examples section is 3112@alice.UUCP; it looks like Wikipedia is missing the quip below the sign off, "Never attribute to malice what can be found in scientific american, under computer recreations."
The program behind Mark V. Shaney used a third order markov chain, so it makes statistics on triplets of words, and generates the next work by looking two words back. Reading the usenet articles, I want to revisit my little "politisnak" project, which is a basic markov chain looking at tuples of words, and expand it.
Sometimes it's fun to sit on top of a trove of old usenet articles!
Rob Pike added to the exchange on the TUHS mailing list, quoted at the beginning:
My name is Rob Pike and I approve this message.
:-)
🕛︎ - 2025-05-17
A week or so ago I upgraded my laptop to the upcoming Debian 13 (trixie). I was hoping that it would fix VLC not showing any pictures for some videos (it didn't help, the fix was to change "Hardware-accelerated decoding" from "VDPAU video decoder" to "VA-API video decoder").
The only problem I have had so far is GnuPG, which has changed behaviour, so now it tries all private keys when decrypting eg my .authinfo.gpg
- prompting for cards and what have you.
It also broke the integration with Emacs, which is annoying as my password manager is simply a gpg-encrypted text file.
Downgrading to the gnupg package in Debian 12 (bookworm) helped, so that's my solution so far.
🕤︎ - 2025-05-16

A couple of years ago I started making an ActivityPub server to make it easy for me to access the fediverse over nntp.
One of the challenges - at least in the way I built my server - is that every server it talks to speaks a slightly different dialect of ActivityPub. Mastodon generates JSON with one structure, Pleroma might include a field that others don't, GotoSocial might leave one out, etc. etc.
Around a month ago Klaus lured me into trying the Bluesky↔Fediverse bridge Bridgy Fed. TechCrunch had a nice introduction on how to bridge your account, which I followed.
And then I had to adjust my ActivityPub server to the quirks of the bridge. Usually it's stuff like "this field I have only ever encountered having a string as the value, but now it's a dictionary, or a list", and it takes me a while to handle all of them.
Had I implemented the spec rather than just building something and trying it against other servers, I would probably have fewer of these surprises. But, hey, I was (and still am) building Illuminant for fun, so I was happy to implement just enough to interact.
One thing that in hindsight is sort of expected and also sort of a downer is that boosts and parent blueskidoos only show up if the sender's account is bridged. So rather than being a useful expanse of the fediverse, this bridging turns out - to me, anyway - to be more of a curiosity than anything else.
🕧︎ - 2025-04-18

From time to time I add domains to a configuration file, header_checks
, for my mail server, Postfix, to block them as spam.
To do this, I copy the email-address from a spam email, paste it into the configuration file, and modify it to escape the .
and remove up until the @
.
Doing that for a lot of spam is of course tedious, and so I usually create a macro in Emacs to do the pasting and massaging.
However, creating that macro is a bit tedious and that makes me not do it for the first couple of spams. Until I get too annoyed. So it keeps me from updating the checks. Not great.
Emacs is capable of generating the lisp code corresponding to a macro, so I thought "Hah, I will save the macro in a comment in the file, then I can evaluate it when I'm updating the file, and I don't have to record the macro every time!"
Alas, the command M-x insert-kdb-macro RET
inserted some code that gave an error when evaluated - how odd?!
At first I assumed I was doing something wrong - the documentation only mentions saving a named macro and I was saving the last, unnamed one - but after looking at the lisp code of the function insert-kbd-macro
(Emacs makes this easy), it was clear to me that it was supposed to work: it contains specific code to handle the last macro case.
I also tried an earlier version of Emacs (I usually run a development version), the one in Debian stable - version 28.2, and there the code generated was different, and, even more interestingly, worked!
So, I created a bug report for Emacs (#77317) and was swiftly told by one of the current Emacs maintainers that the documentation only says it works for named macros.
I argued that the function specifically has a branch for the last (and unnamed) keyboard macro, and that the functionality was broken in 2022, and Cc'ed the previous Emacs maintainer who made the breaking change (which was an improvement to the other branch of the function).
He asked for an explanation of my use case, and after I supplied that, he made a change to fix the problem, which I tested swiftly; it worked, and pushed it to the development version of Emacs a couple of days later.
Nice!
So, now I could easily generate the code to restore the macro and put it in a comment in the file.
Shortly after it hit me: Emacs can evaluate code from a file when loading it, I could have it restore the macro automatically on opening the file!
A quick 3 lines at the bottom of the file later:
# Local Variables:
# eval: (setq last-kbd-macro (kmacro--keys (kmacro "| C-y M-<left> <left> [ <right> ] C-r @ <right> M-<backspace> C-s ) <left>")))
# End:
and now, more than ever, Bob is indeed my uncle!