MoreRSS

site iconArne BahloModify

a developer, podcaster & dad based near Frankfurt, Germany.
Please copy the RSS to your reader, or quickly subscribe to:

Inoreader Feedly Follow Feedbin Local Reader

Rss preview of Blog of Arne Bahlo

My favorite things of 2024

2024-12-22 08:00:00

Here are my favorite things, digital and physical, of 2024!

Music

Books

These links go to my reviews (no spoilers):

Apps

  • Manet: I’ve started buying my music and hosting it myself this year1, this app makes it a joy to listen to music.
  • CouchTimes: The best TV show tracker in the App Store, period. It doesn’t have feature bloat and it respects your privacy.
  • Ghostty: A super fast, new Terminal emulator for the Mac.
  • Halide: This camera app has been around for a while, but this year they released Process Zero, a mode that applies minimal corrections and makes photos look more like digital camera photos.

Goods

  • aranet4: There are cheaper options, but a CO2 monitor for my home office has helped me keep the air at good levels.
  • heat-it: This tiny tool draws power from your phone to relieve itch & pain from insect bites.
  • Leatherman 2H Wave+: The only big Leatherman that’s legal to carry in Germany, super versatile and handy to have around.

What were your favorites?

  1. I even wrote a blogpost about it.

Jujutsu in practice

2024-10-21 08:00:00

This post is not about the Japanese martial arts Jiu-jitsu, it’s about a new VCS, or version control system. There are some great tutorials and introductions for Jujutsu, short jj, so I want to give some insight in how I use it day to day for this website.

Initialize a repository

You can initialize jj in an existing git repository like this:

$ jj git init --git-repo .

This will create a .jj directory next to .git. You can now use both git and jj, although I wouldn’t recommend it. There’s a work-in-progress native storage backend, but since all my projects are git repositories anyway, this works great for me.

Note that this is non-destructive; if you want to go back to git, all it takes is a rm -r .jj.

Get an overview

Running jj log in the repository for this very website gives this output:

$ jj log
@  qzsvtxpv [email protected] 2024-10-21 09:58:06 e18f7532
  Add blog/jj-in-practice
 ○  yoxxsupn [email protected] 2024-10-20 22:55:03 ae5d9109
├─╯  Add library/calibans-war
 ○  tvkvwslw [email protected] 2024-10-20 22:49:54 5e4dee1f
├─╯  Add library/the-posthumous-memoirs-of-bras-cubas
 ○  pywmtrys [email protected] 2024-10-20 21:20:11 7bda14b7
 │  Add atoms/1
 ○  xnlzypwn [email protected] 2024-10-20 21:20:10 8a004404
├─╯  Add atoms functionality
  wxxtrmqk [email protected] 2024-10-20 16:18:15 main HEAD@git 1eb46c81
  Add weekly/166
~

This already shows one of the biggest differences, compared to git: There’s no branches, other than main. You can create branches, which are called bookmarks in jj, but you don’t need to. Instead, you work mostly with changes1.

The terminal above shows the change w (you can use the first letter to reference changes; on your terminal it’ll be highlighted as well) as a parent to x, t, y and q. All these child-revisions don’t have a branch/bookmark, but they don’t need one. You can see what’s in-flight at this repository better than with any git repo, especially if branches haven’t been cleaned up in a while.

Create a revision

My usual flow with git is to leave changes in the staging area until I’m ready to commit. Sometimes, if I have to switch branches or want to save my work, I’ll stash or create a WIP commit.

In jj, there is no staging area—everything is a revision. Let’s create a new revision on top of my revisions where I add atoms functionality:

$ jj new -r p

Running git log again:

$ jj log
@  kxqvnxnw [email protected] 2024-10-21 10:03:20 22c020cf
  (empty) (no description set)
  pywmtrys [email protected] 2024-10-20 21:20:11 HEAD@git 7bda14b7
  Add atoms/1
  xnlzypwn [email protected] 2024-10-20 21:20:10 8a004404
  Add atoms functionality
 ○  qzsvtxpv [email protected] 2024-10-21 10:03:18 27229dca
├─╯  Add blog/jj-in-practice
 ○  yoxxsupn [email protected] 2024-10-20 22:55:03 ae5d9109
├─╯  Add library/calibans-war
 ○  tvkvwslw [email protected] 2024-10-20 22:49:54 5e4dee1f
├─╯  Add library/the-posthumous-memoirs-of-bras-cubas
  wxxtrmqk [email protected] 2024-10-20 16:18:15 main 1eb46c81
  Add weekly/166
~

You’ll notice that our active revisions are now left-aligned, and the one to add this very blog post has moved to the right. There’s no hierarchy, they’re all descendants of Add weekly/166.

After doing some work, e.g. adding a new atom, I can describe that revision with jj describe. This is comparable to git commit, but it doesn’t actually create a commit or a revision, it only describes the current one.

Sometimes I want to update a previous revision, in this case Add atoms/1. I can run jj squash to merge the current one with its parent.

Push and pull

To fetch new revisions, I run jj git fetch; to push branches/bookmarks, I run jj git push. This uses the same git server it was using before.

Before pushing, I need to move my bookmark to the revision I want to push. I push the main branch to deploy my website, so if I wanted to publish my atoms functionality (should I?), I would run jj bookmark set main -r p before pushing.

Rebase and split

Sometimes I need to rebase. Fortunately that’s a lot simpler than it is in git: I can run jj rebase -s <source> -d <destination> to move revisions around. If I wanted support for atoms for this blog post, I would run jj rebase -s q -d p and it would move the revision for this blog post on top of “Add atoms/1”.

jj also does automatic rebasing, e.g. if you squash changes into a revision that has descendants.

And if I have a revision that I’d like to be two, I run jj split and interactively select what belongs to which revision.

Undo

Undoing an (interactive) rebase in git is not fun. jj undo undoes the last jj operation, doesn’t matter if it’s abandoning (deleting) a revision or doing a rebase. This is a life saver! You can also run jj op log to display your last jj operations.

Things I stumble upon

I’ve been using git for a long, long time. My brain assumes that after a commit, I’m working on the next one. It also assumes that jj describe does the same as git commit (it does not). I often describe a revision and continue editing files, which then erroneously get added to the current revision. I’m not saying this is wrong, it makes sense in the jj world, but I keep tripping over that and have to run jj split to pull changes out again.
alterae on Lobste.rs pointed out that you can describe and immediately create a new revision on top of it with jj commit. Thanks!

One other thing is that you cannot check out a revision directly (or maybe I just don’t know how to), so when I’ve moved to a different revision and want to move back, I run jj new <revision>, which creates an empty revision on top of it. This means that if I’m not done with the revision, I have to keep running jj squash to move changes into it.
gecko on Lobste.rs pointed out that you can check out a revision directly with jj edit <revision>. Thanks!

Why it works for me

A week ago, I removed jj from my website’s repository, to see if I’d miss it. I added it back the same day. Jujutsu feels lighter than git, while at the same time giving you a full overview of what’s in-flight right now2. Having no staging area means I only need to worry about revisions (see caveat above).

If trying new things sounds fun to you, give Jujutsu a spin!

Further reading

  1. What’s cool about a jj change, is that updating it doesn’t change its ID.

  2. If you work in large projects with many contributors, you can tune your jj log to only your revisions.

We need more zero config tools

2024-10-01 08:00:00

It just works. — Steve Jobs

If you follow this blog, you’ll know that I’m doing a series called “Emacs Config From Scratch”1. Emacs is an editor operating system, where you can configure and customize literally everything. I like that you can truly make it yours, but it’s a lot of work to get there.

Recently, I’ve become fond of (command line) tools that just work, out of the box2. This blogpost is an ode to them.

Fish

Julia Evans recently posted Reasons I still love the fish shell, and the first point she makes is “no configuration”.

Things that require plugins and lots of code in shells like ZSH, like autosuggestions, are included and configured by default in fish. At the time of writing this, my fish config has less than 31 loc, most of which are abbreviations.

I have two fish plugins configured: z for jumping to a directory, and hydro as my shell prompt. Neither need configuration.

Helix

My Neovim config had 21 external plugins. Making LSP, tree-sitter and formatting work took a while (LSP alone needs 3 plugins) and in the end there were still things that didn’t work.

I’ve switched to Helix, which can do so much out of the box, here’s a non-exhaustive list:

  • LSP (including autocompletion, show signature, go to definition, show references, etc.) just works
  • Tree-sitter is built in, you can even do selections on tree-sitter objects
  • A file picker and global search
  • Pressing a key in normal mode shows subsequent keys you can press, and what they do
  • You can jump to any visible word, add/remove/replace quotes or other characters
  • … and so much more

The config for the code editor I use all day is 5 loc. Here it is:

theme = "kanagawa"

[editor]
line-number = "relative"
cursorline = true
rulers = [80]

I will say that it takes some getting used to as it folows the selection -> action model, i.e. you need to run wd instead of dw to delete the next word.

Lazygit

After raving about Magit in London, my team showed me Lazygit and I’ve been using it ever since—it’s really good and it does exactly what you want it to do, without configuration3.

You can toggle different screen modes, panes adjust in size when active and pretty much everything you want to do is only a few keystrokes away.

Zellij

A batteries-included Tmux alternative, Zellij doesn’t need any configuration to work well. You can set up Layouts without additional plugins (although there is a plugin system) and I’m generally not missing anything from my Tmux configuration.

My favorite feature is the floating panes. Press Ctrl + p, w to toggle a pane floating on top of everything else—I often use this for Lazygit.

What else?

Here are some tools you have sent me that are zero/minimal config:

  • Matthew shared Broot, a new way to see and navigate directory trees
  • Ilija shared k9s, a CLI to maange Kubernetes clusters
  • Alexander shared Orbiton, a text editor they built

Do you have a tool that requires no (or minimal) configuration? Send me an email and I’ll add it to the list!

And if you’re building something, please strive to make the default experience work really well for most people.

  1. Check it out

  2. I’m starting to feel the same about programming languages and external dependencies, but that’s a different post.

  3. You can configure almost everything—but you don’t need to.

Emacs Config From Scratch, Part 3: LSP &amp; Tree-sitter

2024-05-11 08:00:00

This is Part 3 of my series, Emacs Config From Scratch1, where I create my perfect editor using Emacs. In this post, I’ll do some housekeeping, set up LSP2, language modes for Rust, Go, TypeScript and Zig, and add search.

Table Of Contents

Housekeeping

The first thing I want to do is make the UI titlebar match the Emacs theme and hide the file icon:

(use-package emacs
  :init
  (add-to-list 'default-frame-alist '(ns-transparent-titlebar . t))
  (add-to-list 'default-frame-alist '(ns-appearance . light))
  (setq ns-use-proxy-icon  nil)
  (setq frame-title-format nil))

The next thing is automatically loading $PATH from my shell using exec-path-from-shell—this is important so Emacs can find our installed binaries, e.g. for language servers:

(use-package exec-path-from-shell
  :init
  (exec-path-from-shell-initialize))

Another problem I ran into was writing square brackets when on the MacBook, as it would interpret the keys as Meta-5/Meta-6. I fixed that by updating the keybindings from Part 1:

(use-package emacs
  :init
  (when (eq system-type 'darwin)
    (setq mac-command-modifier 'super)
    (setq mac-option-modifier nil)
    (setq mac-control-modifier nil)))

I like to keep most of my code at 80 characters, so let’s add a ruler:

(use-package emacs
  :init
  (setq-default fill-column 80)
  (set-face-attribute 'fill-column-indicator nil
                      :foreground "#717C7C" ; katana-gray
                      :background "transparent")
  (global-display-fill-column-indicator-mode 1))

Finally, we want to store backup files in ~/.saves instead of next to the file we’re saving:

(use-package emacs
  :config
  (setq backup-directory-alist `(("." . "~/.saves"))))

LSP

Let’s install company-mode first, for auto-completion:

(use-package company-mode
  :init
  (global-company-mode))

Now we configure the built-in LSP package eglot:

(use-package emacs
  :hook (zig-mode . eglot-ensure)
  :hook (rust-mode . eglot-ensure)
  :hook (go-mode . eglot-ensure)
  :hook (typescript-mode . eglot-ensure)
  :general
  (leader-keys
    "l" '(:ignore t :which-key "lsp")
    "l <escape>" '(keyboard-escape-quit :which-key t)
    "l r" '(eglot-rename :which-key "rename")
    "l a" '(eglot-code-actions :which-key "code actions")))

This runs eglot-ensure in languages we have language servers installed for. It also sets up SPC l r to rename a symbol and SPC l a to prompt for code actions.

Tree-sitter

We’ll use treesit-auto to automatically install and use tree-sitter major modes:

(use-package treesit-auto
  :custom
  (treesit-auto-install 'prompt)
  :config
  (treesit-auto-add-to-auto-mode-alist 'all)
  (global-treesit-auto-mode))

This is handy because it doesn’t require us to think about using e.g. zig-ts-mode instead of zig-mode, it handles everything for us.

Language support

Next, we install all language modes we need:

(use-package markdown-mode
  :config
  (setq markdown-fontify-code-blocks-natively t))
(use-package zig-mode
  :general
  (leader-keys
    "m" '(:ignore t :which-key "mode")
    "m <escape>" '(keyboard-escape-quit :which-key t)
    "m b" '(zig-compile :which-key "build")
    "m r" '(zig-run :which-key "run")
    "m t" '(zig-test :which-key "test")))
(use-package rust-mode
  :general
  (leader-keys
    "m" '(:ignore t :which-key "mode")
    "m <escape>" '(keyboard-escape-quit :which-key t)
    "m b" '(rust-compile :which-key "build")
    "m r" '(rust-run :which-key "run")
    "m t" '(rust-test :which-key "test")
    "m k" '(rust-check :which-key "check")
    "m c" '(rust-run-clippy :which-key "clippy")))
(use-package go-mode)
(use-package gotest
  :general
  (leader-keys
    "m" '(:ignore t :which-key "mode")
    "m <escape>" '(keyboard-escape-quit :which-key t)
    "m t" '(go-test-current-project :which-key "test")
    "m r" '(go-run :which-key "run")))
(use-package typescript-mode)

I’m using SPC m to change based on major-mode, e.g. means SPC m t means test in most programming modes, but won’t exist in markdown-mode.

Search

Sometimes we don’t know what file we’re looking for, so let’s add rg.el to help us find it:

(use-package rg
  :general
  (leader-keys
    "f" '(rg-menu :which-key "find")))

This opens a Magit-like menu and allows you to search in various modes (dwim, regex, literal, etc.).

Wrapping up

Opening a Zig project now looks like this3; see also the final init.el:

A screenshot of Emacs with a dark theme showing Zig code and a context menu for code actions

I’m going to switch to Emacs as my primary editor and tune it further in the coming weeks. In the next part, I want to add support for Org-mode, show a dashboard on startup, enable font ligatures and fix all the small things that I’ll find.

Subscribe to the RSS feed so you don’t miss Part 4, and let me know what you think!

  1. Check out parts one and two if you haven’t already!

  2. Language Server Protocol

  3. By the way, I switched my theme to Kanagawa.

Writing an SDK in Zig

2024-05-09 08:00:00

The first project I used Zig for was a rewrite of a custom static site generator for the Fire Chicken Webring, you can read that post here: Thoughts on Zig.

Writing a small application is a lot easier than writing a library, especially if you’re hacking it together like I was. Let’s do something harder.

And because I work at Axiom, we’re going to write an SDK for the public API. In this first part I’ll set up the library and add a single getDatasets fn to fetch all datasets the token has access to.

We’re using Zig 0.12. It might not work with a different version.

Bootstrap the SDK

First, we create a directory called axiom-zig and run zig init:

info: created build.zig
info: created build.zig.zon
info: created src/main.zig
info: created src/root.zig
info: see `zig build --help` for a menu of options

We also want to create a .gitignore to ignore the following folders:

/zig-cache
/zig-out

Perfect. Next step: Create a client struct in root.zig. We’ll need an Axiom API token to authenticate requests, a std.http.Client to make requests and an std.mem.Allocator to allocate and free resources:

const std = @import("std");
const Allocator = std.mem.Allocator;
const http = std.http;
// We'll need these later:
const fmt = std.fmt;
const json = std.json;

/// SDK provides methods to interact with the Axiom API.
pub const SDK = struct {
    allocator: Allocator,
    api_token: []const u8,
    http_client: http.Client,

    /// Create a new SDK.
    fn new(allocator: Allocator, api_token: []const u8) SDK {
        return SDK{ .allocator = allocator, .api_token = api_token, .http_client = http.Client{ .allocator = allocator } };
    }

    /// Deinitialize the SDK.
    fn deinit(self: *SDK) void {
        self.http_client.deinit();
    }
}

test "SDK.init/deinit" {
    var sdk = SDK.new(std.testing.allocator, "token");
    defer sdk.deinit();
    try std.testing.expectEqual(sdk.api_token, "token");
}

Initially I had deinit(self: SDK) (without the pointer). Zig didn’t like this at all and led me down a rabbit hole of storing the http.Client as a pointer too—once I found my way out and remembered I need a pointer everything worked.

I like that Zig encourages writing tests not only next to the source code (like Go), not only in the same file (like Rust), but next to the code it’s testing.

Add getDatasets

Our first method will be getDatasets, which returns a list of Axiom datasets (see the api documentation).

Create a model

First we need a model:

pub const Dataset = struct {
    id: []const u8,
    name: []const u8,
    description: []const u8,
    who: []const u8,
    created: []const u8,
};

Don’t worry about created being a datetime, we’ll deal with that later.

Add the getDatasets fn

Let’s add a function to get the datasets to our SDK struct:

/// Get all datasets the token has access to.
/// Caller owns the memory.
fn getDatasets(self: *SDK) ![]Dataset {
    // TODO: Store base URL in global const or struct
    const url = comptime std.Uri.parse("https://api.axiom.co/v2/datasets") catch unreachable;

    // TODO: Draw the rest of the owl
}

We’re taking a pointer to SDK called self again, this means that this is a method you call on a created SDK. The ! means it can return an error. In a later post I want to go deeper into error handling, for now it can return any error.

Because there is no dynamic part of the URL, we can parse it at compile time using comptime. I like this explicit keyword, in Rust you need to rely on macros to achieve something similar, or use lazy_static.

Make the HTTP request

Let’s open a connection to the server:

var server_header_buffer: [4096]u8 = undefined; // Is 4kb enough?
var request = try self.http_client.open(.GET, url, .{
    .server_header_buffer = &server_header_buffer,
});
defer request.deinit();

I wonder if 4kb is always enough for server headers. Especially in a library I don’t want it to fail because the server is suddenly sending more headers.

Axiom uses Bearer auth, so let’s set the Authorization header:

var authorization_header_buf: [64]u8 = undefined;
// TODO: Store this on the SDK for better re-use.
const authorization_header = try fmt.bufPrint(&authorization_header_buf, "Bearer {s}", .{self.api_token});
request.headers.authorization = .{ .override = authorization_header };

An Axiom API is 41 characters, plus Bearer ‘s 7 characters equals 48 characters. We’re allocating 64 to be safe if it ever changes (it really shouldn’t).

Also note that I’m calling try fmt.BufPrint; this will return the error to the caller of our function (that’s what the ! indicating).

Finally, we can send the headers to the server and wait for a response:

try request.send();
try request.wait();

Parse the JSON-response

First, we need to read the body into a buffer:

var body: [1024 * 1024]u8 = undefined; // 1mb should be enough?
const content_length = try request.reader().readAll(&body);

Same issue as with the server headers: What is a good size for a fixed buffer here?

I’ve tried doing this dynamically with request.reader().readAllAlloc(...), but parsing the JSON with this allocated memory relied on the allocated []const u8 for string values. This means as soon as I deallocated the body, all string values in the returned JSON were invalid (use-after-free). Yikes.

Next, we call json.parseFromSlice with our body:

const parsed_datasets = try json.parseFromSlice([]Dataset, self.allocator, body[0..content_length], .{});
defer parsed_datasets.deinit();

Now we need to copy the memory out of the parsed_datasets.value to prevent it from being freed on the parsed_datasets.deinit() above and return it:

const datasets = try self.allocator.dupe(Dataset, parsed_datasets.value);
return datasets;

Edit: Matthew let me know on Mastodon that this implementation is still illegal, and it’s only working because the stack memory is not getting clobbered. You can set .{ .allocate = .alloc_always } in json.parseFromSlice, which will dupe the strings, but not actually solve the problem (where do the strings live?). What I ended up doing is creating a Value(T) struct, which embeds both the value and an arena allocator which I pass to json.parseFromSliceLeaky. This means the value you get back from getDatasets will have a deinit() method and you need to do .value to get the actual value. You can read the updated source code on GitHub.

Write a test

And finally we’ll write a test where we initialize the SDK, get the datasets and ensure _traces is the first one returned. Once I set up CI, I’ll create an Axiom org just for testing so we can be sure which datasets are returned.

test "getDatasets" {
    const allocator = std.testing.allocator;

    const api_token = try std.process.getEnvVarOwned(allocator, "AXIOM_TOKEN");
    defer allocator.free(api_token);

    var sdk = SDK.new(allocator, api_token);
    defer sdk.deinit();

    const datasets = try sdk.getDatasets();
    defer allocator.free(datasets);

    try std.testing.expect(datasets.len > 0);
    try std.testing.expectEqualStrings("_traces", datasets[0].name);
}

If you want to see everything together, check it out on GitHub.

Next steps

In the next part I’ll add createDataset, updateDataset and deleteDataset, initial error handling and show how you can import the library in a Zig project.

Any thoughts? Let me know!

Thoughts on Zig

2024-05-01 08:00:00

Zig is a programming language designed by Andrew Kelley. The official website lists three principles of the language:

  • No hidden control flow.
  • No hidden memory allocations.
  • No preprocessor, no macros.

For someone like me coming mostly from Rust, Go and TypeScript, this is different—and different is interesting, so I wanted to know what it feels like to write code in it.

Here are my thoughts after 3 nights of using Zig to rewrite the static site generator1 I use for the Fire Chicken Webring. Note that this is a limited use case and only scratches the surface of the language.

Explicit allocation

This is one of the biggest differentiators of Zig. Go doesn’t force you to think at all, Rust forces you to think about ownership and Zig forces you to think about allocations. If you want to allocate memory, you need to pass an allocator to it and remember to free it afterwards.

One thing that I stumbled upon a lot: Sometimes there is a .deinit() function on the returned struct, sometimes that method takes an allocator, sometimes you need to allocator.free(value) and sometimes it returns an enum or a struct, and you need to figure out which values you have to free.

Documentation

If you write Zig, you’ll find yourself reading Zig a lot to understand how to use a function, which resources you must free, and possibly, why that function panics. There is no generated documentation like docs.rs or pkg.go.dev; if you want to know which methods a library has, look at the source.

Here are some resources other than the source code that I found useful to get started:

Another reason you might need to look at the source code of function you’re calling is confusing error messages.

Errors

The error messages of the Zig compiler can be very hard to figure out. Here’s an example:

$ zig build run
Segmentation fault at address 0x102ee6000
Panicked during a panic. Aborting.

Generally, if you’re used to Rust’s exceptional error handling, this is rough.

Once I got an error from the standard library and only noticed after reading the source code that ArrayList is not a supported type to pass to the given function. Another time, the templating library I’ve temporarily used randomly panicked with an out-of-bounds after doing a nested loop.

Libraries

There are a bunch of libraries for Zig (see awesome-zig) and I can only talk about the one’s I’ve tried, but most of the libraries I’ve looked at are either archived, a thin wrapper around a C library, heavy WIP and barely usable, or have weird error scenarios.

This lead to me implementing my own shitty datetime function2 and using std.fmt instead of templating.

I believe these are due to the immaturity of the language and ecosystem, but I wouldn’t be surprised if people started building their own libraries, which they take everywhere.

Strings

We have to talk about strings. Zig has none; if you want a string, use []const u8. You also can’t compare that type with ==, you need to use a specific function3.

Initially I found this irritating—why not introduce a string type that is []const u8 under the hood and overload the == operator? I think it would improve developer experience, but does it fit into Zig?

No magic

Remember the three idioms from the beginning of the article? Zig is huge on being transparent, i.e., no magic; the code you read is what happens.

And I appreciate that. In Rust, it’s common to build abstractions to hide boilerplate logic (e.g. using macros to generate the deserialization logic of a struct), in Go it’s common to generate code to do this. Zig doesn’t have any of that (though I guess you could generate Zig code). I’m not sure how well that scales in big codebases, but I think it’s interesting.

Conclusion

I like Zig. For a bigger project or something that needs async4, I’ll still reach for Rust for its safety features and vibrant ecosystem, but for small projects, it’s fun to reach for an interesting language.

  1. See Why You Should Write Your Own Static Site Generator

  2. Maybe don’t look too closely.

  3. std.mem.eql

  4. Async functions have been removed from Zig at this time.