Archive of

Aa!! It's a zorting zombie!

I came across an interesting Bash issue today, as I was trying to restore a zstd-compressed CloneZilla Partclone image to a raw file in order to extract some data from it. For some reason, none of the solutions on the internet worked, and searching for the error message turned up no useful results. This was the command line I had constructed:

$ zstdcat nvme0n1p3.ntfs-ptcl-img.zst.* | sudo partclone.ntfs -C -r -W -s - -O image.img

Notice the glob at the end of the only argument to zstdcat. This only gave me:

Partclone v0.3.17 http://partclone.org
Starting to restore image (-) to device (image.img)
This is not partclone image.
Partclone fail, please check /var/log/partclone.log !

partclone kept saying This is not partclone image no matter what I did. I did some sanity checking with the commands:

$ ls nvme0n1p3.ntfs-ptcl-img.zst.*

and also

$ ls nvme0n1p3.ntfs-ptcl-img.zst.a[a-x]

Both returned the following list:

nvme0n1p3.ntfs-ptcl-img.zst.ab  nvme0n1p3.ntfs-ptcl-img.zst.ah  nvme0n1p3.ntfs-ptcl-img.zst.an  nvme0n1p3.ntfs-ptcl-img.zst.at
nvme0n1p3.ntfs-ptcl-img.zst.ac  nvme0n1p3.ntfs-ptcl-img.zst.ai  nvme0n1p3.ntfs-ptcl-img.zst.ao  nvme0n1p3.ntfs-ptcl-img.zst.au
nvme0n1p3.ntfs-ptcl-img.zst.ad  nvme0n1p3.ntfs-ptcl-img.zst.aj  nvme0n1p3.ntfs-ptcl-img.zst.ap  nvme0n1p3.ntfs-ptcl-img.zst.av
nvme0n1p3.ntfs-ptcl-img.zst.ae  nvme0n1p3.ntfs-ptcl-img.zst.ak  nvme0n1p3.ntfs-ptcl-img.zst.aq  nvme0n1p3.ntfs-ptcl-img.zst.aw
nvme0n1p3.ntfs-ptcl-img.zst.af  nvme0n1p3.ntfs-ptcl-img.zst.al  nvme0n1p3.ntfs-ptcl-img.zst.ar  nvme0n1p3.ntfs-ptcl-img.zst.ax
nvme0n1p3.ntfs-ptcl-img.zst.ag  nvme0n1p3.ntfs-ptcl-img.zst.am  nvme0n1p3.ntfs-ptcl-img.zst.as  nvme0n1p3.ntfs-ptcl-img.zst.aa

Wait... What? Notice how ab is the first and aa is the last substitution. Obviously, aa needs to be the first substitution! They need to be zstdcated in order for partclone to recognize the file, as I assume there's a file signature/magic bytes at the start of the raw file contained within the archives.

My question was, why is my alphabetization seemingly broken? According to a web search, Bash globs will always return an ordered/alphabetized list. I was able to start the restore process by manually entering the list of files in order instead of globbing, but curiosity got the better of me and I had an inkling this was related to my system locale.

SPOILERS: It was.

$ locale
LANG=en_US.UTF-8
LANGUAGE=
LC_CTYPE="en_US.UTF-8"
LC_NUMERIC=nb_NO.UTF-8
LC_TIME=en_GB.UTF-8
LC_COLLATE=nb_NO.UTF-8
LC_MONETARY=nb_NO.UTF-8
LC_MESSAGES="en_US.UTF-8"
LC_PAPER=nb_NO.UTF-8
LC_NAME=nb_NO.UTF-8
LC_ADDRESS=nb_NO.UTF-8
LC_TELEPHONE=nb_NO.UTF-8
LC_MEASUREMENT=nb_NO.UTF-8
LC_IDENTIFICATION=nb_NO.UTF-8
LC_ALL=

Observe how LC_COLLATE=nb_NO.UTF-8. I have my system language set to English, but most other locale settings set to Norwegian. In Norwegian, Aa/aa is a common substitution for Å/å, the last character of the Norwegian alphabet. The sorting algorithm, in its infinite wisdom, seems to have decided that a file extension of *.aashould be sorted at the very end because of this, which breaks the argument list.

To fix this, I set LC_COLLATE to C by issuing:

$ sudo localectl set-locale LC_COLLATE=C

This worked for about three seconds before KDE decided that my opinion is wrong, and promptly overwrote it, resurrecting nb_NO.UTF-8 like a digital zombie.

In KDE's System Settings > Region & Language section, there are a few locale related settings, but nothing about sorting. Presumably, it uses one of the other fields to assume the value of LC_COLLATE, and you know what they say about assuming.

To actually fix it, I added export LC_COLLATE="C" to my ~/.bashrc, which seems to work, and persists between terminal sessions.


Fixing macOS' defective sleep mode with a Hammer (...Spoon)

This is kind of a followup post to Using a Mac Mini as a bedroom PC for Kodi, Moonlight and YouTube.

It's been nearly a year since I set up a Mac Mini as my one-stop-shop for all multimedia related tasks in the bedroom. It's generally worked well, and both streaming media, games, general web browsing and PC usage has been smooth. You're rolling around in bed, playing video games, watching YouTube, or maybe the freshest episode of Smiling Friends. But then you fall asleep, and the fun abruptly stops.

"I'm gonna wreck it!" - Apple, probably

Sometimes, the computer will still be on and fully active in the morning after falling asleep using it, soaking up all those precious jiggawatts, directly out of your wallet.

Why, I hear you ask? Maybe there is a mouse connected that causes jiggle when you shuffle around in your sleep?

No, only a USB remote and a keyboard + touchpad combo device (the Logitech K400 Plus, which is absolute fucking garbage[1]) are connected.

How can this be, when the system is set to sleep after 1 hour of inactivity? Surely, Thou Be Trippin'?

Well, as many things Apple, it is a defect by design: Apple lets software overrule this user setting, without notifying the user, and without letting the user change this behaviour. This means that since EmulationStation - the launcher I'm using - is running in the background, and is considered a game, it inhibits system sleep. I've looked all over, and there is no way in current-day macOS to let me, the user, owner, administrator and fucking Dom Top of this machine, ignore what the system thinks, and just Go The Fuck To Sleep after 1 hour, no matter what. That's a huge defect, but dealing with huge defects is the bread and butter of a technical person trying to make an Apple product cooperate.

"I'm gonna fix it!" - Me

I'm not gonna spin a yarn and complain any more about Apple today, even if I could fill pages with that kind of content. I'm a Solutions™ kinda guy, not an Apple Sux™ kinda guy.

Here's how to fix it: Using HammerSpoon, an application for automating macOS, along with a simple Lua script. I have never used HammerSpoon before this, but I've written a ton of Lua (for video games), so getting started was easy enough.

Install HammerSpoon, start it, give it the appropriate permissions, set it to run at boot, and edit your ~/.hammerspoon/init.lua to contain the following:

local function handlePowerEvent(event)
    if event == powerWatcher.systemDidWake or event == powerWatcher.screensDidWake then
      print("System woke up, restarting idle monitoring")
      idleTimer:start()
    elseif event == powerWatcher.systemWillSleep or event == powerWatcher.screensDidSleep then
      print("System going to sleep, suspending monitoring.")
      idleTimer:stop()
    end
end

local function checkSleep()
    local idleTime = hs.host.idleTime()
    if idleTime < sleepThreshold then
        print("Idle for " .. idleTime .. " secs")
    else
        print("Idle time exceeded threshold, going to sleep")
        hs.caffeinate.systemSleep()
    end
end

sleepThreshold = 3600 --seconds
powerWatcher = hs.caffeinate.watcher
powerWatcher.new(handlePowerEvent):start()
idleTimer = hs.timer.new(60, checkSleep)
idleTimer:start()

Save the file, click the HammerSpoon icon on your menu bar, then "Reload Config", and boom, you're in business.

The script is extremely simple. It sets a timer that monitors how long the system has been idle for, which runs every 60 seconds. If the timer sees that the idle time is over the set threshold (set to 3600 seconds here - 1 hour), it tells the computer to go to sleep immediately — come hell or high water. The script additionally monitors the sleep and wake up events, in order to stop and start the timer, so it doesn't run while the computer is asleep (yeah, that can happen). It also prints some log messages to the HammerSpoon console, which you can see by clicking the HammerSpoon icon and clicking "Console".

And that's all there is to it. Apple has a long way to go when it comes to user friendliness, and definitely needs to provide an option for "going to sleep no matter what the system or any piece of software has to say about the present state of things". I'm not going to be in charge of wording the toggle, though.


[1] The Logitech K400 Plus, also known as The Frustrationator 400, is e-waste that they charge money for. The touch pad comes with an infuriating acceleration that can't be turned off, which makes navigating the user interface of the computer an exercise that all but ensures that you go to bed angry. They told me I shouldn't do that, but here we are. It also looks dumb, feels cheap, and makes me nostalgic for the simpler times when I didn't own the Logitech K400 Plus.


Bash script: Reminder to swap mics with automatic pausing of all media players

The problem

I recently purchased a double pack of wireless microphones (specifically, these ones) to replace my ageing and faltering wired one. I am very happy with the audio quality, their ease of use and their range, but for my specific use case, the battery life (around 3 hours) leaves a little to be desired. I mainly use these while hanging out with a friend over the internet while sharing my screen, and we'll watch movies and TV shows together. That can last a couple of hours or even most of the day, and at some point the battery for the microphone will run out.

The first time this happened, it took me a little while to understand what was going on, as there was no beep or any indication from the microphone to signal its demise. Apparently the LED on the device will blink when it's low on battery, but that's impossible to see when it's clipped to my shirt right underneath my chin.

But, through the magic of buying two of them, the solution is easy — swap the depleted mic with a freshly charged one, then recharge the depleted mic while you discharge the fresh one. Still, the problem of not knowing when to do that persists.

The code

That's right, we're writing more Bash. Normally, you could just set a timer for your phone or your computer, but we're watching content that has sound whilst wearing headphones. What I wanted was a solution that could pause all playing media and tell me to swap my mic out, so that's exactly what I wrote:

mic-change.sh:

#!/usr/bin/env bash
matches=$(playerctl -l)
IFS=$'\n'
read -r -d '' -a matchedPlayers <<< "$matches"
numPlayers=${#matchedPlayers[@]}
((numPlayers--)) # To use zero based indexing

for i in $(seq 0 "$numPlayers"); do
    currentPlayer=${matchedPlayers["$i"]}
    status=$(playerctl -p "$currentPlayer" status)
   
    if [[ "$status" == "Playing" ]]; then
        playerctl -p "$currentPlayer" play-pause
        
        if [[ "$currentPlayer" == "kodi" ]]; then
            sleep 1
        fi
    fi
done

mplayer "/opt/sfx/mic-change.mp3"

How to use

Run the script when you turn on your microphone, and re-run it whenever you swap your mic. Examples:

  • Run a sleep then the script from the terminal:
    sleep 9000; /opt/scripts/mic-change.sh
  • Put the sleep at the start of the script (after the shebang) and run it
  • Use a timer app that can launch scripts when the timer finishes, like KClock

(9000 seconds is 2h 30m)

Script explanation

This script uses playerctl to pause all currently playing media players via the MPRIS D-Bus specification. Most players for Linux support this natively. Kodi, my media player of choice, does not, but support can easily be added through an addon. The script goes through every currently registered media player, and checks if it is currently playing. If it is, it pauses it. If it is Kodi, it waits a second before doing anything else, since Kodi has a ˜1 sec delay when pausing before audio stops playing (and also an annoying corresponding ˜1 sec delay before audio starts playing again once you unpause). Finally, it plays the sound defined in the last line of the script using mplayer, which in my case is a TTS voice named Onyx telling me to "Change your mic, motherfucker."

If you'd like to get the same microphones, which are very good despite the relatively short battery life, you can get them on AliExpress here.

The product links in this article are affiliate links. If you buy something using them, I may earn a small commission at no extra cost to you.