1 Commits
tip ... booids

Author SHA1 Message Date
Mitchell Hashimoto
b0573a40e7 BOO! Ids. (Booids, rhymes with "squids")
This introduces an ID format for Ghostty objects, lovingly called the
"booid" (rhyming with "squids" because "guids"). I'm not a huge fan of
overly cute naming -- I prefer obvious names -- but all the obvious
names like "unique ID" or "global ID" are too similar to other, very
real, standard ID formats.

These IDs will be used in the short term by app IDs to reference
surfaces for things like notifications. And in the long term, we'll use
these for the still-to-be-designed Ghostty API. And super long term,
we'll use these for session attach/detach.

There's no concurrency for our ID generation today, so this commit only
has a simple non-thread-safe implementation. That's all we need.

Ghostty's use case has slightly different requirements than most unique
ID requirements, so I believe a custom format is the way to go here.
These requirements are noted here:

The booid is a 64-bit ID to make it easy to send across C ABIs and local
transports like D-Bus, both of which are very important for a desktop
application like Ghostty.

We follow the same timestamp, machine, sequence number format as
Snowflake IDs, but we use the full 64 bits for the ID, rather than 63,
since can explicitly note its an unsigned ID format. Our bit width
breakdown allows for 1,024 nodes in a cluter producing 4,096 IDs per
millisecond.

"A cluster?! What the @#!* are you talking about? This is a local
terminal emulator." Yes, yes indeed. 42-bits of timestamp and 12 bits
of sequence are more than enough for a local terminal emulator, so we
have 10 bits left over (arguably, we need even less sequence bits). For
now, we designate this as a "machine ID" and always set it to "0" since
we are local-only.

But I think this will be useful in the future if/when we introduce the
ability to attach to remote Ghostty instances for session sharing,
restore, etc. This is a use case and feature I have always planned. 

The question of how we assign machine IDs in that future is left to the
future. I propose a simple idea in the comments in this commit: assign
and map them locally! All local IDs can produce machine ID 0 and then
the CLIENT side can replace those bits with some value. This eliminates
a distributed consensus problem and allows a client terminal to attach
to up to 1,024 remote instances at once (unlikely lol).
2025-08-15 11:08:16 -07:00
2 changed files with 118 additions and 0 deletions

117
src/booid.zig Normal file
View File

@@ -0,0 +1,117 @@
const std = @import("std");
const assert = std.debug.assert;
/// A booid (/ˈbwɪd/, rhymes with "squid") is a Ghostty identifier based
/// loosely on Snowflake-style IDs. Booids are used to uniquely identify
/// Ghostty objects.
///
/// A booid is a 64-bit value with the following structure:
///
/// - 42 bits of millisecond level precision time since Aug 6, 2025 GMT.
/// The epoch is 1754438400.
/// - 10 bits for unique machine ID (1024 possible values).
/// - 12 bit monotonic sequence number (4096 possible values).
///
/// As a result, we can generate approximately 4096 booids per millisecond
/// for a single machine ID and ~4 million booids per millisecond across
/// a fully saturated cluster.
///
/// "Cluster?" Why are we talking about clusters? Ghostty is a local
/// terminal emulator! I'm thinking in advance of when we support tmux-style
/// servers that you can attach to and detach from, which will form a
/// cluster. A 10-bit cluster ID seems excessive, but the other used bits
/// are more than big enough so the 10-bits is mostly leftover. We can
/// partition the 10 bits further if we want to later.
///
/// For now, all Ghostty IDs will use random machine ID on launch. We can
/// address the machine ID issue later when we have servers. My thinking
/// for now is that we actually assign machine IDs client side and remap
/// them for API requests so that we don't need any distributed consensus.
/// This would allow a local machine to be connected to at most 1,023 (one
/// reserved value for local) remote machines, which also seems... unlikely!
///
/// ## Design Considerations
///
/// We chose a 64-bit identifier to keep it simple to transport IDs
/// across various ABIs and protocols easily. A 128-bit identifier (such
/// as a UUID) would require manually unpacking and repacking two 64-bit
/// components since there is no well-defined 128-bit integer type for C ABIs
/// and many popular desktop transport protocols such as D-Bus also don't
/// natively support 128-bit integers.
///
/// Ghostty won't be generating booids at a super high rate. At the time
/// of writing this, booids are going to be used to identify surfaces, and
/// surfaces are only created when a new terminal is launched, which requires
/// creating a pty, launching a process, etc. So the speed and number of booids
/// is naturally limited.
pub const Booid = packed struct(u64) {
/// Sequence number. This is a monotonic sequence number that starts
/// at zero per millisecond.j:want
seq: u12,
/// Machine ID. This is always zero for the local machine. Ghostty
/// doesn't currently support saving the ID outside of the local machine
/// so this is mostly unused. See the notes in the struct doc comment
/// for how I'm thinking about this.
machine: u10,
/// Milliseconds since Aug 6, 2025 GMT (or 1754438400 since Unix epoch).
timestamp: u42,
};
pub const epoch = 1754438400; // Aug 6, 2025 GMT
/// A booid generator that assumes no local concurrency.
pub const Generator = struct {
last: Booid,
/// A local ID generator (machine ID 0).
pub const local: Generator = .{
.last = .{
.timestamp = 0,
.machine = 0,
.seq = 0,
},
};
/// Get the next booid from the generator.
pub fn next(self: *Generator) error{Overflow}!Booid {
const timestamp = timestamp: {
const now_unix = std.time.milliTimestamp();
assert(now_unix >= epoch);
const now_i64 = now_unix - epoch;
break :timestamp std.math.cast(u42, now_i64) orelse
return error.Overflow;
};
// If our timestamp changed, we reset our sequence number
if (timestamp != self.last.timestamp) {
assert(timestamp > self.last.timestamp);
const result: Booid = .{
.timestamp = timestamp,
.machine = self.last.machine,
.seq = 0,
};
self.last = result;
return result;
}
// Increase our sequence number
self.last.seq = std.math.add(u12, self.last.seq, 1) catch
return error.Overflow;
return self.last;
}
};
test Generator {
const testing = std.testing;
var g: Generator = .local;
const a = try g.next();
const b = try g.next();
try testing.expect(a != b);
try testing.expect(a.timestamp <= b.timestamp);
try testing.expect(a.machine == b.machine);
try testing.expect(a.seq < b.seq);
try testing.expect(@as(u64, @bitCast(a)) < @as(u64, @bitCast(b)));
}

View File

@@ -171,6 +171,7 @@ pub const std_options: std.Options = .{
};
test {
_ = @import("booid.zig");
_ = @import("pty.zig");
_ = @import("Command.zig");
_ = @import("font/main.zig");