- commit
- c597d8d
- parent
- 370157c
- author
- Eric Bower
- date
- 2026-01-09 19:40:48 -0500 EST
feat(history): add `--vt` and `--html` flag The `--vt` flag will send the raw ansi escape sequences to recreate the terminal session. The idea is that this could be useful for debugging purposes. The `--html` flag was added because, why not? It's there, might as well use it.
1 files changed,
+55,
-14
+55,
-14
1@@ -257,8 +257,12 @@ const Daemon = struct {
2 client.has_pending_output = true;
3 }
4
5- pub fn handleHistory(self: *Daemon, client: *Client, term: *ghostty_vt.Terminal) !void {
6- if (serializeTerminalPlainText(self.alloc, term)) |output| {
7+ pub fn handleHistory(self: *Daemon, client: *Client, term: *ghostty_vt.Terminal, payload: []const u8) !void {
8+ const format: HistoryFormat = if (payload.len > 0)
9+ @enumFromInt(payload[0])
10+ else
11+ .plain;
12+ if (serializeTerminal(self.alloc, term, format)) |output| {
13 defer self.alloc.free(output);
14 try ipc.appendMessage(self.alloc, &client.write_buf, .History, output);
15 client.has_pending_output = true;
16@@ -313,10 +317,21 @@ pub fn main() !void {
17 };
18 return kill(&cfg, session_name);
19 } else if (std.mem.eql(u8, cmd, "history") or std.mem.eql(u8, cmd, "hi")) {
20- const session_name = args.next() orelse {
21+ var session_name: ?[]const u8 = null;
22+ var format: HistoryFormat = .plain;
23+ while (args.next()) |arg| {
24+ if (std.mem.eql(u8, arg, "--vt")) {
25+ format = .vt;
26+ } else if (std.mem.eql(u8, arg, "--html")) {
27+ format = .html;
28+ } else if (session_name == null) {
29+ session_name = arg;
30+ }
31+ }
32+ if (session_name == null) {
33 return error.SessionNameRequired;
34- };
35- return history(&cfg, session_name);
36+ }
37+ return history(&cfg, session_name.?, format);
38 } else if (std.mem.eql(u8, cmd, "attach") or std.mem.eql(u8, cmd, "a")) {
39 const session_name = args.next() orelse {
40 return error.SessionNameRequired;
41@@ -395,7 +410,7 @@ fn help() !void {
42 \\ [d]etach Detach all clients from current session (ctrl+\ for current client)
43 \\ [l]ist List active sessions
44 \\ [k]ill <name> Kill a session and all attached clients
45- \\ [hi]story <name> Output session scrollback as plain text
46+ \\ [hi]story <name> [--vt|--html] Output session scrollback (--vt or --html for escape sequences)
47 \\ [v]ersion Show version information
48 \\ [h]elp Show this help message
49 \\
50@@ -554,7 +569,13 @@ fn kill(cfg: *Cfg, session_name: []const u8) !void {
51 try w.interface.flush();
52 }
53
54-fn history(cfg: *Cfg, session_name: []const u8) !void {
55+const HistoryFormat = enum(u8) {
56+ plain = 0,
57+ vt = 1,
58+ html = 2,
59+};
60+
61+fn history(cfg: *Cfg, session_name: []const u8, format: HistoryFormat) !void {
62 var gpa = std.heap.GeneralPurposeAllocator(.{}){};
63 defer _ = gpa.deinit();
64 const alloc = gpa.allocator();
65@@ -577,7 +598,8 @@ fn history(cfg: *Cfg, session_name: []const u8) !void {
66 };
67 defer posix.close(result.fd);
68
69- ipc.send(result.fd, .History, "") catch |err| switch (err) {
70+ const format_byte = [_]u8{@intFromEnum(format)};
71+ ipc.send(result.fd, .History, &format_byte) catch |err| switch (err) {
72 error.BrokenPipe, error.ConnectionResetByPeer => return,
73 else => return err,
74 };
75@@ -1127,7 +1149,7 @@ fn daemonLoop(daemon: *Daemon, server_sock_fd: i32, pty_fd: i32) !void {
76 break :clients_loop;
77 },
78 .Info => try daemon.handleInfo(client),
79- .History => try daemon.handleHistory(client, &term),
80+ .History => try daemon.handleHistory(client, &term, msg.payload),
81 .Run => try daemon.handleRun(client, pty_fd, msg.payload),
82 .Output, .Ack => {},
83 }
84@@ -1374,16 +1396,33 @@ fn serializeTerminalState(alloc: std.mem.Allocator, term: *ghostty_vt.Terminal)
85 };
86 }
87
88-fn serializeTerminalPlainText(alloc: std.mem.Allocator, term: *ghostty_vt.Terminal) ?[]const u8 {
89+fn serializeTerminal(alloc: std.mem.Allocator, term: *ghostty_vt.Terminal, format: HistoryFormat) ?[]const u8 {
90 var builder: std.Io.Writer.Allocating = .init(alloc);
91 defer builder.deinit();
92
93- var term_formatter = ghostty_vt.formatter.TerminalFormatter.init(term, .plain);
94+ const opts: ghostty_vt.formatter.Options = switch (format) {
95+ .plain => .plain,
96+ .vt => .vt,
97+ .html => .html,
98+ };
99+ var term_formatter = ghostty_vt.formatter.TerminalFormatter.init(term, opts);
100 term_formatter.content = .{ .selection = null };
101- term_formatter.extra = .none;
102+ term_formatter.extra = switch (format) {
103+ .plain => .none,
104+ .vt => .{
105+ .palette = false,
106+ .modes = true,
107+ .scrolling_region = true,
108+ .tabstops = false,
109+ .pwd = true,
110+ .keyboard = true,
111+ .screen = .all,
112+ },
113+ .html => .styles,
114+ };
115
116 term_formatter.format(&builder.writer) catch |err| {
117- std.log.warn("failed to format terminal plain text err={s}", .{@errorName(err)});
118+ std.log.warn("failed to format terminal err={s}", .{@errorName(err)});
119 return null;
120 };
121
122@@ -1391,7 +1430,9 @@ fn serializeTerminalPlainText(alloc: std.mem.Allocator, term: *ghostty_vt.Termin
123 if (output.len == 0) return null;
124
125 return alloc.dupe(u8, output) catch |err| {
126- std.log.warn("failed to allocate terminal plain text err={s}", .{@errorName(err)});
127+ std.log.warn("failed to allocate terminal output err={s}", .{@errorName(err)});
128 return null;
129 };
130 }
131+
132+