2024-12-22 08:00:00
Here are my favorite things, digital and physical, of 2024!
These links go to my reviews (no spoilers):
What were your favorites?
I even wrote a blogpost about it. ↩
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.
You can initialize jj
in an existing git
repository like this:
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
.
Running jj log
in the repository for this very website gives this output:
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.
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:
Running git log
again:
()
)
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.
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.
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.
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.
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!
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!
What’s cool about a jj
change, is that updating it doesn’t change its ID. ↩
If you work in large projects with many contributors, you can
tune your jj log
to only your revisions. ↩
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.
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.
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:
The config for the code editor I use all day is 5 loc. Here it is:
= "kanagawa"
[]
= "relative"
= true
= [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.
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.
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.
Here are some tools you have sent me that are zero/minimal config:
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.
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.
The first thing I want to do is make the UI titlebar match the Emacs theme and hide the file icon:
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:
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:
I like to keep most of my code at 80 characters, so let’s add a ruler:
Finally, we want to store backup files in ~/.saves
instead of next to the file
we’re saving:
Let’s install company-mode first, for auto-completion:
Now we configure the built-in LSP package eglot:
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.
We’ll use treesit-auto to automatically install and use tree-sitter major modes:
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.
Next, we install all language modes we need:
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
.
Sometimes we don’t know what file we’re looking for, so let’s add rg.el to help us find it:
This opens a Magit-like menu and allows you to search in various modes (dwim, regex, literal, etc.).
Opening a Zig project now looks like this3; see also the final
init.el
:
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!
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.
First, we create a directory called axiom-zig
and run zig init
:
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.
Our first method will be getDatasets
, which returns a list of Axiom datasets
(see the api documentation).
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.
getDatasets
fnLet’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.
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();
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.
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.
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!
2024-05-01 08:00:00
Zig is a programming language designed by Andrew Kelley. The official website lists three principles of the language:
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.
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.
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.
The error messages of the Zig compiler can be very hard to figure out. Here’s an example:
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.
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.
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?
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.
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.