Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions include/ghostty.h
Original file line number Diff line number Diff line change
Expand Up @@ -858,6 +858,28 @@ typedef struct {
uint64_t len;
} ghostty_action_scrollbar_s;

// apprt.action.TmuxControl.Event
typedef enum {
GHOSTTY_TMUX_ENTER = 0,
GHOSTTY_TMUX_EXIT = 1,
GHOSTTY_TMUX_WINDOWS_CHANGED = 2,
GHOSTTY_TMUX_PANE_OUTPUT = 3,
GHOSTTY_TMUX_LAYOUT_CHANGE = 4,
GHOSTTY_TMUX_WINDOW_ADD = 5,
GHOSTTY_TMUX_WINDOW_CLOSE = 6,
GHOSTTY_TMUX_WINDOW_RENAMED = 7,
GHOSTTY_TMUX_SESSION_CHANGED = 8,
GHOSTTY_TMUX_SESSION_RENAMED = 9,
} ghostty_tmux_event_e;

// apprt.action.TmuxControl.C
typedef struct {
ghostty_tmux_event_e event;
uint32_t id;
const uint8_t *data;
uintptr_t data_len;
} ghostty_action_tmux_control_s;

// apprt.Action.Key
typedef enum {
GHOSTTY_ACTION_QUIT,
Expand Down Expand Up @@ -924,6 +946,7 @@ typedef enum {
GHOSTTY_ACTION_SEARCH_SELECTED,
GHOSTTY_ACTION_READONLY,
GHOSTTY_ACTION_COPY_TITLE_TO_CLIPBOARD,
GHOSTTY_ACTION_TMUX_CONTROL,
} ghostty_action_tag_e;

typedef union {
Expand Down Expand Up @@ -964,6 +987,7 @@ typedef union {
ghostty_action_search_total_s search_total;
ghostty_action_search_selected_s search_selected;
ghostty_action_readonly_e readonly;
ghostty_action_tmux_control_s tmux_control;
} ghostty_action_u;

typedef struct {
Expand Down
13 changes: 13 additions & 0 deletions src/Surface.zig
Original file line number Diff line number Diff line change
Expand Up @@ -1181,6 +1181,19 @@ pub fn handleMessage(self: *Surface, msg: Message) !void {
.{ .selected = v },
);
},

.tmux_control => |v| {
defer v.data.deinit();
_ = try self.rt_app.performAction(
.{ .surface = self },
.tmux_control,
.{
.event = v.event,
.id = v.id,
.data = v.data.slice(),
},
);
},
}
}

Expand Down
52 changes: 52 additions & 0 deletions src/apprt/action.zig
Original file line number Diff line number Diff line change
Expand Up @@ -340,6 +340,11 @@ pub const Action = union(Key) {
/// otherwise the terminal-set title.
copy_title_to_clipboard,

/// A tmux control mode event from the Viewer. The embedder uses
/// this to create/destroy native windows and panes that mirror
/// the tmux server state.
tmux_control: TmuxControl,
Comment on lines +343 to +346
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- Action.Key excerpt ---'
sed -n '343,418p' src/apprt/action.zig

printf '\n%s\n' '--- GTK performAction excerpt ---'
sed -n '661,768p' src/apprt/gtk/class/application.zig

printf '\n%s\n' '--- tmux_control references under src/apprt ---'
rg -n '\.tmux_control\b|tmux_control' src/apprt

Repository: manaflow-ai/ghostty

Length of output: 6315


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Get the full performAction function
printf '%s\n' '--- Full performAction function ---'
rg -A 200 'pub fn performAction' src/apprt/gtk/class/application.zig | head -250

# Check if .tmux_control is handled anywhere in the switch
printf '\n%s\n' '--- Check for tmux_control handling in application.zig ---'
grep -n 'tmux_control' src/apprt/gtk/class/application.zig || echo "No tmux_control found"

# Check the exact line count of the file
printf '\n%s\n' '--- Line count of application.zig ---'
wc -l src/apprt/gtk/class/application.zig

Repository: manaflow-ai/ghostty

Length of output: 7328


Add .tmux_control to the exhaustive switch in src/apprt/gtk/class/application.zig:661-768 before merging.

The performAction function uses a comptime exhaustive switch. Adding the .tmux_control enum variant to Action.Key without handling it in this switch will cause a compile error. Either add an explicit handler or include it in the unimplemented catch-all case at the end of the switch.

The switch currently lists unimplemented cases like .secure_input, .close_all_windows, etc., but .tmux_control is missing entirely.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/apprt/action.zig` around lines 343 - 346, The enum variant .tmux_control
was added to Action.Key but performAction's comptime exhaustive switch in
function performAction does not handle it, which will break compilation; update
the exhaustive switch to handle the new variant by adding a case for
.tmux_control that either implements the intended behavior (using the
TmuxControl payload) or forwards to the existing unimplemented handler, or
include .tmux_control in the catch-all unimplemented branch at the end of the
switch so the switch remains exhaustive; look for the performAction function and
the Action.Key switch and add the .tmux_control arm (or route it to the
unimplemented logic) accordingly.


/// Sync with: ghostty_action_tag_e
pub const Key = enum(c_int) {
quit,
Expand Down Expand Up @@ -406,6 +411,7 @@ pub const Action = union(Key) {
search_selected,
readonly,
copy_title_to_clipboard,
tmux_control,

test "ghostty.h Action.Key" {
try lib.checkGhosttyHEnum(Key, "GHOSTTY_ACTION_");
Expand Down Expand Up @@ -998,6 +1004,52 @@ pub const SearchSelected = struct {
}
};

/// Tmux control mode event from the Viewer state machine.
/// The embedder receives these to manage native windows/panes
/// that mirror the tmux server state.
pub const TmuxControl = struct {
event: Event,
/// Contextual ID: pane_id for pane_output, window_id for window_*
/// and layout_change events. Unused for enter/exit/session events.
id: u32 = 0,
data: []const u8 = &.{},

pub const Event = enum(c_int) {
enter = 0,
exit = 1,
windows_changed = 2,
pane_output = 3,
layout_change = 4,
window_add = 5,
window_close = 6,
window_renamed = 7,
session_changed = 8,
session_renamed = 9,

// Sync with: ghostty_tmux_event_e
test "ghostty.h TmuxControl.Event" {
try lib.checkGhosttyHEnum(Event, "GHOSTTY_TMUX_");
}
};

// Sync with: ghostty_action_tmux_control_s
pub const C = extern struct {
Comment on lines +1030 to +1036
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Six C API event types are defined but never emitted

GHOSTTY_TMUX_LAYOUT_CHANGE, GHOSTTY_TMUX_WINDOW_ADD, GHOSTTY_TMUX_WINDOW_CLOSE, GHOSTTY_TMUX_WINDOW_RENAMED, GHOSTTY_TMUX_SESSION_CHANGED, and GHOSTTY_TMUX_SESSION_RENAMED appear in ghostty_tmux_event_e and in the Event enum here, but no code path in stream_handler.zig currently emits them (only exit, windows_changed, and pane_output are wired). An embedder author who writes a handler for e.g. GHOSTTY_TMUX_LAYOUT_CHANGE will never receive it.

If these are intentionally reserved for a future PR, it would help to add a comment noting their unimplemented status (e.g., // Not yet emitted – reserved for future use) so consumers aren't silently confused.

event: Event,
id: u32,
data: [*]const u8,
data_len: usize,
Comment on lines +1011 to +1040
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major

Widen TmuxControl.id before freezing this ABI.

Internal tmux pane/window ids are still usize, but the new public payload fixes them to u32. That bakes a downcast into every emitter on 64-bit builds. Since this C ABI is brand new, prefer u64/uint64_t here (and in ghostty_action_tmux_control_s) before consumers depend on the narrower shape.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/apprt/action.zig` around lines 1011 - 1040, The public ABI fixes
TmuxControl IDs to u32 which forces downcasts on 64-bit hosts; change the id
field types to u64: update the top-level id default (currently "id: u32 = 0") to
"id: u64 = 0" and the extern C struct (pub const C) id from u32 to u64, and also
update the corresponding C definition (ghostty_action_tmux_control_s /
ghostty.h) to use uint64_t so the Zig Event/C ABI matches 64-bit internal
pane/window ids without implicit truncation.

};

pub fn cval(self: TmuxControl) C {
return .{
.event = self.event,
.id = self.id,
.data = self.data.ptr,
.data_len = self.data.len,
};
}
};

test {
_ = std.testing.refAllDeclsRecursive(@This());
}
16 changes: 16 additions & 0 deletions src/apprt/surface.zig
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
const std = @import("std");
const Allocator = std.mem.Allocator;

const action = @import("action.zig");
const apprt = @import("../apprt.zig");
const build_config = @import("../build_config.zig");
const App = @import("../App.zig");
Expand Down Expand Up @@ -108,6 +109,21 @@ pub const Message = union(enum) {
/// Selected search index change
search_selected: ?usize,

/// A tmux control mode event from the Viewer. Carries the event type
/// and associated data from the I/O thread to the app thread, where
/// it will be converted to an action for the embedder.
tmux_control: TmuxControlMsg,

pub const TmuxControlMsg = struct {
event: action.TmuxControl.Event,
/// Contextual ID: pane_id for pane_output, window_id for window_*
/// and layout_change events. Unused for enter/exit/session events.
id: u32 = 0,
/// Event-specific data. Uses WriteReq for safe cross-thread transfer
/// (data is either inline or heap-allocated, never a borrowed pointer).
data: WriteReq = .{ .small = .{ .data = undefined, .len = 0 } },
};

pub const ReportTitleStyle = enum {
csi_21_t,

Expand Down
37 changes: 37 additions & 0 deletions src/terminal/tmux/layout.zig
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,43 @@ pub const Layout = struct {
vertical: []const Layout,
};

/// Custom JSON serialization: flattens width/height/x/y + content
/// into a single JSON object (e.g. {"width":80,"height":24,"x":0,"y":0,"pane":0}).
pub fn jsonStringify(self: Layout, jw: anytype) !void {
try jw.beginObject();
try jw.objectField("width");
try jw.write(self.width);
try jw.objectField("height");
try jw.write(self.height);
try jw.objectField("x");
try jw.write(self.x);
try jw.objectField("y");
try jw.write(self.y);
switch (self.content) {
.pane => |id| {
try jw.objectField("pane");
try jw.write(id);
},
.horizontal => |children| {
try jw.objectField("horizontal");
try jw.beginArray();
for (children) |child| {
try child.jsonStringify(jw);
}
try jw.endArray();
},
.vertical => |children| {
try jw.objectField("vertical");
try jw.beginArray();
for (children) |child| {
try child.jsonStringify(jw);
}
try jw.endArray();
},
}
try jw.endObject();
}

pub const ParseError = Allocator.Error || error{SyntaxError};

/// Parse a layout string that includes a 4-character checksum prefix.
Expand Down
50 changes: 41 additions & 9 deletions src/terminal/tmux/viewer.zig
Original file line number Diff line number Diff line change
Expand Up @@ -211,6 +211,13 @@ pub const Viewer = struct {
/// never reuses window IDs within a server process lifetime.
windows: []const Window,

/// Pane output data. The embedder should feed this data into its
/// own virtual terminal surface for the given pane. The Viewer
/// also feeds this data into its internal Terminal, so both the
/// Viewer's canonical state and the embedder's rendering surface
/// receive the same output stream.
pane_output: PaneOutput,

pub fn format(self: Action, writer: *std.Io.Writer) !void {
const T = Action;
const info = @typeInfo(T).@"union";
Expand Down Expand Up @@ -253,6 +260,11 @@ pub const Viewer = struct {
}
};

pub const PaneOutput = struct {
pane_id: usize,
data: []const u8,
};

pub const Pane = struct {
terminal: Terminal,

Expand Down Expand Up @@ -459,14 +471,29 @@ pub const Viewer = struct {
command_consumed = true;
},

.output => |out| self.receivedOutput(
out.pane_id,
out.data,
) catch |err| {
log.warn(
"failed to process output for pane id={}: {}",
.{ out.pane_id, err },
);
.output => |out| {
// Feed output into the Viewer's internal Terminal (canonical state).
self.receivedOutput(
out.pane_id,
out.data,
) catch |err| {
log.warn(
"failed to process output for pane id={}: {}",
.{ out.pane_id, err },
);
};

// Also emit a pane_output action for the embedder, but only
// for tracked panes. Untracked pane output is silently dropped
// by receivedOutput above and should not be forwarded.
if (self.panes.contains(out.pane_id)) {
var arena = self.action_arena.promote(self.alloc);
defer self.action_arena = arena.state;
actions.append(
arena.allocator(),
.{ .pane_output = .{ .pane_id = out.pane_id, .data = out.data } },
) catch return self.defunct();
Comment on lines +489 to +495
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 @intCast(out.pane_id) silently truncates large pane IDs in ReleaseFast builds

out.pane_id is usize (64-bit on 64-bit targets) while TmuxControlMsg.id is u32. @intCast panics on overflow in debug/safe builds, but in ReleaseFast / ReleaseSmall mode the behaviour is undefined. While tmux pane IDs rarely exceed 32-bit range in practice, the cast is also applied downstream in stream_handler.zig at line 471 (@intCast(out.pane_id)), making the same assumption in two places.

Consider asserting or saturating the cast, or widening id to usize/u64 to match the viewer's type.

Comment on lines +486 to +495
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Don't forward pane_output until pane bootstrap completes.

self.panes.contains(out.pane_id) only means the pane exists in the map. syncLayouts() inserts new panes before the capture-pane/bootstrap commands finish, so live %output can be forwarded ahead of the initial snapshot. That will make embedders render duplicated or out-of-order content on busy panes.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/terminal/tmux/viewer.zig` around lines 486 - 495, The code forwards
pane_output for any pane present in self.panes, but syncLayouts() can insert
panes before their capture-pane/bootstrap completes; change the condition that
appends .pane_output so it only forwards output for panes whose bootstrap has
completed (e.g., check a bootstrap-complete flag or set, such as a
Pane.bootstrap_complete boolean or a separate self.bootstrapped set) instead of
just self.panes.contains(out.pane_id); update the condition around
actions.append in the block with self.action_arena/promote so it requires the
bootstrap check (and ensure any new bookkeeping is updated when
capture-pane/bootstrap finishes).

}
},

// Session changed means we switched to a different tmux session.
Expand Down Expand Up @@ -1767,7 +1794,12 @@ test "initial flow" {
.input = .{ .tmux = .{ .output = .{ .pane_id = 0, .data = "new output" } } },
.check = (struct {
fn check(v: *Viewer, actions: []const Viewer.Action) anyerror!void {
try testing.expectEqual(0, actions.len);
// Expect .pane_output action for tracked pane
try testing.expectEqual(1, actions.len);
try testing.expect(actions[0] == .pane_output);
try testing.expectEqual(0, actions[0].pane_output.pane_id);
try testing.expectEqualStrings("new output", actions[0].pane_output.data);
// Also verify the internal terminal received the data
const pane: *Viewer.Pane = v.panes.getEntry(0).?.value_ptr;
const screen: *Screen = pane.terminal.screens.active;
const str = try screen.dumpStringAlloc(
Expand Down
85 changes: 79 additions & 6 deletions src/termio/stream_handler.zig
Original file line number Diff line number Diff line change
Expand Up @@ -428,10 +428,11 @@ pub const StreamHandler = struct {
log.info("tmux viewer action={f}", .{action});
switch (action) {
.exit => {
// We ignore this because we will fully exit when
// our DCS connection ends. We may want to handle
// this in the future to notify our GUI we're
// disconnected though.
self.surfaceMessageWriter(.{
.tmux_control = .{
.event = .exit,
},
});
Comment on lines +431 to +435
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Normal tmux enter/exit still never reaches the embedder.

Lines 431-435 only run when viewer.next() returns .exit from the defunct/error path. The real control-mode .enter/.exit notifications are still consumed earlier in dcsCommand, so embedders miss the clean lifecycle transitions this API now advertises.

Possible fix
                     .enter => {
                         // Setup our viewer state
                         assert(self.tmux_viewer == null);
                         const viewer = try self.alloc.create(terminal.tmux.Viewer);
                         errdefer self.alloc.destroy(viewer);
                         viewer.* = try .init(self.alloc);
                         errdefer viewer.deinit();
                         self.tmux_viewer = viewer;
+                        self.surfaceMessageWriter(.{
+                            .tmux_control = .{ .event = .enter },
+                        });
                         break :tmux;
                     },

                     .exit => {
+                        self.surfaceMessageWriter(.{
+                            .tmux_control = .{ .event = .exit },
+                        });
                         // Free our viewer state if we have one
                         if (self.tmux_viewer) |viewer| {
                             viewer.deinit();
                             self.alloc.destroy(viewer);
                             self.tmux_viewer = null;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/termio/stream_handler.zig` around lines 431 - 435, The embedder never
receives real tmux control-mode enter/exit because those events are consumed
inside dcsCommand instead of being forwarded; update dcsCommand (or its callers)
so that when it detects control-mode .enter or .exit it invokes
self.surfaceMessageWriter with {.tmux_control = .{ .event = .enter }} and
{.tmux_control = .{ .event = .exit }} (or returns a sentinel that viewer.next()
propagates) rather than swallowing the event, ensuring the same tmux_control
enter/exit notifications produced in the viewer.next()/viewer.next() defunct
path are emitted for clean lifecycle transitions.

},

.command => |command| {
Expand All @@ -443,8 +444,37 @@ pub const StreamHandler = struct {
));
},

.windows => {
// TODO
.windows => |windows| {
const json = serializeTmuxWindows(
Copy link
Copy Markdown

@cubic-dev-ai cubic-dev-ai Bot Mar 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: Caller-owned JSON buffer from serializeTmuxWindows is never freed after WriteReq.init, causing repeated leaks on tmux windows updates.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/termio/stream_handler.zig, line 448:

<comment>Caller-owned JSON buffer from `serializeTmuxWindows` is never freed after `WriteReq.init`, causing repeated leaks on tmux windows updates.</comment>

<file context>
@@ -443,8 +444,37 @@ pub const StreamHandler = struct {
-                        .windows => {
-                            // TODO
+                        .windows => |windows| {
+                            const json = serializeTmuxWindows(
+                                self.alloc,
+                                viewer,
</file context>
Fix with Cubic

self.alloc,
viewer,
windows,
) catch |err| {
log.warn("failed to serialize tmux windows: {}", .{err});
break :tmux;
};
self.surfaceMessageWriter(.{
.tmux_control = .{
.event = .windows_changed,
.data = try apprt.surface.Message.WriteReq.init(
self.alloc,
json,
),
},
});
Comment on lines +447 to +464
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Free the temporary windows JSON after WriteReq.init().

The helper at Lines 1583-1585 returns caller-owned memory, and WriteReq.init() copies those bytes into message-owned storage. As written, every .windows_changed event leaks the serialized JSON buffer.

Possible fix
                         .windows => |windows| {
                             const json = serializeTmuxWindows(
                                 self.alloc,
                                 viewer,
                                 windows,
                             ) catch |err| {
                                 log.warn("failed to serialize tmux windows: {}", .{err});
                                 break :tmux;
                             };
+                            defer self.alloc.free(json);
                             self.surfaceMessageWriter(.{
                                 .tmux_control = .{
                                     .event = .windows_changed,
                                     .data = try apprt.surface.Message.WriteReq.init(
                                         self.alloc,
                                         json,
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
.windows => |windows| {
const json = serializeTmuxWindows(
self.alloc,
viewer,
windows,
) catch |err| {
log.warn("failed to serialize tmux windows: {}", .{err});
break :tmux;
};
self.surfaceMessageWriter(.{
.tmux_control = .{
.event = .windows_changed,
.data = try apprt.surface.Message.WriteReq.init(
self.alloc,
json,
),
},
});
.windows => |windows| {
const json = serializeTmuxWindows(
self.alloc,
viewer,
windows,
) catch |err| {
log.warn("failed to serialize tmux windows: {}", .{err});
break :tmux;
};
defer self.alloc.free(json);
self.surfaceMessageWriter(.{
.tmux_control = .{
.event = .windows_changed,
.data = try apprt.surface.Message.WriteReq.init(
self.alloc,
json,
),
},
});
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/termio/stream_handler.zig` around lines 447 - 464, The serialized tmux
windows JSON returned by serializeTmuxWindows is caller-owned and currently
leaked after calling apprt.surface.Message.WriteReq.init; update the .windows
branch so that after successfully creating the WriteReq
(apprt.surface.Message.WriteReq.init) you free the temporary JSON buffer (the
value returned by serializeTmuxWindows) using the allocator (or use a defer that
frees json after init succeeds), ensuring you free json regardless of early
returns or errors and before calling self.surfaceMessageWriter with the
.tmux_control .windows_changed message.

},
Comment on lines +447 to +465
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Memory leak: json allocation never freed

serializeTmuxWindows returns a heap-allocated slice owned by self.alloc (via std.json.Stringify.valueAlloc). WriteReq.init always copies the data — either into its small inline buffer or via alloc.dupe — it never takes ownership of the original slice. As a result, the original json allocation is leaked every time a windows_changed event fires.

Additionally, if WriteReq.init returns error.OutOfMemory, the try propagates the error upward while json is still allocated, and there is no errdefer to clean it up.

The fix is to always free json after WriteReq.init returns:

.windows => |windows| {
    const json = serializeTmuxWindows(
        self.alloc,
        viewer,
        windows,
    ) catch |err| {
        log.warn("failed to serialize tmux windows: {}", .{err});
        break :tmux;
    };
    defer self.alloc.free(json);
    self.surfaceMessageWriter(.{
        .tmux_control = .{
            .event = .windows_changed,
            .data = try apprt.surface.Message.WriteReq.init(
                self.alloc,
                json,
            ),
        },
    });
},


.pane_output => |out| {
self.surfaceMessageWriter(.{
.tmux_control = .{
.event = .pane_output,
.id = @intCast(out.pane_id),
.data = try apprt.surface.Message.WriteReq.init(
self.alloc,
out.data,
),
},
});
},
}
}
Expand Down Expand Up @@ -1549,3 +1579,46 @@ pub const StreamHandler = struct {
self.surfaceMessageWriter(.{ .progress_report = report });
}
};

/// Serialize tmux Viewer windows state to JSON for the embedder.
/// Produces: {"session_id":N,"tmux_version":"X.Y","windows":[...]}
/// Caller owns the returned slice and must free it with `alloc`.
fn serializeTmuxWindows(
alloc: Allocator,
viewer: *const terminal.tmux.Viewer,
windows: []const terminal.tmux.Viewer.Window,
) Allocator.Error![]const u8 {
const WindowJson = struct {
id: usize,
width: usize,
height: usize,
layout: terminal.tmux.Layout,
};

const Payload = struct {
session_id: usize,
tmux_version: []const u8,
windows: []const WindowJson,
};

// Build the JSON-friendly window array. We use a temporary allocation
// for the wrapper structs (they reference Layout by value which has
// jsonStringify for custom serialization).
const json_windows = try alloc.alloc(WindowJson, windows.len);
defer alloc.free(json_windows);

for (windows, 0..) |win, i| {
json_windows[i] = .{
.id = win.id,
.width = win.width,
.height = win.height,
.layout = win.layout,
};
}

return std.json.Stringify.valueAlloc(alloc, Payload{
.session_id = viewer.session_id,
.tmux_version = viewer.tmux_version,
.windows = json_windows,
}, .{});
}
Loading