A Weird Imagination

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…

Formulas in Gnumeric

Posted in

The problem#

I mostly use Gnumeric for my personal spreadsheets, which are generally quite simple. But recently I wanted do some analysis that required writing a formula more complicated than just using the SUM() function and found Gnumeric's function documentation quite lacking in sufficiently detailed explanations and examples to be able to figure out how to use any of the more complicated functions.

The specific problem I was trying to solve was that I had some data on payments over time labeled with categories and wanted to summarize payments by category for each time period.

The solution#

The short version is that Gnumeric's formula language is nearly the same as Microsoft Excel's formula language, so there's no need to look for Gnumeric-specific help on writing formulas. There's plenty of advice online on writing formulas for Excel, and the information transfers without modification to writing formulas for Gnumeric.

For my specific problem, my layout was that each column was a time period and each row was a payee with column A being the name of the payee and column B being the category. I made entries further down in column A of the categories and wanted the cells in those rows to be the sum of the payments for the corresponding column's time period but only for the payees matching the category for the row. The function to do that is called SUMIF(), but that documentation page from Gnumeric is very unhelpful except for the line

This function is Excel compatible.

The Excel help page on SUMIF() has examples and a lot more explanation. Here's what the formula looks like in cell P50:

=SUMIF($B$2:$B$40,$A50,P$2:P$40)

where B2:B40 is the range where the categories are given for each payee (named in A2:A40), A50 contains the category being summed, and P2:P40 contains the actual values being conditionally summed (i.e., the values for the time period named in P1).

The details#

Read more…

Extracting Tametsi puzzles in the browser

Posted in

The problem#

Previously, I figured out how to extract Tametsi's puzzles, but I wanted to make something user-friendly that made use of those puzzles, so I didn't want to require people to install something or run console commands. I had also figured out how to get Java programs running in a browser, so I figured it would be straightforward to combine the two. As you may have guessed from this paragraph being in the "the problem" section, it was not. Specifically, while the Java command-line will read tametsi.exe as a JAR file, Doppio gives the error

Invalid Zip file: Central directory record has invalid signature

The solution#

As a workaround, use a different library to unzip tametsi.exe. There's no need to present it as a JAR file instead of a directory, so no need to rezip it:

var fs = BrowserFS.BFSRequire("fs");

async function unzipToDirectory(zipfile, dir) {
  const z = new zip.fs.FS();
  await z.importBlob(zipfile);

  async function extract(z, dir) {
    fs.mkdir(dir, true)
    if (z.directory) {
      const childDir = z.name
        ? `${dir}/${z.name}`
        : dir;
      for (const child of z.children) {
        await extract(child, childDir);
      }
    } else {
      fs.writeFileSync(
        `${dir}/${z.name}`,
        new buffer(await (await z.getBlob()).arrayBuffer()));
    }
  }

  await extract(z.root, dir);
}

To call that, in the uploadFile() function, replace the reader.onload with

await unzipToDirectory(f, process.cwd() + '/tametsi');

The details#

Read more…

Working around a broken ad-block block

The problem#

Slashdot recently made a change to their ad code that made the site completely fail to load for me, showing the message

Failed to load website properly since html-load.com is blocked. Please allow html-load.com

and then blaming ad blocking:

This page could not be loaded properly due to incorrect / bad filtering rule(s) of adblockers in use. Please disable all adblockers to continue using the website. (click OK if you'd like to learn more)

I could see the page loaded just fine behind those messages, so it was obviously a lie. But the page continually reloaded if I tried to dismiss those pop-ups, so the site was unusable.

(Actually, this appears to have been disabled in the time it took me to write this blog post, so I guess this is no longer needed. Although it may be applicable to other websites using the same or similar mechanisms.)

The solution#

Install this user script. I have only tested it with Greasemonkey on Firefox so it require modification to work on other user script managers or browsers.

The details#

Read more…

React to reacting to Discord join message

Posted in

The problem#

I run a Discord community for a local hobby-focused friends group. Since it's theoretically people I know in person, I don't want it to be an open invite; I want confirmation that everyone that joins has a real in-person connection to the group. I handle by gating access to most of the channels behind a role that I grant to new users once I've identified them. As the group has grown, I am often not directly connected to everyone joining, so I wanted an easy way to grant trusted users the ability to do the same. The straightforward way is to just use Discord permissions to let other people assign roles, but I wanted something smoother and easier for non-technical users.

The solution#

YAGPDB ("Yet Another General Purpose Discord Bot") is an extremely flexible and configurable Discord bot that can be set up to do pretty much anything. For this purpose, we're deep in its customization features which include a programming language for running custom scripts in response to various triggers. Since people in the community tend to 👋 react to the join message Discord generates when someone they know joins, I wanted to specifically make that the signal that a user is a real person. Here's a YAGPDB custom command that will do so when set up to be triggered by adding a reaction and configured with the desired allowed roles and filling in the configurable values at the top of the script:

{{/*
    Grant role to user when trusted user
    reacts to their join message.
    By Daniel Perelman <https://github.com/dperelman/>

    Loosely based on
    <https://yagpdb-cc.github.io/utilities/reaction-logs>
    by Satty9361 <https://github.com/Satty9361>
*/}}

{{/* Configurable values */}}
{{$logging_channel_id := }}
{{$roleName := }}
{{$adminUserId := }}
{{/* End of configurable values */}}

{{/* Actual CODE */}}
{{ if (and
        (eq .ReactionMessage.Type 7)
        (eq .Reaction.Emoji.APIName "👋")) }}
    {{$newUser := .ReactionMessage.Author}}
    {{$role := getRole $roleName}}
    {{$adminUserLink := (print "[" (userArg $adminUserId) "]"
        "(<https://discord.com/users/" $adminUserId ">)")}}
    {{$userLink := (print "[" .User "]"
        "(<https://discord.com/users/" .User.ID ">)")}}
    {{$newUserLink := (print "[" $newUser "]"
        "(<https://discord.com/users/" $newUser.ID ">)")}}
    {{ if targetHasRoleID $newUser.ID $role.ID }}
        {{/* sendMessage $logging_channel_id (print
            "DEBUG: " $userLink " would have granted `"
            $role.Name "` to " $newUserLink
            " but they already have that role.") */}}
    {{ else }}
        {{ giveRoleID $newUser.ID $role.ID }}
        {{ sendMessage $logging_channel_id (print
            $userLink " granted `" $role.Name "` to
            " $newUserLink " by reacting :wave: to their "
            "join message.") }}
        {{ sendDM (print
            "You have granted " $newUserLink " the role `"
            $role.Name "` by :wave: reacting to their "
            "join message. If this was in error, "
            "please contact " $adminUserLink ".") }}
    {{ end }}
{{ end }}

The details#

Read more…

Running Java in JavaScript

Posted in

The problem#

Java and JavaScript are insufficiently easily confused. I had some code I wanted to run on the user's computer and not the server, and it specifically had to be in Java to reference a library written in Java. I found1 Doppio, a JVM written in TypeScript, but it had bitrotted enough that I was having trouble getting it to run, despite confirming it did what I wanted using the official demo, which provides an in-browser console-like interface.

The solution#

I was able to piece together a working simple page using Doppio, modified from the official example. It's in a repository on GitHub and should be straightforward to modify for your needs.

The details#

Read more…

Extracting Tametsi puzzles

The problem#

Tametsi (available on Steam1) is a great logic puzzle game that is a collection of Minesweeper puzzles that can be solved without guessing. The game consists of 100 puzzles plus 60 "bonus" puzzles. The bonus puzzles are in the game directory in an XML-based format that another player has documented well enough that they have even created some puzzles of their own and a viewer for those files. But the base 100 puzzles are nowhere to be found in the puzzle directory, and I had ideas for doing something with them.

The solution#

Given the file DumpPuzzles.java:

public class DumpPuzzles {
    public static void main(String[] args) {
        // Loading puzzles sets the graph on
        //  Game's MouseHandler, so it has to exist.
        game.Game.mh = new io.MouseHandler(null);

        for (int i = 1; i <= 111; i++) {
            puzzle.PuzzleOut.writePuzzle(
                new puzzle.Puzzle(i),
                String.format("puzzle_%03d.puz", i));
        }
    }
}

put it in the same directory as tametsi.exe2 and run

$ javac -classpath tametsi.exe DumpPuzzles.java 
$ java -classpath tametsi.exe:. DumpPuzzles

Then the puzzles/ directory will be full of files named puzzle_001.puz, etc.

The details#

Read more…