Commit 5f6d911

Eric Bower  ·  2026-10-20 20:00:44 -0400 EDT
parent 186393f
chore: cleanup imports and prepare docs
10 files changed,  +194, -33
+1, -0
......@@ -11,6 +11,7 @@ Use spec: https://common-changelog.org/
1111
1212 ### Added
1313
14+- zmx as a zig package
1415 - Completions for [`yash`](https://magicant.github.io/yash/)
1516 - History command now accepts --screen (print only active screen) and --scrollback (num of rows to print)
1617
+39, -0
......@@ -23,6 +23,7 @@
2323 - Send commands to a session without attaching to it
2424 - Print scrollback history of a terminal session in plain text
2525 - Works on mac and linux
26+- Use zmx as a zig package
2627 - This project does **NOT** provide windows, tabs, or splits
2728
2829 ## demos
......@@ -121,6 +122,44 @@ Commands:
121122 [h]elp Show this help
122123 ```
123124
125+## using zmx as a zig package
126+
127+The library in `src/lib.zig` is importable as the `zmx` package from other Zig projects. All functions take `(alloc, io, ...)` from `std.process.Init`.
128+
129+```zig
130+// build.zig.zon
131+.dependencies = .{
132+ .zmx = .{
133+ .url = "git+https://github.com/your-org/zmx#<commit>",
134+ .hash = "...",
135+ },
136+},
137+```
138+
139+```zig
140+// build.zig
141+const zmx = b.dependency("zmx", .{ .target = target, .optimize = optimize });
142+exe_mod.addImport("zmx", zmx.module("zmx"));
143+```
144+
145+```zig
146+// your source
147+const zmx = @import("zmx");
148+
149+const exit_code = try zmx.run(gpa, io, "my-session", &.{"echo", "hi"}, .{});
150+const sessions = try zmx.list(gpa, io);
151+defer {
152+ for (sessions) |s| s.deinit(gpa);
153+ gpa.free(sessions);
154+}
155+```
156+
157+Notes:
158+
159+- `zmx.run` creates the session (and daemon) if it doesn't exist, no need to call `zmx.create` first.
160+- `zmx.create` is only available when the package is built with `-Ddaemon=true` (the default).
161+- See [`examples/`](examples/) for complete working programs.
162+
124163 ## nested sessions
125164
126165 Nested sessions are not supported. Inside a session `ZMX_SESSION` is set, and `attach` reads it: instead of creating another client it switches the calling terminal to the session you named.
+25, -15
......@@ -18,7 +18,7 @@ pub fn build(b: *std.Build) void {
1818 const version = b.option([]const u8, "version", "Version string for release") orelse
1919 @as([]const u8, build_zig_zon.version);
2020
21- const enable_daemon = b.option(bool, "daemon", "Include daemon support and Ghostty VT in libzmx") orelse true;
21+ const enable_daemon = b.option(bool, "daemon", "Include daemon support and Ghostty VT in zmx pkg") orelse true;
2222
2323 const options = b.addOptions();
2424 options.addOption([]const u8, "version", version);
......@@ -51,19 +51,7 @@ pub fn build(b: *std.Build) void {
5151 );
5252 }
5353
54- // libzmx module: programmatic interface for embedding or external tools
55- const libzmx = b.addModule("libzmx", .{
56- .root_source_file = b.path("src/lib.zig"),
57- .target = target,
58- .optimize = optimize,
59- .link_libc = true,
60- });
61- libzmx.addOptions("build_options", options);
62- if (dep) |d| {
63- libzmx.addImport("ghostty-vt", d.module("ghostty-vt"));
64- }
65-
66- // Also export as "zmx" for packages that depend on the package name
54+ // zmx module: programmatic interface for embedding or external tools
6755 const zmx_pkg_mod = b.addModule("zmx", .{
6856 .root_source_file = b.path("src/lib.zig"),
6957 .target = target,
......@@ -75,6 +63,28 @@ pub fn build(b: *std.Build) void {
7563 zmx_pkg_mod.addImport("ghostty-vt", d.module("ghostty-vt"));
7664 }
7765
66+ // Examples: build the usage examples in examples/ against the zmx module
67+ {
68+ const examples_step = b.step("examples", "Build usage examples");
69+ const Example = struct { name: []const u8, file: []const u8 };
70+ const example_sources: []const Example = &.{
71+ .{ .name = "example-run-and-wait", .file = "examples/run_and_wait.zig" },
72+ .{ .name = "example-list-sessions", .file = "examples/list_sessions.zig" },
73+ };
74+ for (example_sources) |ex| {
75+ const ex_mod = b.createModule(.{
76+ .root_source_file = b.path(ex.file),
77+ .target = target,
78+ .optimize = optimize,
79+ .link_libc = true,
80+ });
81+ ex_mod.addImport("zmx", zmx_pkg_mod);
82+ const ex_exe = b.addExecutable(.{ .name = ex.name, .root_module = ex_mod });
83+ const install = b.addInstallArtifact(ex_exe, .{});
84+ examples_step.dependOn(&install.step);
85+ }
86+ }
87+
7888 // Run
7989 {
8090 const run_step = b.step("run", "Run the app");
......@@ -137,7 +147,7 @@ pub fn build(b: *std.Build) void {
137147 } else {
138148 const lib_check = b.addLibrary(.{
139149 .name = "zmx",
140- .root_module = libzmx,
150+ .root_module = zmx_pkg_mod,
141151 .linkage = .static,
142152 });
143153 check.dependOn(&lib_check.step);
+1, -0
......@@ -13,5 +13,6 @@
1313 "build.zig",
1414 "build.zig.zon",
1515 "src",
16+ "examples",
1617 },
1718 }
+52, -0
......@@ -0,0 +1,52 @@
1+# zmx package examples
2+
3+Usage examples for the `zmx` Zig package (the programmatic interface in
4+`src/lib.zig`).
5+
6+## Building
7+
8+```sh
9+zig build examples
10+```
11+
12+Installs the example binaries to `zig-out/bin/`.
13+
14+## Examples
15+
16+- **`example-run-and-wait`** — run a command in a task-mode session, stream
17+ its output, read the scrollback, and kill the session.
18+- **`example-list-sessions`** — list active sessions with pid, exit code, and
19+ labels.
20+
21+## Using the package from another project
22+
23+```zig
24+// build.zig.zon
25+.dependencies = .{
26+ .zmx = .{
27+ .url = "git+https://github.com/you/zmx#<commit>",
28+ .hash = "...",
29+ },
30+},
31+```
32+
33+```zig
34+// build.zig
35+const zmx = b.dependency("zmx", .{ .target = target, .optimize = optimize });
36+exe_mod.addImport("zmx", zmx.module("zmx"));
37+```
38+
39+```zig
40+// your source
41+const zmx = @import("zmx");
42+const exit_code = try zmx.run(gpa, io, "my-session", &.{"echo", "hi"}, .{});
43+```
44+
45+Notes:
46+
47+- All functions take `(alloc, io, ...)` — get them from `std.process.Init`.
48+- `zmx.list()` returns a slice you must free, and each `SessionEntry` has
49+ allocated fields freed via `entry.deinit(alloc)` (see
50+ `list_sessions.zig`).
51+- `zmx.create()` is only available when the package is built with
52+ `-Ddaemon=true` (the default).
+30, -0
......@@ -0,0 +1,30 @@
1+//! Example: list active zmx sessions and print their details.
2+const std = @import("std");
3+const zmx = @import("zmx");
4+
5+pub fn main(init: std.process.Init) !void {
6+ const gpa = init.gpa;
7+ const io = init.io;
8+
9+ const sessions = try zmx.list(gpa, io);
10+ defer {
11+ for (sessions) |s| s.deinit(gpa);
12+ gpa.free(sessions);
13+ }
14+
15+ if (sessions.len == 0) {
16+ std.debug.print("no active sessions\n", .{});
17+ return;
18+ }
19+
20+ var buf: [4096]u8 = undefined;
21+ var w = std.Io.File.stdout().writer(io, &buf);
22+ for (sessions) |s| {
23+ try w.interface.print("{s}\t", .{s.name});
24+ if (s.pid) |pid| try w.interface.print("pid={d}\t", .{pid});
25+ if (s.is_error) try w.interface.print("[error]\t", .{});
26+ if (s.task_exit_code) |code| try w.interface.print("exit={d}\t", .{code});
27+ if (s.labels) |labels| try w.interface.print("{s}\t", .{labels});
28+ try w.interface.print("\n", .{});
29+ }
30+}
+32, -0
......@@ -0,0 +1,32 @@
1+//! Example: run a command in a zmx session, stream its output, then read the
2+//! scrollback and clean up.
3+const std = @import("std");
4+const zmx = @import("zmx");
5+
6+pub fn main(init: std.process.Init) !void {
7+ const gpa = init.gpa;
8+ const io = init.io;
9+
10+ // Run `echo "hello from libzmx"` in a task-mode session.
11+ // With detached = false, output is streamed to stdout while it runs.
12+ const exit_code = try zmx.run(
13+ gpa,
14+ io,
15+ "example-run",
16+ &.{ "echo", "hello from libzmx" },
17+ .{},
18+ );
19+
20+ // Read the full scrollback of the session.
21+ const scrollback = try zmx.history(gpa, io, "example-run", .plain);
22+ defer gpa.free(scrollback);
23+
24+ var buf: [4096]u8 = undefined;
25+ var w = std.Io.File.stdout().writer(io, &buf);
26+ try w.interface.print("exit code: {d}\n--- scrollback ---\n", .{exit_code});
27+ try w.interface.writeAll(scrollback);
28+ try w.interface.flush();
29+
30+ // Clean up the session.
31+ _ = try zmx.kill(gpa, io, "example-run", .{});
32+}
+3, -12
......@@ -4,17 +4,12 @@ const build_options = @import("build_options");
44 const Allocator = std.mem.Allocator;
55 const Io = std.Io;
66
7-pub const has_daemon = if (@hasDecl(build_options, "has_daemon")) build_options.has_daemon else true;
8-
9-// Publicly exported locked-in IPC wire protocol
10-pub const ipc = @import("ipc.zig");
11-
127 const client = @import("client.zig");
138 const util = @import("util.zig");
149
15-// ============================================================================
16-// Public Types
17-// ============================================================================
10+pub const has_daemon = if (@hasDecl(build_options, "has_daemon")) build_options.has_daemon else true;
11+
12+pub const ipc = @import("ipc.zig");
1813
1914 pub const HistoryFormat = util.HistoryFormat;
2015 pub const SessionEntry = util.SessionEntry;
......@@ -25,10 +20,6 @@ pub const RunOptions = client.RunOptions;
2520 pub const KillOptions = client.KillOptions;
2621 pub const KillStatus = client.KillStatus;
2722
28-// ============================================================================
29-// CLI-Mirrored Top-Level Functions
30-// ============================================================================
31-
3223 pub const connect = client.connect;
3324 pub const attach = client.attach;
3425 pub const detach = client.detach;
+2, -5
......@@ -1,9 +1,4 @@
11 const std = @import("std");
2-const build_options = @import("build_options");
3-const has_daemon = if (@hasDecl(build_options, "has_daemon")) build_options.has_daemon else true;
4-const ghostty_vt = if (has_daemon) @import("ghostty-vt") else struct {
5- pub const Terminal = struct {};
6-};
72 const ipc = @import("ipc.zig");
83 const log = @import("log.zig");
94 const util = @import("util.zig");
......@@ -16,6 +11,8 @@ const signal = @import("signal.zig");
1611 const assert = std.debug.assert;
1712 const daemonize = @import("daemonize.zig");
1813 const builtin = @import("builtin");
14+const terminal = @import("terminal.zig");
15+const ghostty_vt = terminal.ghostty_vt;
1916
2017 /// clientLoop sends ipc commands to its corresponding daemon. It uses poll() as its non-blocking
2118 /// mechanism. It will send stdin to the daemon and receive stdout from the daemon.
+9, -1
......@@ -1,7 +1,15 @@
11 const std = @import("std");
2-const ghostty_vt = @import("ghostty-vt");
2+const build_options = @import("build_options");
33 const util = @import("util.zig");
44 const testing = std.testing;
5+const has_daemon = if (@hasDecl(build_options, "has_daemon")) build_options.has_daemon else true;
6+pub const ghostty_vt = if (has_daemon) @import("ghostty-vt") else struct {
7+ pub const Terminal = struct {};
8+ pub const Parser = struct {};
9+ pub const Selection = struct {};
10+ pub const formatter = struct {};
11+ pub const color = struct {};
12+};
513
614 pub const HistoryFormat = util.HistoryFormat;
715