A Weird Imagination

Linting Markdown reference-style links

The problem#

When writing blog posts, I like to use Markdown's reference-style links which let you avoid writing URLs inline and instead provide a short name and define it elsewhere in the document. I always put them at the end, which results in the bottom of the Markdown file looking like a bibliography for the post. But then there's the extra task of making sure the references at the bottom of the post are consistent with their usage in the blog post; this isn't a huge problem as usually I add a link and immediately use it, looking at the preview to make sure it got used properly. But sometimes I'll start a post by entering a list of links I expect to use, and sometimes I'll miss something checking the preview.

The solution#

lint_refs.py takes any number of Markdown files as arguments and prints the references that are invalid or not used:

#!/usr/bin/env python

import sys
from pathlib import Path
from markdown import markdown, extensions, postprocessors


class ReferenceProxy(dict):
    def __init__(self, *arg, **kw):
        super(ReferenceProxy, self).__init__(*arg, **kw)
        self.read = set()

    def __contains__(self, key):
        self.read.add(key)
        return super().__contains__(key)


class ReferenceLintExtension(extensions.Extension,
                             postprocessors.Postprocessor):
    def __init__(self, filename):
        self.filename = filename

    def extendMarkdown(self, md):
        self.refs = md.references = \
                ReferenceProxy(**md.references)
        md.postprocessors.register(self, 'ref_lint', 1)

    def run(self, text):
        undefined = self.refs.read - set(self.refs.keys())
        unused = set(self.refs.keys()) - self.refs.read
        if unused or undefined:
            print(f"\n\n# {self.filename}")
            if undefined:
                print("\n## UNDEFINED REFERENCES")
                print('\n'.join(sorted(undefined)))
            if unused:
                print("\n## UNUSED REFERENCES")
                print('\n'.join(sorted(unused)))
        return text


for filename in sys.argv[1:]:
    markdown(Path(filename).read_text(), extensions=[
        ReferenceLintExtension(filename),
        'markdown.extensions.extra'])

Given example.md:

A [broken link][broken]. A [working link][working].

[working]: https://example.com/
[not-used]: https://example.org/
$ ./lint-refs.py example.md 


# example.md

## UNDEFINED REFERENCES
broken
broken link

## UNUSED REFERENCES
not-used

The details#

Read more…

Interpreting keypresses in shell

The problem#

Last time, I presented a script read_keypress.sh which would read any single keypress in a shell script. For printable characters, it's straightforward what this means, but for keys like left arrow (🠈), it's not obvious how to deal with them.

The solution#

keytest.sh demonstrates using read_keypress.sh to respond to keypresses including 🠈 and Ctrl+c:

#!/bin/bash

esc=$(printf '\e')
ctrlc=$(printf '\x03')

while true
do
  key="$(./read_keypress.sh)"
  case $key in
    q|"$ctrlc")
      echo "Quitting..."
      exit
      ;;
    "${esc}[D")
      echo "Moved left."
      ;;
    *)
      echo "Unexpected key: $key ($(echo -n "$key" | xxd))"
      ;;
  esac
done

The details#

Read more…

Handling keypresses in shell

The problem#

For certain applications it can be useful to get many quick responses from the computer, so we would like it to react to individual keypresses instead of requiring an entire command to be typed out and confirmed by pressing Enter. While this is a common feature of GUI and TUI toolkits (e.g., ncurses), it can also be useful for very lightweight inactive experiences written as simple shell scripts.

The solution#

read_keypress.sh will wait for the next keypress and return it as a string:

#!/bin/sh

STTY=$(stty --save)
stty raw -echo
SEL=$(dd if=/dev/tty bs=1k count=1 2>/dev/null)
stty "$STTY"
echo -n "$SEL"

The details#

Read more…

Thunar custom actions

The problem#

While I use the command-line and keyboard-based interfaces a lot to manage files, I also regularly use graphical and mouse-based interfaces. As with any time there's different ways of interacting with a system, there's some times when in one mode when you want the features of the other.

The solution#

Xfce's file manager Thunar has a feature called custom actions, which lets you add menu items that run commands on files or directories you are viewing or selecting.

The documentation suggests multiple ideas of actions you might want, including converting among different formants, rotating images, and various file management actions like viewing disk usage and bulk moving files. Additionally, the default action "Open Terminal Here" is one I use frequently.

The details#

Read more…

Playing Android motion photos

The problem#

My Android phone's camera app has an option to take a "motion photo", which, similar to the iOS "live photo" feature, records a short, silent video along with the photo. When viewing the photos on the phone, there's an option to play the video. But image viewer programs on my computer like Eye of GNOME (eog) do not support playing them.

The solution#

ExifTool (in Debian, the package name is libimage-exiftool-perl) can extract the video to a file using this example from a forum post:

exiftool -b -EmbeddedVideoFile photo.jpg > photo_motion.mp4

You can also play it directly without saving it to a file by piping to VLC:

exiftool -b -EmbeddedVideoFile photo.jpg | vlc -

The details#

Read more…

Viewing Signal messenger backups

Posted in

The problem#

Signal is a privacy-focused instant messaging application which ensures end-to-end encryption on all messages and generally goes out of its way to avoid accidentally revealing your conversations to third parties. As is common with security mechanisms, this necessarily adds some friction some some tasks you actually want to do. In particular, this means viewing your message history outside of the official app is unsupported. Which is especially a problem due to the app being tied to a smartphone that will likely only continue being usable for at most several years. So there's no official way to maintain access to your conversations indefinitely in contrast to IRC and Pidgin logs I have going back decades.

The solution#

The Signal Andriod app supports making encrypted backups (there is no way to get messages out of Signal iOS).

When you enable backups, it gives you a "passphrase" (a sequence of 30 digits). If you have lost this passphrase, there is no way to recover it, but you can disable backups and then re-enable them which will generate a fresh passphrase that will be used for your backups going forward. I recommend storing this passphrase in a password manager like KeePassXC.

To actually use the backup file, you'll need to know where it is. The location is shown on the Signal settings under Chats -> Chat backups -> Backup folder. You can copy it to your computer by plugging the phone in with a USB cable and using an MTP client, which is included in most file managers (I use XFCE's Thunar).

Once you have the backup file and passphrase, you can use signalbackup-tools to convert the backup to something human-readable:

signalbackup-tools [input] [passphrase] \
                   --exporthtml [directory] --allhtmlpages

The output does a good job of trying to look like the Signal interface. See the documentation for more options on controlling the output including various options to filter the output to just some conversations or time ranges.

The details#

Read more…

Move window to current desktop

The problem#

Previously, I wrote a script for opening and immediately focusing xfce4-appfinder. But xfce4-appfinder will notice if it's already open and just assume you want to use the existing window. Even if it's on a different desktop. And therefore attempting to focus it will either do nothing or switch to that desktop, neither of which is desirable.

The solution#

The actual solution I settled on is more of a workaround than a solution: having xfce4-appfinder on a different desktop doesn't really make sense, so I just set it to be on all desktops (specified as the non-existent desktop -1), so it would never be on the wrong desktop:

xdotool set_desktop_for_window "$winid" -1

To actually move the window to the current desktop, replace xdotool windowactivate "$winid" with

desktop="$(xdotool get_desktop)"
win_desktop="$(xdotool get_desktop_for_window "$winid")"
if [[ "$desktop" == "$win_desktop" ]]
then
  xdotool windowactivate "$winid"
else
  xdotool set_desktop_for_window "$winid" -1
  xdotool set_desktop_for_window "$winid" "$desktop" \
          windowactivate "$winid"
fi

Although this assumes that you know the $winid of the window you want to move. If you have just the title it works just as well to use

wmctrl -R "Application Finder"

The details#

Read more…

Tracking household tasks

Posted in

The problem#

The new year is a traditional time for adopting new organizational schemes, among other oft-broken promises to oneself of improved habits. In that vein, I recently adopted a new system for managing my TODO list.

Managing a household involves a lot of infrequent tasks that are easy to forget like checking filters on various appliances every few months and similar invisible maintenance tasks. I had been managing such tasks using recurring Google Calendar events with email reminders, but it was getting unwieldy for multiple reasons. It didn't provide a good record of whether and when tasks were completed (which matters for tasks that should be done some number of weeks or months since the last time it was completed, not since the last time I was reminded of it). Additionally, it doesn't provide a good way to share the TODOs with other members of the household who are also responsible for some of those tasks. And it also cluttered up my calendar with items that didn't really have a meaningful assignment to a particular day or time.

The solution#

Task management systems are very personal: while I will describe what I came up with that hopefully I will continue to find useful, what works for you may be very different.

I set up a TODO list for my household (plus a separate one just for myself) using Sleek which uses the todo.txt format (see todotxt.org for more info and other software). The directory containing the TODO file is shared with the rest of the household with Syncthing. For backups, the directory is a ZFS dataset, so it is automatically snapshotted regularly and included in my backups. If you wanted, you could also apply my copy on save logic to snapshot every change, but that's likely overkill.

Example tasks#

While the simplicity of the todo.txt format means it's easy to edit by hand or use any tool including ones you write yourself, the Sleek GUI handles the syntax for you so it is accessible to non-technical users as well.

Sleek supports "threshold" dates before which tasks are hidden from view by default and recurring tasks which can be "strict" (based on due date, prefixed with +) or not (based on completion date), which allows the specification of tasks like "check funance filter 2-3 months since the last time it was checked":

rec:3m t:2025-02-15 due:2025-03-15 check furnace @filter

as well as "pay the electric bill between the 20th and the end of each month":

rec:+1m t:2024-12-20 due:2025-12-30 pay electric @bill

The thresholds allow for keeping the noise down on the list by hiding tasks that cannot be done yet (can't pay a bill that hasn't arrived yet) or don't make sense to do so soon after they were last done.

The details#

Read more…

Killing pipes from within

The problem#

Last week, I mentioned that I needed a hack to kill xprop that seemed like it should be unnecessary. Specifically, I had its output piped to a Bash while read loop and once that had found a line to act on, there was no further need to get more lines from xprop, but break or exit didn't result in xprop exiting.

The solution#

Use $BASHPID to get the actual PID of the subshell, ps to climb the process tree to the appropriate parent and pkill to kill its children:

some_pipeline |
  while read -r line
  do
    # Do whatever until ready to kill the pipe...
    # ... then kill it:
    ppid=$BASHPID
    ppid=$(ps -o ppid:1= "$ppid")
    pkill -9 -P "$ppid" 
  done

Rewrite appfinder-and-focus.sh from last time:

#!/usr/bin/bash
xprop -spy -root _NET_CLIENT_LIST | stdbuf -oL head -2 |
  while read -r l
  do
    winid="${l/#*, /}"
    if [[ $(xdotool getwindowname "$winid") == \
      "Application Finder" ]]
    then
      xdotool windowactivate "$winid"
      ppid=$BASHPID
      ppid=$(ps -o ppid:1= "$ppid")
      pkill -9 -P "$ppid" 
    fi
  done) 2>/dev/null &
xfce4-appfinder &

The details#

Read more…

Force focus new window immediately

The problem#

I have my window manager set to not focus new windows because I dislike having a new window pop up while typing and having the keystrokes surprisingly sent to the new window instead of the one I thought I was typing in. While this is usually what I want, this does mean extra clicks when I did mean to open the new window.

This is particularly bad for xfce4-appfinder (or any other application launcher), since the purpose to be able to set a global keyboard shortcut like Super+Space so you can press that combination and quickly type in the application or action you want (or, even better, type just the first few characters of its name). And since it's being intentionally launched by a keyboard shortcut, there's no real concern of it grabbing keyboard focus unexpectedly.

The solution#

Put the following script in a file appfinder-and-focus.sh and set the keyboard shortcut to run it instead of just running xfce4-appfinder directly:

#!/usr/bin/bash
(xprop -spy -root _NET_CLIENT_LIST | stdbuf -oL tail -n +2 |
  while read -r line
  do
    winid="${line/#*, /}"
    if [[ $(xdotool getwindowname "$winid") == \
        "Application Finder" ]]
    then
      xdotool windowactivate "$winid"
    fi
  done) 2>/dev/null &
xfce4-appfinder &

# Wait for window to appear, then kill xprop.
xdotool search --sync --name "Application Finder" >/dev/null
pkill -P "$(jobs -p %1)"

The details#

Read more…