1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
|
//! Centralized terminal color / style helpers for the TUI.
//!
//! Today the CLI emits raw ANSI escapes inline (e.g. `\x1b[2m` for dim,
//! `\x1b[36m` for tool/cyan). This module collects those into one named
//! palette so components don't hand-roll escapes. This is *centralization*,
//! not a configurable theming system — the palette is fixed at comptime.
//!
//! Ergonomic shape (mirrors pi's `fg(name, text)` / `bg(name, text)`):
//! - `Theme.fg(name)` / `Theme.bg(name)` return a `Style` value.
//! - `Style.open()` / `Style.close()` return the raw escape strings, for
//! callers that build lines manually (the common TUI case, where a line is
//! assembled into a buffer).
//! - `Style.wrap(alloc, text)` returns an allocator-owned
//! `open ++ text ++ close` string, for convenience.
//! - `Style.write(writer, text)` writes `open ++ text ++ close` into a
//! `*std.Io.Writer`, for streaming callers.
//!
//! All escapes are static strings, so `open()`/`close()` never allocate and
//! never fail. Only `wrap` allocates.
const std = @import("std");
/// Reset-all sequence. Closing any style emits this.
pub const reset = "\x1b[0m";
/// Named styles the P1 components need. Each maps to a fixed ANSI open
/// sequence; the close is always `reset`.
pub const StyleName = enum {
/// Dimmed text — used for thinking blocks and status/retry lines.
dim,
/// Assistant message body. Plain (no escape); present so call sites can
/// be explicit rather than emitting nothing.
assistant,
/// User-entered text.
user,
/// Tool invocation prefix / cyan accent.
tool,
/// Footer / chrome line.
footer,
/// The virtual cursor: reverse video block.
cursor,
/// Error / retry text.
err,
};
/// A resolved style: the opening escape and (implicitly) the `reset` close.
pub const Style = struct {
open_seq: []const u8,
/// When true the style is "empty" (no visible escape); `open()` returns
/// "" and `close()` returns "" so wrapping plain text is a no-op.
is_plain: bool = false,
pub fn open(self: Style) []const u8 {
return self.open_seq;
}
pub fn close(self: Style) []const u8 {
return if (self.is_plain) "" else reset;
}
/// Allocate `open ++ text ++ close`. Caller owns the returned slice.
pub fn wrap(self: Style, alloc: std.mem.Allocator, text: []const u8) ![]u8 {
return std.fmt.allocPrint(alloc, "{s}{s}{s}", .{ self.open(), text, self.close() });
}
/// Write `open ++ text ++ close` into `writer`.
pub fn write(self: Style, writer: *std.Io.Writer, text: []const u8) !void {
try writer.writeAll(self.open());
try writer.writeAll(text);
try writer.writeAll(self.close());
}
};
/// The (fixed) theme. A value type so the engine can hold one by value.
pub const Theme = struct {
/// Resolve a foreground style by name.
pub fn fg(self: Theme, name: StyleName) Style {
_ = self;
return styleFor(name);
}
/// Resolve a background-oriented style by name. For this fixed palette the
/// only background-style use is `cursor` (reverse video already covers
/// fg+bg); other names fall back to the same escape as `fg`. Kept as a
/// distinct entry point so future call sites read clearly and a richer
/// palette can diverge later without churn.
pub fn bg(self: Theme, name: StyleName) Style {
_ = self;
return styleFor(name);
}
};
/// The single shared theme instance.
pub const default: Theme = .{};
fn styleFor(name: StyleName) Style {
return switch (name) {
.dim => .{ .open_seq = "\x1b[2m" },
// Plain assistant text: no escape, no reset.
.assistant => .{ .open_seq = "", .is_plain = true },
.user => .{ .open_seq = "\x1b[39m" },
.tool => .{ .open_seq = "\x1b[36m" },
.footer => .{ .open_seq = "\x1b[2m" },
// Reverse video — the virtual cursor block.
.cursor => .{ .open_seq = "\x1b[7m" },
.err => .{ .open_seq = "\x1b[31m" },
};
}
test "open/close are static and reversible" {
const t = default;
const dim = t.fg(.dim);
try std.testing.expectEqualStrings("\x1b[2m", dim.open());
try std.testing.expectEqualStrings(reset, dim.close());
}
test "plain assistant style emits nothing" {
const t = default;
const a = t.fg(.assistant);
try std.testing.expectEqualStrings("", a.open());
try std.testing.expectEqualStrings("", a.close());
}
test "wrap allocates open++text++close" {
const t = default;
const s = try t.fg(.tool).wrap(std.testing.allocator, "read");
defer std.testing.allocator.free(s);
try std.testing.expectEqualStrings("\x1b[36mread\x1b[0m", s);
}
test "write streams open++text++close" {
var buf: [64]u8 = undefined;
var w = std.Io.Writer.fixed(&buf);
try default.fg(.cursor).write(&w, "X");
try std.testing.expectEqualStrings("\x1b[7mX\x1b[0m", w.buffered());
}
|