jcd.pub

posts by Jonathan Davies

Archive Music Mixtapes
  • Screen-shake on failed commands in the kitty terminal

    Nov 4, 2026

    The kitty terminal recently added support for custom shaders, with a nice implementation using the Slang shader language, which is sort of like a better version of GLSL which compiles to different GPU targets.

    When I saw that you could trigger them on the BEL character, I knew I had to implement the screen-shake effect that games love to use when the player has taken damage. BEL is an ASCII control code that is meant to have your terminal play a sound, and is usually ignored these days. Now I had a way to trigger arbitrary shader effects based on output from the shell though, there was no excuse not to try it.

    Config

    I configured fish to print BEL when a command failed:

    function bell_on_failure --on-event fish_postexec
        if test $status -ne 0
            printf '\a'
        end
    end
    

    Doing this in bash or zsh is left as an exercise for the reader. Maybe you can use the prompt or something.

    To make this work in kitty, beyond just registering the shader pipeline I found that kitty doesnt trigger the shader unless the visual_bell_duration option is nonzero, which also triggers its own built-in flash effect. Fortunately you can render this invisible by setting it to a very small value:

    # kitty.conf
    
    custom_shaders ~/kitty-shaders/shake.pipeline
    visual_bell_duration 0.00001
    

    Horizontal shake

    Here’s the shader shown in the above video, save this to shake.slang:

    #language slang 2026
    
    import kitty_custom_shader_types;
    static const float max_displacement = 17.5;
    
    public float4 fragment_main(float4 color, KittyTextures t,
                                KittyCustomShaderData d) {
      float4 rect = d.bell_window_geometry;
      float2 rect_end = rect.xy + rect.zw;
      if (rect.z <= 0.0 || rect.w <= 0.0 || any(t.pos < rect.xy) ||
          any(t.pos >= rect_end)) {
        return color;
      }
    
      float direction = (d.frame_counter & 1u) == 0u ? 1.0 : -1.0;
      float pixels = direction * max_displacement * (1.0 - t.animation_progress);
    
      float2 sample_pos = t.pos;
      sample_pos.x -= pixels / float(d.viewport_size_pixels.x);
    
      float4 sampled = t.backbuffer.Sample(sample_pos);
      // Undo the premultiplied alpha
      return float4(sampled.rgb / max(sampled.a, 1e-6), sampled.a);
    }
    

    This is kitty pipeline file to drive it. Note that I set animation_step to 1 which will attempt to run it on every frame, capped by repaint_delay. As this only runs for a few hundred ms, this is desired behavior. Save this to shake.pipeline in the same dir as the .slang file.

    startgroup
        animation_start bell-in-window
        animation_stop 150
        animation_curve ease-out
        animation_step 1
        shaders shake
    
        var float max_displacement 15.0
    endgroup
    

    Octave x,y shake

    This one uses octave noise to shake randomly in both axes. I prefer the first, its less distacting but of course one has to experiment with a bunch of the different well-known screenshake variations.

    #language slang 2026
    
    import kitty_custom_shader_types;
    
    static const float BASE_FREQUENCY_HZ = 5.0;
    static const float BASE_X_PIXELS = 30.0;
    static const float BASE_Y_PIXELS = 30.0;
    static const float3 OCTAVE_WEIGHTS = float3(0.03, 1.0, 2.4);
    static const float3 OCTAVE_FALLOFF_EXPONENTS = float3(4.0, 2.0, 1.0);
    
    uint hash_bits(uint value) {
        value ^= value >> 16;
        value *= 0x7feb352du;
        value ^= value >> 15;
        value *= 0x846ca68bu;
        value ^= value >> 16;
        return value;
    }
    
    float gradient(int point, uint seed) {
        uint bits = hash_bits(uint(point) ^ seed);
        return float(bits & 0xffffu) / 32767.5 - 1.0;
    }
    
    float gradient_noise(float time, uint seed) {
        float point = floor(time);
        float phase = time - point;
        float blend = phase * phase * (3.0 - 2.0 * phase);
    
        float left = gradient(int(point), seed) * phase;
        float right = gradient(int(point) + 1, seed) * (phase - 1.0);
    
        return 2.0 * lerp(left, right, blend);
    }
    
    float octave_noise(float time, uint seed, float progress) {
        float3 octaves = float3(
            gradient_noise(time, seed),
            gradient_noise(2.0 * time, seed ^ 0x9e3779b9u),
            gradient_noise(4.0 * time, seed ^ 0x85ebca6bu)
        );
        float total_weight = OCTAVE_WEIGHTS.x + OCTAVE_WEIGHTS.y + OCTAVE_WEIGHTS.z;
        float3 remaining = pow(float3(1.0 - progress), OCTAVE_FALLOFF_EXPONENTS);
    
        return dot(octaves, OCTAVE_WEIGHTS * remaining) / total_weight;
    }
    
    public float4 fragment_main(float4 color, KittyTextures t, KittyCustomShaderData d) {
        float4 rect = d.bell_window_geometry;
        float2 rect_end = rect.xy + rect.zw;
    
        if (rect.z <= 0.0 || rect.w <= 0.0 ||
            any(t.pos < rect.xy) || any(t.pos >= rect_end)) {
            return color;
        }
    
        float time = d.timestamp * BASE_FREQUENCY_HZ;
    
        float2 movement = float2(
            octave_noise(time, 0x1337133bu, t.animation_progress),
            octave_noise(time, 0x7f4a7c15u, t.animation_progress)
        );
        float2 pixels = movement * float2(BASE_X_PIXELS, BASE_Y_PIXELS);
        float2 sample_pos = t.pos - pixels / float2(d.viewport_size_pixels);
    
        float4 sampled = t.backbuffer.Sample(sample_pos);
        return float4(sampled.rgb / max(sampled.a, 1e-6), sampled.a);
    }
    

    …and heres a pipeline. Note that this one uses the linear animation_curve, and sets animation_step to 16 for a 60fps display. The reason for this is that the shader uses a different falloff curve for each octave of noise, so it needs to handle the timing inside the shader itself rather than relying on kitty’s animation curves. You can override any of the constants as shown in the previous example.

    startgroup
        animation_start bell-in-window
        animation_stop 150
        animation_curve linear
        animation_step 8
        shaders shake_octaves
    endgroup
    
  • Bypassing Bluesky Age-Assurance on a self-hosted PDS

    Oct 23, 2026

    Modern life consists mostly in refusal.

    If you’re unlucky enough to live in the United Kingdom (or apparently Texas) you’re subject to age-checks on various websites, including bluesky. This is only enforced client-side, so you can try and patch it in the browser, or if you’re self-hosting your own bluesky server, you can override the endpoint it uses to check the ageAssurance state. There are various posts about how to do this, but naturally the API changes over time. Here is a currently working caddy config:

    handle /xrpc/app.bsky.ageassurance.getState {
      vars {
        lastInitiatedAt "2026-06-24T10:12:54.275Z"
        accountCreatedAt "2026-06-23T12:56:39.174Z"
      }
    
      header Content-Type "application/json"
      header Access-Control-Allow-Headers "authorization,dpop,atproto-accept-labelers,atproto-proxy,x-atproto-device-id,x-atproto-session-id"
      header Access-Control-Allow-Origin "*"
    
      respond `{"state":{"lastInitiatedAt":"{vars.lastInitiatedAt}","status":"assured","access":"full"},"metadata":{"accountCreatedAt":"{vars.accountCreatedAt}"}}` 200
    }
    

    Here I’ve extracted the dates to variables for convenience. I’m not sure there’s actually any code that cares about their values but it’s nice for them to be plausible. You can get your correct accountCreatedAt from the public api:

    curl -fsSG 'https://public.api.bsky.app/xrpc/app.bsky.actor.getProfile' \
      --data-urlencode 'actor=USERNAME' | jq -r '.createdAt'
    

    subbing in your handle without the @. lastInitiatedAt is then supposed to be the time when you validated your age, so some time after that.

  • Disabling the Raspberry Pi 3B power LED on a mainline kernel

    Aug 18, 2026

    The raspberry pi 3B doesn’t present /sys/class/leds/PWR when you’re running the upstream kernel rather than the raspberry pi foundation’s official fork. You get /sys/class/leds/ACT which controls the green LED correctly, but no PWR entry for the red one. You’ll also see that dtparam=pwr_led_trigger=none and related commands that you are advised to add to config.txt do nothing.

    This is because the upstream device-tree simply doesn’t have an entry for it, as it’s implemented weirdly in the hardware. The fix is to define it yourself. Click here to skip to the code, or read on for an explanation.

    Read on →

  • Bit-Reversal Line Rasterization

    May 15, 2025

    On the left, smooth and stable. On the right, flickering and janky.
    Matthias with his trophy

    On the 3rd of June 2023, at the X demoparty in Someren in the southern Netherlands, quiss aka Matthias Kramm presented Boo as his entry in the C64 4K Intro category. You can watch it on YouTube here. While the demo mainly featured a couple of spinning cubes, the edges of those cubes were drawn using a new algorithm he calls “Bit-Reversal Rendering”. This brought the number of CPU cycles needed to plot each pixel of a line on a Commodore 64 down from 7 to 5, reducing the overhead on a number of key graphical effects. This earned him first place, and the adoration of the assembled demosceners.

    There are maybe half a dozen line plotting methods in existence and most of them date back to the cold war, so presenting a brand new one as a 4 kilobyte C64 intro that does parallel processing on the floppy disk controller is pretty cool.

    Of more general interest though, is the better temporal stability of this algorithm in comparison to others, which can be seen in the above animation.

    He later published details of how the algorithm works, but didn’t include an implementation. In this article I provide such an implementation, along with some further notes and explanations. I have also reproduced his animations in interactive form, and at the end I’ll show a new variation of the function I came up with.

    Read on →

  • The gamer's guide to line algorithms

    Apr 10, 2025

    The five algorithms from this article frolicking in their natural habitat. Can you tell which one is which?

    If you’ve ever tried making your own roguelike, you’ve likely encountered all sorts of strange-looking 1960s algorithms for doing geometry. Bresenham’s line algorithm is the most famous, but you often see people talking about “DDA lines” among other things. Hopefully, you found your way to the excellent Red Blob Games article on the subject, which describes a modern implementation in detail, with lovely interactive examples.

    A line algorithm used to menace an innocent watervine farmer

    Perhaps this left you with unanswered questions. These vintage functions look so different from each other, but they all seem to do the same thing!

    This post is structured as an ahistorical journey back through time. We start with the floating point algorithm given by Red Blob, which is optimal on modern computers, and end at Bresenham’s 1962 algorithm which uses pure integer arithmetic, as was optimal on the hardware of the day. We can then consider the other functions as steps along our imagined refactoring process, replacing floats piece by piece with integers.

    The aim is to present these functions in a unified style so they can be easily compared, and to try and give a little intuition as to what is going on with the geometry.

    Read on →

  • pure_rng - an immutable random number generator

    Mar 14, 2025

    Stepping through bad RNG configurations. PureRNG will of course produce pure white noise :)

    Anyone who considers arithmetical methods of producing random digits is, of course, in a state of sin.

    — John von Neumann, 1949

    PureRng, my newest creation, is a random number generator library for rust. The twist is that each generator gets consumed after outputting a single value. To get a new value you must instantiate a new generator, with a new seed. A nice API is provided to ensure this isn’t as much hassle as it sounds - a hash function is employed to generate seeds from arbitrary data.

    Read on →

  • NixOS on a Raspberry Pi 4 in 2025

    Jan 30, 2025

    Below is my minimal NixOS configuration for a Raspberry Pi 4B. It includes everything needed, ready to be loaded from a generic flake.nix.

    First enable binfmt on your build machine:

    boot.binfmt.emulatedSystems = [ "aarch64-linux" ];
    

    Then build the SD card image with:

    nix build '.#nixosConfigurations.YOUR_HOSTNAME.config.system.build.sdImage'
    

    …and rebuild remotely with:

    nixos-rebuild switch --flake .#YOUR_HOSTNAME --target-host RASPI_IP --use-remote-sudo
    

    Below is the configuration.nix, with notes on how this all works after the fold:

    { lib, pkgs, modulesPath, ... }:
    {
      imports = [
        # This module installs the firmware
        "${modulesPath}/installer/sd-card/sd-image-aarch64.nix"
      ];
    
      nix.settings = {
        # This is needed to allow building remotely
        trusted-users = [ "YOUR_USERNAME" ];
    
        # The nix-community cache has aarch64 builds of unfree packages,
        # which aren't in the normal cache
        substituters = [
          "https://nix-community.cachix.org"
        ];
        trusted-public-keys = [
          "nix-community.cachix.org-1:mB9FSh9qf2dCimDSUo8Zy7bkq5CX+/rkCWyvRCYg3Fs="
        ];
      };
    
      nixpkgs = {
        hostPlatform = "aarch64-linux";
        config = {
          allowUnfree = true;
        };
      };
    
      # These options make the sd card image build faster
      boot.supportedFilesystems.zfs = lib.mkForce false;
      sdImage.compressImage = false;
    
      networking = {
        # Set your hostname
        hostName = "YOUR_HOSTNAME";
        useNetworkd = true;
      };
    
      # Replace networkd with NetworkManager at your discretion
      systemd = {
        network = {
          enable = true;
    
          networks."10-lan" = {
            # This is the correct interface name on my raspi 4b
            matchConfig.Name = "end0";
    
            networkConfig.DHCP = "yes";
            linkConfig.RequiredForOnline = "routable";
          };
        };
      };
    
      # Add your username and ssh key
      users.users.YOUR_USERNAME = {
        isNormalUser = true;
        extraGroups = [ "wheel" ];
        openssh.authorizedKeys.keys = [ "YOUR_SSH_PUBLIC_KEY" ];
      };
    
      # Our user doesn't have a password, so we let them
      # do sudo without one
      security.sudo.wheelNeedsPassword = false;
    
      services = {
        openssh.enable = true;
      };
    
      # Set your timezone
      time.timeZone = "YOUR_TIMEZONE";
    
      environment.systemPackages = with pkgs; [
        libraspberrypi
        raspberrypi-eeprom
      ];
    
      hardware.enableRedistributableFirmware = true;
    
      system.stateVersion = "24.11";
    }
    

    Some notes:

    Read on →

  • Setting the zellij tab title to the running process in zsh

    Jun 24, 2024

    Here’s how to do it:

    function current_dir() {
        local current_dir=$PWD
        if [[ $current_dir == $HOME ]]; then
            current_dir="~"
        else
            current_dir=${current_dir##*/}
        fi
        
        echo $current_dir
    }
    
    function change_tab_title() {
        local title=$1
        command nohup zellij action rename-tab $title >/dev/null 2>&1
    }
    
    function set_tab_to_working_dir() {
        local result=$?
        local title=$(current_dir)
        # uncomment the following to show the exit code after a failed command
        # if [[ $result -gt 0 ]]; then
        #     title="$title [$result]" 
        # fi
    
        change_tab_title $title
    }
    
    function set_tab_to_command_line() {
        local cmdline=$1
        change_tab_title $cmdline
    }
    
    if [[ -n $ZELLIJ ]]; then
        add-zsh-hook precmd set_tab_to_working_dir
        add-zsh-hook preexec set_tab_to_command_line
    fi
    

    Not actually the running process, but the last command line. This does unfortunately add a couple of milliseconds of lag but it’s fairly imperceptible. If you can make this happen faster, please comment on the gist.

« Older