Skip to content

Commit 7c87c25

Browse files
port to Zig 0.16-dev
Major migration from Zig 0.15.2 to 0.16.0-dev.3039: - New compat.zig layer for removed std APIs (file I/O, time, sleep) - std.posix.{fork,close,write,open,dup2,pipe2,waitpid,getenv} → linux/c calls - std.io.fixedBufferStream → compat.MemWriter/MemReader - std.time.nanoTimestamp → compat.nanoTimestamp (clock_gettime) - std.Thread.sleep → linux.nanosleep - std.heap.GeneralPurposeAllocator → std.heap.DebugAllocator - std.process.argsAlloc → std.process.Args.Iterator - ArrayListUnmanaged init .{} → .empty - pub fn main() → pub fn main(init: std.process.Init.Minimal) 11,273 lines. 725KB binary. All tests pass on Zig 0.16-dev.
1 parent 9ac4073 commit 7c87c25

File tree

22 files changed

+467
-175
lines changed

22 files changed

+467
-175
lines changed

CLAUDE.md

Lines changed: 27 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# teru -- AI-first terminal emulator
22

3-
Written in Zig 0.15+. Uses libc.
3+
Written in Zig 0.16+. Uses libc.
44

55
## Build
66
zig build # build
@@ -14,11 +14,33 @@ zig build run # run
1414
- src/agent/ -- Agent protocol (OSC 9999), Claude Code hook handler
1515
- src/tiling/ -- Layout engine (master-stack, grid, monocle, floating)
1616
- src/persist/ -- Session serialization, binary format
17-
- src/config/ -- Lua config loader (planned)
18-
- src/platform/ -- Platform shells: GTK4/Linux, AppKit/macOS, Win32/Windows (planned)
17+
- src/config/ -- Config file parser (key=value format)
18+
- src/compat.zig -- Zig 0.16 compatibility layer (file I/O, time, sleep)
19+
- src/platform/ -- Platform shells: X11+Wayland/Linux, AppKit/macOS, Win32/Windows (planned)
1920

20-
## Zig 0.15 API Notes
21-
- No std.io.getStdOut() -- use posix.write(STDOUT_FILENO, data)
21+
## Zig 0.16 API Notes
22+
- std.io module removed -- no fixedBufferStream, GenericWriter, GenericReader
23+
- std.fs.cwd() removed -- use compat.openFile/createFile (raw POSIX openat)
24+
- std.posix: fork, close, write, open, dup2, pipe2, waitpid, getenv, fcntl, ftruncate all removed
25+
- fork -> std.os.linux.fork() (returns usize, check with @bitCast)
26+
- close -> std.posix.system.close() (returns c_int, must assign to _)
27+
- write -> std.c.write()
28+
- open -> posix.openatZ(posix.AT.FDCWD, ...)
29+
- dup2 -> std.c.dup2()
30+
- pipe2 -> std.c.pipe()
31+
- waitpid -> std.c.waitpid()
32+
- getenv -> std.c.getenv() (returns ?[*:0]u8, use std.mem.sliceTo)
33+
- fcntl -> std.c.fcntl()
34+
- ftruncate -> std.c.ftruncate()
35+
- exit -> std.os.linux.exit()
36+
- std.time.nanoTimestamp() removed -> use linux.clock_gettime(.REALTIME, &ts)
37+
- std.Thread.sleep() removed -> use linux.nanosleep()
38+
- std.heap.GeneralPurposeAllocator -> std.heap.DebugAllocator
39+
- std.process.argsAlloc() removed -> accept std.process.Init.Minimal in main()
40+
- ArrayListUnmanaged: default init .{} -> .empty
41+
- PROT.READ|PROT.WRITE -> .{ .READ = true, .WRITE = true } (packed struct)
42+
- Sigaction handler: fn(c_int) -> fn(posix.SIG)
43+
- @Type builtin removed (macOS MsgSendType needs future replacement)
2244
- callconv(.c) not .C
2345
- winsize fields: .row, .col (no ws_ prefix)
2446
- termios flags: direct bool fields (raw.iflag.ICRNL = false)

README.md

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,9 @@ One binary replaces Alacritty + tmux + terminal management in your window manage
1010

1111
## Build
1212

13-
```bash
14-
# Arch Linux
15-
sudo pacman -S zig
13+
Requires **Zig 0.16+** (0.16-dev or later).
1614

15+
```bash
1716
# Build and run
1817
zig build run
1918

build.zig.zon

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
.{
22
.name = .teru,
33
.version = "0.0.1",
4-
.minimum_zig_version = "0.15.0",
4+
.minimum_zig_version = "0.16.0",
55
.fingerprint = 0xb6637fdb179e7a3c,
66

77
.paths = .{

src/compat.zig

Lines changed: 243 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,243 @@
1+
//! Compatibility layer for Zig 0.16-dev.
2+
//!
3+
//! Replaces removed APIs:
4+
//! - std.fs.cwd().openFile/createFile/deleteFile → raw POSIX fd ops
5+
//! - std.io.fixedBufferStream → MemStream (in-memory writer/reader)
6+
//! - std.time.nanoTimestamp() → clock_gettime(REALTIME)
7+
//! - std.Thread.sleep(ns) → linux.nanosleep
8+
9+
const std = @import("std");
10+
const posix = std.posix;
11+
const linux = std.os.linux;
12+
const Allocator = std.mem.Allocator;
13+
14+
// Minimal stat struct matching Linux x86_64 struct stat (for fstat)
15+
const Stat = extern struct {
16+
dev: u64,
17+
ino: u64,
18+
nlink: u64,
19+
mode: u32,
20+
uid: u32,
21+
gid: u32,
22+
__pad0: u32 = 0,
23+
rdev: u64,
24+
size: i64,
25+
blksize: i64,
26+
blocks: i64,
27+
atim: linux.timespec,
28+
mtim: linux.timespec,
29+
ctim: linux.timespec,
30+
__unused: [3]i64 = .{ 0, 0, 0 },
31+
};
32+
33+
// libc fstat — std.c.fstat is void on Linux in 0.16-dev
34+
extern "c" fn fstat(fd: posix.fd_t, buf: *Stat) c_int;
35+
36+
// ── File operations (replaces removed std.fs.cwd()) ─────────────
37+
38+
pub const File = struct {
39+
fd: posix.fd_t,
40+
41+
pub fn close(self: File) void {
42+
_ = posix.system.close(self.fd);
43+
}
44+
45+
/// Read entire file contents into an allocator-owned buffer (up to max_bytes).
46+
pub fn readToEndAlloc(self: File, allocator: Allocator, max_bytes: usize) ![]u8 {
47+
// Get file size via fstat
48+
var stat_buf: Stat = undefined;
49+
const stat_rc = fstat(self.fd, &stat_buf);
50+
if (stat_rc != 0) return error.StatFailed;
51+
const size: usize = @intCast(stat_buf.size);
52+
if (size > max_bytes) return error.FileTooBig;
53+
54+
const buf = try allocator.alloc(u8, size);
55+
errdefer allocator.free(buf);
56+
57+
var total: usize = 0;
58+
while (total < size) {
59+
const n = posix.read(self.fd, buf[total..]) catch |err| switch (err) {
60+
else => return error.ReadFailed,
61+
};
62+
if (n == 0) break;
63+
total += n;
64+
}
65+
return buf[0..total];
66+
}
67+
68+
/// Read all bytes into a pre-allocated buffer. Returns number of bytes read.
69+
pub fn readAll(self: File, buf: []u8) !usize {
70+
var total: usize = 0;
71+
while (total < buf.len) {
72+
const n = posix.read(self.fd, buf[total..]) catch |err| switch (err) {
73+
else => return error.ReadFailed,
74+
};
75+
if (n == 0) break;
76+
total += n;
77+
}
78+
return total;
79+
}
80+
81+
pub fn writeAll(self: File, data: []const u8) !void {
82+
var total: usize = 0;
83+
while (total < data.len) {
84+
const rc = std.c.write(self.fd, data[total..].ptr, data.len - total);
85+
if (rc < 0) return error.WriteFailed;
86+
if (rc == 0) return error.WriteFailed;
87+
total += @intCast(rc);
88+
}
89+
}
90+
91+
/// Get file size.
92+
pub fn stat(self: File) !struct { size: usize } {
93+
var stat_buf: Stat = undefined;
94+
const stat_rc = fstat(self.fd, &stat_buf);
95+
if (stat_rc != 0) return error.StatFailed;
96+
return .{ .size = @intCast(stat_buf.size) };
97+
}
98+
};
99+
100+
/// Open a file relative to CWD. Replaces std.fs.cwd().openFile().
101+
pub fn openFile(path: []const u8, comptime _: struct {}) !File {
102+
const fd = try posix.openat(posix.AT.FDCWD, path, .{ .ACCMODE = .RDONLY }, 0);
103+
return .{ .fd = fd };
104+
}
105+
106+
/// Create a file relative to CWD. Replaces std.fs.cwd().createFile().
107+
pub fn createFile(path: []const u8, comptime _: struct {}) !File {
108+
const fd = try posix.openat(posix.AT.FDCWD, path, .{
109+
.ACCMODE = .WRONLY,
110+
.CREAT = true,
111+
.TRUNC = true,
112+
}, 0o644);
113+
return .{ .fd = fd };
114+
}
115+
116+
/// Check if a file is accessible (exists and readable).
117+
pub fn access(path: []const u8) bool {
118+
const fd = posix.openat(posix.AT.FDCWD, path, .{ .ACCMODE = .RDONLY }, 0) catch return false;
119+
_ = posix.system.close(fd);
120+
return true;
121+
}
122+
123+
/// Delete a file. Replaces std.fs.cwd().deleteFile().
124+
pub fn deleteFile(path_z: [*:0]const u8) void {
125+
_ = std.c.unlink(path_z);
126+
}
127+
128+
// ── In-memory stream (replaces removed std.io.fixedBufferStream) ──
129+
130+
/// Minimal in-memory writer that provides writeAll/writeInt/writeByte.
131+
pub const MemWriter = struct {
132+
buffer: []u8,
133+
pos: usize = 0,
134+
135+
pub fn writeAll(self: *MemWriter, data: []const u8) !void {
136+
if (self.pos + data.len > self.buffer.len) return error.NoSpaceLeft;
137+
@memcpy(self.buffer[self.pos..][0..data.len], data);
138+
self.pos += data.len;
139+
}
140+
141+
pub fn writeByte(self: *MemWriter, byte: u8) !void {
142+
if (self.pos >= self.buffer.len) return error.NoSpaceLeft;
143+
self.buffer[self.pos] = byte;
144+
self.pos += 1;
145+
}
146+
147+
pub fn writeInt(self: *MemWriter, comptime T: type, value: T, comptime endian: std.builtin.Endian) !void {
148+
const bytes = std.mem.toBytes(if (endian == .big) std.mem.nativeToBig(T, value) else std.mem.nativeToLittle(T, value));
149+
try self.writeAll(&bytes);
150+
}
151+
152+
pub fn getWritten(self: *const MemWriter) []const u8 {
153+
return self.buffer[0..self.pos];
154+
}
155+
};
156+
157+
/// Minimal in-memory reader that provides readAll/readInt/readByte.
158+
pub const MemReader = struct {
159+
buffer: []const u8,
160+
pos: usize = 0,
161+
162+
pub fn readAll(self: *MemReader, dest: []u8) !usize {
163+
const avail = self.buffer.len - self.pos;
164+
const n = @min(avail, dest.len);
165+
@memcpy(dest[0..n], self.buffer[self.pos..][0..n]);
166+
self.pos += n;
167+
return n;
168+
}
169+
170+
pub fn readByte(self: *MemReader) !u8 {
171+
if (self.pos >= self.buffer.len) return error.EndOfStream;
172+
const byte = self.buffer[self.pos];
173+
self.pos += 1;
174+
return byte;
175+
}
176+
177+
pub fn readInt(self: *MemReader, comptime T: type, comptime endian: std.builtin.Endian) !T {
178+
const size = @sizeOf(T);
179+
if (self.pos + size > self.buffer.len) return error.EndOfStream;
180+
const bytes = self.buffer[self.pos..][0..size];
181+
self.pos += size;
182+
const raw = std.mem.bytesToValue(T, bytes);
183+
return if (endian == .big) std.mem.bigToNative(T, raw) else std.mem.littleToNative(T, raw);
184+
}
185+
};
186+
187+
/// Minimal dynamic writer backed by an allocator (replaces ArrayListAligned + writer).
188+
pub const DynWriter = struct {
189+
items: []u8 = &.{},
190+
len: usize = 0,
191+
allocator: Allocator,
192+
193+
pub fn writeAll(self: *DynWriter, data: []const u8) !void {
194+
try self.ensureCapacity(self.len + data.len);
195+
@memcpy(self.items[self.len..][0..data.len], data);
196+
self.len += data.len;
197+
}
198+
199+
pub fn writeByte(self: *DynWriter, byte: u8) !void {
200+
try self.ensureCapacity(self.len + 1);
201+
self.items[self.len] = byte;
202+
self.len += 1;
203+
}
204+
205+
pub fn writeInt(self: *DynWriter, comptime T: type, value: T, comptime endian: std.builtin.Endian) !void {
206+
const bytes = std.mem.toBytes(if (endian == .big) std.mem.nativeToBig(T, value) else std.mem.nativeToLittle(T, value));
207+
try self.writeAll(&bytes);
208+
}
209+
210+
pub fn getWritten(self: *const DynWriter) []const u8 {
211+
return self.items[0..self.len];
212+
}
213+
214+
pub fn deinit(self: *DynWriter) void {
215+
if (self.items.len > 0) self.allocator.free(self.items);
216+
self.* = .{ .allocator = self.allocator };
217+
}
218+
219+
fn ensureCapacity(self: *DynWriter, needed: usize) !void {
220+
if (needed <= self.items.len) return;
221+
var new_cap = if (self.items.len == 0) @as(usize, 256) else self.items.len;
222+
while (new_cap < needed) new_cap *= 2;
223+
const new_buf = try self.allocator.alloc(u8, new_cap);
224+
if (self.len > 0) @memcpy(new_buf[0..self.len], self.items[0..self.len]);
225+
if (self.items.len > 0) self.allocator.free(self.items);
226+
self.items = new_buf;
227+
}
228+
};
229+
230+
// ── Time (replaces removed std.time.nanoTimestamp) ───────────────
231+
232+
pub fn nanoTimestamp() i128 {
233+
var ts: linux.timespec = undefined;
234+
_ = linux.clock_gettime(.REALTIME, &ts);
235+
return @as(i128, ts.sec) * std.time.ns_per_s + ts.nsec;
236+
}
237+
238+
// ── Sleep (replaces removed std.Thread.sleep) ────────────────────
239+
240+
pub fn sleepNanos(ns: u64) void {
241+
const req = linux.timespec{ .sec = @intCast(ns / std.time.ns_per_s), .nsec = @intCast(ns % std.time.ns_per_s) };
242+
_ = linux.nanosleep(&req, null);
243+
}

src/config/Config.zig

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212

1313
const std = @import("std");
1414
const Allocator = std.mem.Allocator;
15+
const compat = @import("../compat.zig");
1516
const Config = @This();
1617

1718
// ── Fields ────────────────────────────────────────────────────────
@@ -54,16 +55,13 @@ allocator: Allocator,
5455
pub fn load(allocator: Allocator) !Config {
5556
var config = Config{ .allocator = allocator };
5657

57-
const home = std.posix.getenv("HOME") orelse return config;
58+
const home = if (std.c.getenv("HOME")) |ptr| std.mem.sliceTo(ptr, 0) else return config;
5859

5960
// Build path: $HOME/.config/teru/teru.conf
6061
var path_buf: [std.fs.max_path_bytes]u8 = undefined;
6162
const path = std.fmt.bufPrint(&path_buf, "{s}/.config/teru/teru.conf", .{home}) catch return config;
6263

63-
const file = std.fs.cwd().openFile(path, .{}) catch |err| switch (err) {
64-
error.FileNotFound => return config,
65-
else => return config,
66-
};
64+
const file = compat.openFile(path, .{}) catch return config;
6765
defer file.close();
6866

6967
// Read the entire file (cap at 64KB — config files should be tiny)

src/config/Hooks.zig

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,9 @@ pub fn fire(self: *const Hooks, hook: HookEvent) void {
6666
.session_save => self.on_session_save,
6767
} orelse return;
6868

69-
const pid = posix.fork() catch return;
69+
const rc = std.os.linux.fork();
70+
const pid: isize = @bitCast(rc);
71+
if (pid < 0) return; // fork failed
7072
if (pid == 0) {
7173
// Child process: exec /bin/sh -c "<command>"
7274
const argv = [_:null]?[*:0]const u8{
@@ -75,9 +77,9 @@ pub fn fire(self: *const Hooks, hook: HookEvent) void {
7577
cmd,
7678
};
7779
const envp: [*:null]const ?[*:0]const u8 = @ptrCast(std.c.environ);
78-
posix.execveZ("/bin/sh", &argv, envp) catch {};
80+
_ = posix.system.execve("/bin/sh", &argv, @ptrCast(envp));
7981
// execve only returns on error — exit the child
80-
posix.exit(1);
82+
std.os.linux.exit(1);
8183
}
8284
// Parent: fire-and-forget — don't waitpid.
8385
// SIGCHLD with SA_NOCLDWAIT or ignoring prevents zombies.
@@ -167,5 +169,6 @@ test "fire executes command asynchronously" {
167169

168170
// Brief wait for the child to execute (fire-and-forget, so we just
169171
// verify no crash/hang). In practice the child is already done.
170-
std.Thread.sleep(10_000_000); // 10ms
172+
const req = std.os.linux.timespec{ .sec = 0, .nsec = 10_000_000 }; // 10ms
173+
_ = std.os.linux.nanosleep(&req, null);
171174
}

src/core/Multiplexer.zig

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ next_pane_id: u64,
2424

2525
pub fn init(allocator: Allocator) Multiplexer {
2626
return .{
27-
.panes = .{},
27+
.panes = .empty,
2828
.layout_engine = LayoutEngine.init(allocator),
2929
.active_workspace = 0,
3030
.allocator = allocator,

src/core/Pane.zig

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,9 +21,10 @@ pub fn init(allocator: Allocator, rows: u16, cols: u16, id: u64) !Pane {
2121
var pty = try Pty.spawn(.{ .rows = rows, .cols = cols });
2222
errdefer pty.deinit();
2323

24-
// Set PTY to non-blocking
25-
const flags = try posix.fcntl(pty.master, posix.F.GETFL, 0);
26-
_ = try posix.fcntl(pty.master, posix.F.SETFL, flags | 0x800);
24+
// Set PTY to non-blocking via C fcntl (posix.fcntl removed in 0.16)
25+
const flags = std.c.fcntl(pty.master, posix.F.GETFL);
26+
if (flags < 0) return error.FcntlFailed;
27+
_ = std.c.fcntl(pty.master, posix.F.SETFL, flags | 0x800);
2728

2829
return .{
2930
.pty = pty,

0 commit comments

Comments
 (0)