mirror of
https://github.com/ghostty-org/ghostty.git
synced 2026-08-05 15:18:40 +00:00
terminal: add stream continuation tracking for replay
This adds opt-in continuation tracking to `terminal.Stream` that allows
any caller to call `writeContinuation` in order to get the minimum bytes
necessary from a grounded parser state to the identical state.
This enables reliable stream restart across serialization states, which
could be used for local restart, networked terminals, etc. For me, this
is used for multiplexers. :)
## Implementation
The implementation of this was really carefully done to avoid any
negative performance impact particularly when continuation tracking is
_off_.
The way this work is simple:
1. ESC is the only char that leaves the ground state and most
ESC sequences are short. So if we're in a non-ground state, we
do a backwards vectorized search to find the last `ESC` in the
input slice. If one doesn't exist, we assume we found it previously
and store the whole slice (rare, since ESC sequences are usually
short like I said).
2. If we're in the ground state that means we only have a potential
incomplete UTF-8 codepoint, so we find the lead UTF-8 byte.
3. When writing, we normalize the suffix to drop things like BEL
commands that would've already been handled to avoid
double-calling.
## Performance
Via `ghostty-bench +terminal-stream`
Corpus main tracking off tracking on
plain ASCII (256 MiB) 175.4ms 175.8ms 175.5ms
UTF-8 (32 MiB) 268.4ms 270.0ms 269.9ms
5% invalid UTF-8 (32 MiB) 316.4ms 317.8ms 320.2ms
CSI-heavy (32 MiB) 145.0ms 146.3ms 145.9ms
OSC (32 MiB) 1621.0ms 1627.5ms 1638.3ms
Kitty APC (128 MiB) 95.5ms 96.9ms 96.3ms
mixed traffic (32 MiB) 172.4ms 172.0ms 172.3ms
giant APC (128 MiB) 38.3ms 38.3ms 40.8ms
This commit is contained in:
@@ -71,7 +71,10 @@ pub fn create(
|
||||
errdefer alloc.destroy(ptr);
|
||||
ptr.* = .{
|
||||
.opts = opts,
|
||||
.stream = .init(.{ .alloc = alloc }),
|
||||
.stream = .init(.{
|
||||
.allocator = alloc,
|
||||
.handler = .{ .alloc = alloc },
|
||||
}),
|
||||
};
|
||||
return ptr;
|
||||
}
|
||||
|
||||
@@ -37,7 +37,13 @@ pub const Options = struct {
|
||||
@"terminal-rows": u16 = 80,
|
||||
@"terminal-cols": u16 = 120,
|
||||
|
||||
/// The data to read as a filepath. If this is "-" then
|
||||
/// Enable opt-in continuation tracking on the stream.
|
||||
@"continuation-enabled": bool = false,
|
||||
|
||||
/// Maximum continuation suffix retained when tracking is enabled.
|
||||
@"continuation-max-bytes": usize = 1024 * 1024,
|
||||
|
||||
/// Pre-generated data from ghostty-gen. If this is "-" then
|
||||
/// we will read stdin. If this is unset, then we will
|
||||
/// do nothing (benchmark is a noop). It'd be more unixy to
|
||||
/// use stdin by default but I find that a hanging CLI command
|
||||
@@ -61,7 +67,15 @@ pub fn create(
|
||||
}),
|
||||
.stream = undefined,
|
||||
};
|
||||
ptr.stream = .initAlloc(alloc, .init(&ptr.terminal));
|
||||
errdefer ptr.terminal.deinit(alloc);
|
||||
ptr.stream = .init(.{
|
||||
.allocator = alloc,
|
||||
.handler = .init(&ptr.terminal),
|
||||
.continuation_max_bytes = if (opts.@"continuation-enabled")
|
||||
opts.@"continuation-max-bytes"
|
||||
else
|
||||
null,
|
||||
});
|
||||
|
||||
return ptr;
|
||||
}
|
||||
@@ -86,8 +100,8 @@ fn setup(ptr: *anyopaque) Benchmark.Error!void {
|
||||
// Always reset our terminal state
|
||||
self.terminal.fullReset();
|
||||
|
||||
// Open our data file to prepare for reading. We can do more
|
||||
// validation here eventually.
|
||||
// Open our data file to prepare for reading. We can do more validation
|
||||
// here eventually.
|
||||
assert(self.data_f == null);
|
||||
self.data_f = options.dataFile(self.opts.data) catch |err| {
|
||||
log.warn("error opening data file err={}", .{err});
|
||||
@@ -142,4 +156,12 @@ test TerminalStream {
|
||||
|
||||
const bench = impl.benchmark();
|
||||
_ = try bench.run(.once);
|
||||
|
||||
const tracked: *TerminalStream = try .create(alloc, .{
|
||||
.@"continuation-enabled" = true,
|
||||
});
|
||||
defer tracked.destroy(alloc);
|
||||
|
||||
const tracked_bench = tracked.benchmark();
|
||||
_ = try tracked_bench.run(.once);
|
||||
}
|
||||
|
||||
@@ -30,7 +30,10 @@ pub const Stream = struct {
|
||||
|
||||
return .{
|
||||
.events = events,
|
||||
.parser_stream = .initAlloc(alloc, handler),
|
||||
.parser_stream = .init(.{
|
||||
.allocator = alloc,
|
||||
.handler = handler,
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -371,7 +371,10 @@ pub fn deinit(self: *Terminal, alloc: Allocator) void {
|
||||
/// for handling escape sequences split across write boundaries), you
|
||||
/// must store and reuse the returned stream.
|
||||
pub fn vtStream(self: *Terminal) Stream {
|
||||
return .initAlloc(self.gpa(), self.vtHandler());
|
||||
return Stream.init(.{
|
||||
.allocator = self.gpa(),
|
||||
.handler = self.vtHandler(),
|
||||
});
|
||||
}
|
||||
|
||||
/// This is the handler-side only for vtStream.
|
||||
|
||||
@@ -457,7 +457,10 @@ fn new_(
|
||||
.terminal = t,
|
||||
.io_impl = io_impl,
|
||||
.tmp_dir_path = undefined, // Only used if temporary directory is set with API calls
|
||||
.stream = .initAlloc(alloc, handler),
|
||||
.stream = Stream.init(.{
|
||||
.allocator = alloc,
|
||||
.handler = handler,
|
||||
}),
|
||||
};
|
||||
|
||||
return wrapper;
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
540
src/terminal/stream_continuation.zig
Normal file
540
src/terminal/stream_continuation.zig
Normal file
@@ -0,0 +1,540 @@
|
||||
const std = @import("std");
|
||||
const assert = @import("../quirks.zig").inlineAssert;
|
||||
const Allocator = std.mem.Allocator;
|
||||
const Parser = @import("Parser.zig");
|
||||
const UTF8Decoder = @import("UTF8Decoder.zig");
|
||||
|
||||
/// Retains the input needed to reconstruct unfinished Stream parser state.
|
||||
///
|
||||
/// A feed is one chunk of bytes given to a Stream. It can end in the middle of
|
||||
/// a VT sequence or a UTF-8 codepoint. To continue in another Stream, that
|
||||
/// Stream starts from ground (with no sequence in progress) and reads the
|
||||
/// saved bytes again. The first saved byte is called the replay start.
|
||||
///
|
||||
/// Stream updates this tracker once per feed based on where that replay start
|
||||
/// is:
|
||||
///
|
||||
/// - Ground (parser and UTF-8 decoder): `reset`, no suffix is needed.
|
||||
/// - Anything unfinished: `append`, which replaces the suffix with the
|
||||
/// bytes from the replay start onward when that start is inside this
|
||||
/// feed, and otherwise extends the suffix with this whole feed because
|
||||
/// the unfinished state began in an earlier one.
|
||||
///
|
||||
/// This keeps the retained bytes minimal: they always begin at the replay
|
||||
/// start, so the feed path never needs to parse or trim old input. The suffix
|
||||
/// may still contain a byte whose visible terminal effect already happened,
|
||||
/// such as BEL inside an unfinished CSI sequence. `write` leaves out those
|
||||
/// bytes so replay does not perform the same effect twice.
|
||||
///
|
||||
/// If the suffix exceeds `max_bytes`, or retaining it fails, `broken`
|
||||
/// is set until a later feed ends at ground (`reset`) or contains a new
|
||||
/// replay start (`replace`), both of which need nothing that was lost.
|
||||
///
|
||||
/// Replay semantics are guaranteed for the standard TerminalStream handler.
|
||||
/// Custom handlers, including handlers that intercept vtRaw, are unsupported.
|
||||
pub const Tracker = struct {
|
||||
const initial_capacity = 4096;
|
||||
|
||||
alloc: Allocator,
|
||||
max_bytes: usize,
|
||||
bytes: std.ArrayList(u8) = .empty,
|
||||
broken: bool = false,
|
||||
|
||||
/// Initialize a tracker.
|
||||
pub fn init(alloc: Allocator, max_bytes: usize) Tracker {
|
||||
var result: Tracker = .{
|
||||
.alloc = alloc,
|
||||
.max_bytes = max_bytes,
|
||||
};
|
||||
result.bytes.ensureTotalCapacity(
|
||||
alloc,
|
||||
@min(max_bytes, initial_capacity),
|
||||
) catch {
|
||||
// We ignore memory errors here. They'll mark the tracker
|
||||
// as broken in a future append.
|
||||
|
||||
};
|
||||
return result;
|
||||
}
|
||||
|
||||
pub fn deinit(self: *Tracker) void {
|
||||
self.bytes.deinit(self.alloc);
|
||||
}
|
||||
|
||||
/// Clear the current continuation suffix and broken state while keeping
|
||||
/// its allocation. Stream calls this after reaching ground because no
|
||||
/// earlier input is needed to reconstruct the next unfinished state.
|
||||
pub fn reset(self: *Tracker) void {
|
||||
self.broken = false;
|
||||
self.bytes.clearRetainingCapacity();
|
||||
}
|
||||
|
||||
/// Which state machine is unfinished at the end of a feed. When the VT
|
||||
/// parser is outside ground the unfinished state is an escape sequence.
|
||||
/// When it is grounded, only the UTF-8 decoder can be unfinished, with
|
||||
/// an incomplete codepoint ending the feed.
|
||||
pub const Pending = enum { vt, utf8 };
|
||||
|
||||
/// Retain the part of `input` needed to replay a feed that ended with
|
||||
/// `pending` state unfinished.
|
||||
///
|
||||
/// When the input contains the replay start for that state, it replaces
|
||||
/// the retained suffix outright because nothing earlier is needed. The
|
||||
/// new suffix is complete on its own, so this also repairs a broken
|
||||
/// tracker. Otherwise the unfinished state began in an earlier feed and
|
||||
/// this whole input extends the current suffix.
|
||||
pub fn append(self: *Tracker, pending: Pending, input: []const u8) void {
|
||||
const start = switch (pending) {
|
||||
.vt => findVTReplayStart(input),
|
||||
.utf8 => findUtf8ReplayStart(input),
|
||||
} orelse {
|
||||
self.extend(input);
|
||||
return;
|
||||
};
|
||||
self.replace(input[start..]);
|
||||
}
|
||||
|
||||
/// Replace the continuation suffix with one that has a new replay start.
|
||||
fn replace(self: *Tracker, suffix: []const u8) void {
|
||||
self.bytes.clearRetainingCapacity();
|
||||
if (suffix.len > self.max_bytes) {
|
||||
self.markBroken();
|
||||
return;
|
||||
}
|
||||
self.bytes.appendSlice(self.alloc, suffix) catch {
|
||||
self.markBroken();
|
||||
return;
|
||||
};
|
||||
self.broken = false;
|
||||
}
|
||||
|
||||
/// Extend the continuation suffix with a fragment that contains no replay
|
||||
/// start of its own. The stream stayed unfinished for the whole fragment,
|
||||
/// so the existing suffix plus this fragment reproduces the current state.
|
||||
fn extend(self: *Tracker, fragment: []const u8) void {
|
||||
if (self.broken) return;
|
||||
if (fragment.len > self.max_bytes -| self.bytes.items.len) {
|
||||
self.markBroken();
|
||||
return;
|
||||
}
|
||||
self.bytes.appendSlice(self.alloc, fragment) catch self.markBroken();
|
||||
}
|
||||
|
||||
/// Write the replay-safe continuation suffix to `writer`. The retained
|
||||
/// bytes already begin at the replay start; this omits only bytes that
|
||||
/// would repeat committed terminal effects without contributing to the
|
||||
/// unfinished parser state (e.g. a BEL inside an unfinished CSI). The
|
||||
/// tracker must not be broken.
|
||||
pub fn write(
|
||||
self: *const Tracker,
|
||||
writer: *std.Io.Writer,
|
||||
) std.Io.Writer.Error!void {
|
||||
var scanner: BoundaryScanner = .init();
|
||||
for (self.bytes.items) |c| {
|
||||
if (scanner.next(c) == .omittable) continue;
|
||||
try writer.writeByte(c);
|
||||
}
|
||||
}
|
||||
|
||||
fn markBroken(self: *Tracker) void {
|
||||
self.broken = true;
|
||||
self.bytes.clearRetainingCapacity();
|
||||
}
|
||||
};
|
||||
|
||||
/// Find where replay must begin when a feed ends inside a VT sequence.
|
||||
///
|
||||
/// VT parsers treat ESC specially: it abandons the previous parser state and
|
||||
/// starts a fresh escape state no matter what was being parsed. Because of
|
||||
/// that rule, feeding the bytes from the last ESC onward into a grounded
|
||||
/// Stream recreates the unfinished state at the end of `input`.
|
||||
///
|
||||
/// The caller must only use this when the VT parser ended outside ground.
|
||||
/// The returned value is the index of the last ESC in `input`. A null result
|
||||
/// means the sequence began in an earlier feed, so this entire input must be
|
||||
/// appended to the continuation bytes already retained.
|
||||
fn findVTReplayStart(input: []const u8) ?usize {
|
||||
const esc: u8 = 0x1B;
|
||||
var rem: usize = input.len;
|
||||
|
||||
// Escape sequences are short, so when the stream ends inside one the
|
||||
// replay start is usually within the last few bytes. Scan backward in
|
||||
// vector chunks; pure payload inputs (no ESC at all) still scan quickly.
|
||||
if (comptime std.simd.suggestVectorLength(u8)) |lanes| {
|
||||
const V = @Vector(lanes, u8);
|
||||
const Bits = std.meta.Int(.unsigned, lanes);
|
||||
const needle: V = @splat(esc);
|
||||
|
||||
// Process several vectors per iteration so long inputs without ESC
|
||||
// (e.g. huge string payloads) stay memory-bound rather than
|
||||
// loop-bound. On a match, rescan the group precisely.
|
||||
const unroll = 4;
|
||||
while (rem >= lanes * unroll) {
|
||||
const start = rem - lanes * unroll;
|
||||
var match: @Vector(lanes, bool) = @splat(false);
|
||||
inline for (0..unroll) |block| {
|
||||
const v: V = input[start + block * lanes ..][0..lanes].*;
|
||||
match = match | (v == needle);
|
||||
}
|
||||
if (@reduce(.Or, match)) {
|
||||
inline for (0..unroll) |i| {
|
||||
const block = unroll - 1 - i;
|
||||
const v: V = input[start + block * lanes ..][0..lanes].*;
|
||||
const bits: Bits = @bitCast(v == needle);
|
||||
if (bits != 0) {
|
||||
return start + block * lanes + (lanes - 1 - @clz(bits));
|
||||
}
|
||||
}
|
||||
unreachable;
|
||||
}
|
||||
rem = start;
|
||||
}
|
||||
|
||||
while (rem >= lanes) {
|
||||
const start = rem - lanes;
|
||||
const v: V = input[start..][0..lanes].*;
|
||||
const bits: Bits = @bitCast(v == needle);
|
||||
if (bits != 0) return start + (lanes - 1 - @clz(bits));
|
||||
rem = start;
|
||||
}
|
||||
}
|
||||
|
||||
while (rem > 0) {
|
||||
rem -= 1;
|
||||
if (input[rem] == esc) return rem;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/// Find where replay must begin when a feed ends inside a UTF-8 codepoint.
|
||||
///
|
||||
/// A UTF-8 codepoint starts with a lead byte and may have up to three
|
||||
/// continuation bytes. If the lead byte is in this input, replay must begin
|
||||
/// there so the decoder sees the complete partial codepoint again. Since an
|
||||
/// incomplete codepoint can contain at most three bytes, only the final three
|
||||
/// input bytes need to be searched.
|
||||
///
|
||||
/// The caller must only use this when the VT parser is at ground and the
|
||||
/// UTF-8 decoder ended mid-codepoint. The returned value is the lead byte's
|
||||
/// index. A null result means the lead byte was in an earlier feed, so this
|
||||
/// entire input must be appended to the continuation bytes already retained.
|
||||
fn findUtf8ReplayStart(input: []const u8) ?usize {
|
||||
const max_pending = 3;
|
||||
var idx = input.len;
|
||||
while (idx > 0 and input.len - idx < max_pending) {
|
||||
idx -= 1;
|
||||
if (input[idx] >= 0xC0) return idx;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/// Classifies bytes of a continuation suffix for export.
|
||||
///
|
||||
/// The retained suffix reconstructs unfinished state exactly, but it can
|
||||
/// contain bytes whose handler-visible work already committed the first
|
||||
/// time (e.g. a C0 control executed inside an unfinished CSI sequence).
|
||||
/// This implements a minimal VT stream processor (to avoid circular imports
|
||||
/// with stream.zig) so `Tracker.write` can omit those bytes and replay
|
||||
/// never repeats a terminal effect. It only runs at export time, never on
|
||||
/// the feed path.
|
||||
///
|
||||
/// This is allocation-free.
|
||||
const BoundaryScanner = struct {
|
||||
parser: Parser,
|
||||
utf8decoder: UTF8Decoder,
|
||||
|
||||
/// Initialize both state machines at runtime. Parser initialization may
|
||||
/// inspect the runtime environment, so it cannot be a struct field default
|
||||
/// that Zig requires to be comptime-known.
|
||||
fn init() BoundaryScanner {
|
||||
return .{
|
||||
.parser = .init(),
|
||||
.utf8decoder = .{},
|
||||
};
|
||||
}
|
||||
|
||||
/// How one byte affects committed work and the continuation suffix.
|
||||
const Effect = enum {
|
||||
/// The byte commits no handler-visible work. It must remain because it
|
||||
/// may still build the unfinished VT, UTF-8, APC, or DCS state.
|
||||
uncommitted,
|
||||
|
||||
/// The byte commits handler-visible work but also changes a parser
|
||||
/// state tag or reaches ground. It cannot be omitted in isolation;
|
||||
/// boundary analysis decides whether the surrounding prefix is safe.
|
||||
committed,
|
||||
|
||||
/// The byte commits handler-visible work without changing either
|
||||
/// state-machine tag or reaching ground. It can be omitted without
|
||||
/// changing the unfinished state, avoiding a duplicate effect.
|
||||
omittable,
|
||||
};
|
||||
|
||||
/// True only when neither state machine needs prior bytes to continue.
|
||||
fn ground(self: *const BoundaryScanner) bool {
|
||||
return self.parser.state == .ground and self.utf8decoder.state == 0;
|
||||
}
|
||||
|
||||
/// Consume one byte using the same scalar UTF-8 retry and VT transitions
|
||||
/// as Stream, then classify its effect on continuation construction.
|
||||
fn next(self: *BoundaryScanner, c: u8) Effect {
|
||||
// Preserve the state tags so we can recognize committed controls that
|
||||
// do not contribute to the unfinished sequence.
|
||||
const parser_state = self.parser.state;
|
||||
const utf8_state = self.utf8decoder.state;
|
||||
|
||||
// A byte is committed when replaying it would repeat handler-visible
|
||||
// work already captured outside the continuation suffix. Actions that
|
||||
// only build unfinished APC or DCS state are not committed because
|
||||
// replay must reconstruct that state.
|
||||
const committed = committed: {
|
||||
if (self.parser.state == .ground) {
|
||||
// Match Stream's scalar UTF-8 path, including retrying a byte that
|
||||
// follows a malformed sequence.
|
||||
var committed = false;
|
||||
const res = self.utf8decoder.next(c);
|
||||
if (res[0]) |cp| committed = self.codepoint(cp) or committed;
|
||||
if (!res[1]) {
|
||||
const retry = self.utf8decoder.next(c);
|
||||
assert(retry[1]);
|
||||
if (retry[0]) |cp| committed = self.codepoint(cp) or committed;
|
||||
}
|
||||
|
||||
break :committed committed;
|
||||
}
|
||||
|
||||
const actions = self.parser.next(c);
|
||||
for (actions) |action_opt| {
|
||||
const action = action_opt orelse continue;
|
||||
switch (action) {
|
||||
// These actions only build standard handler state. Their
|
||||
// matching end/unhook actions are committed work.
|
||||
.dcs_hook,
|
||||
.dcs_put,
|
||||
.apc_start,
|
||||
.apc_put,
|
||||
=> {},
|
||||
else => break :committed true,
|
||||
}
|
||||
}
|
||||
|
||||
break :committed false;
|
||||
};
|
||||
|
||||
if (!committed) return .uncommitted;
|
||||
|
||||
// A committed byte is independently omittable only while the stream
|
||||
// remains unfinished and neither state-machine tag changes.
|
||||
const state_changed = parser_state != self.parser.state or
|
||||
utf8_state != self.utf8decoder.state;
|
||||
return if (!state_changed and !self.ground())
|
||||
.omittable
|
||||
else
|
||||
.committed;
|
||||
}
|
||||
|
||||
/// Apply the Stream.handleCodepoint behavior relevant to replay. Returns
|
||||
/// whether the accepted codepoint would commit handler-visible work.
|
||||
fn codepoint(self: *BoundaryScanner, cp: u21) bool {
|
||||
if (cp == 0x1B) {
|
||||
self.parser.state = .escape;
|
||||
self.parser.clear();
|
||||
return false;
|
||||
}
|
||||
|
||||
// Match Stream.handleCodepoint: all other accepted codepoints have
|
||||
// already caused a handler-visible action (including supported C0s).
|
||||
return true;
|
||||
}
|
||||
};
|
||||
|
||||
test "boundary scanner classifies effects and ground" {
|
||||
const testing = std.testing;
|
||||
var scanner: BoundaryScanner = .init();
|
||||
|
||||
try testing.expect(scanner.ground());
|
||||
try testing.expectEqual(BoundaryScanner.Effect.committed, scanner.next('A'));
|
||||
try testing.expect(scanner.ground());
|
||||
|
||||
try testing.expectEqual(BoundaryScanner.Effect.uncommitted, scanner.next(0x1B));
|
||||
try testing.expect(!scanner.ground());
|
||||
try testing.expectEqual(BoundaryScanner.Effect.uncommitted, scanner.next('['));
|
||||
try testing.expectEqual(BoundaryScanner.Effect.uncommitted, scanner.next('1'));
|
||||
|
||||
// BEL commits an execute action without changing the unfinished CSI.
|
||||
try testing.expectEqual(BoundaryScanner.Effect.omittable, scanner.next(0x07));
|
||||
try testing.expect(!scanner.ground());
|
||||
|
||||
try testing.expectEqual(BoundaryScanner.Effect.committed, scanner.next('m'));
|
||||
try testing.expect(scanner.ground());
|
||||
}
|
||||
|
||||
test "boundary scanner preserves unfinished builder actions" {
|
||||
const testing = std.testing;
|
||||
|
||||
var apc: BoundaryScanner = .init();
|
||||
try testing.expectEqual(BoundaryScanner.Effect.uncommitted, apc.next(0x1B));
|
||||
try testing.expectEqual(BoundaryScanner.Effect.uncommitted, apc.next('_'));
|
||||
try testing.expectEqual(BoundaryScanner.Effect.uncommitted, apc.next('G'));
|
||||
try testing.expectEqual(BoundaryScanner.Effect.committed, apc.next(0x1B));
|
||||
|
||||
var dcs: BoundaryScanner = .init();
|
||||
try testing.expectEqual(BoundaryScanner.Effect.uncommitted, dcs.next(0x1B));
|
||||
try testing.expectEqual(BoundaryScanner.Effect.uncommitted, dcs.next('P'));
|
||||
try testing.expectEqual(BoundaryScanner.Effect.uncommitted, dcs.next('q'));
|
||||
try testing.expectEqual(BoundaryScanner.Effect.uncommitted, dcs.next('x'));
|
||||
try testing.expectEqual(BoundaryScanner.Effect.committed, dcs.next(0x1B));
|
||||
}
|
||||
|
||||
test "boundary scanner handles UTF-8 and malformed retries" {
|
||||
const testing = std.testing;
|
||||
|
||||
var valid: BoundaryScanner = .init();
|
||||
try testing.expectEqual(BoundaryScanner.Effect.uncommitted, valid.next(0xF0));
|
||||
try testing.expect(!valid.ground());
|
||||
try testing.expectEqual(BoundaryScanner.Effect.uncommitted, valid.next(0x9F));
|
||||
try testing.expectEqual(BoundaryScanner.Effect.uncommitted, valid.next(0x98));
|
||||
try testing.expectEqual(BoundaryScanner.Effect.committed, valid.next(0x84));
|
||||
try testing.expect(valid.ground());
|
||||
|
||||
var malformed: BoundaryScanner = .init();
|
||||
try testing.expectEqual(BoundaryScanner.Effect.uncommitted, malformed.next(0xE0));
|
||||
try testing.expectEqual(BoundaryScanner.Effect.uncommitted, malformed.next(0xA0));
|
||||
try testing.expect(malformed.next(0xF0) != .uncommitted);
|
||||
try testing.expect(!malformed.ground());
|
||||
}
|
||||
|
||||
test "findVTReplayStart finds the latest ESC across vector boundaries" {
|
||||
const testing = std.testing;
|
||||
|
||||
try testing.expectEqual(@as(?usize, null), findVTReplayStart(""));
|
||||
try testing.expectEqual(@as(?usize, null), findVTReplayStart("no escapes here"));
|
||||
try testing.expectEqual(@as(?usize, 0), findVTReplayStart("\x1b"));
|
||||
try testing.expectEqual(@as(?usize, 5), findVTReplayStart("\x1b[1m\x20\x1b[2"));
|
||||
|
||||
// Exercise both the vector loop and the scalar remainder with replay
|
||||
// starts at every position of a buffer larger than any vector width.
|
||||
var buf: [193]u8 = undefined;
|
||||
@memset(&buf, 'a');
|
||||
try testing.expectEqual(@as(?usize, null), findVTReplayStart(&buf));
|
||||
for (0..buf.len) |idx| {
|
||||
@memset(&buf, 'a');
|
||||
buf[idx] = 0x1B;
|
||||
try testing.expectEqual(@as(?usize, idx), findVTReplayStart(&buf));
|
||||
|
||||
// The latest ESC wins.
|
||||
if (idx > 0) {
|
||||
buf[idx - 1] = 0x1B;
|
||||
try testing.expectEqual(@as(?usize, idx), findVTReplayStart(&buf));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
test "findUtf8ReplayStart finds the pending lead byte" {
|
||||
const testing = std.testing;
|
||||
|
||||
try testing.expectEqual(@as(?usize, null), findUtf8ReplayStart(""));
|
||||
try testing.expectEqual(@as(?usize, 4), findUtf8ReplayStart("text\xF0"));
|
||||
try testing.expectEqual(@as(?usize, 4), findUtf8ReplayStart("text\xF0\x9F"));
|
||||
try testing.expectEqual(@as(?usize, 4), findUtf8ReplayStart("text\xF0\x9F\x98"));
|
||||
try testing.expectEqual(@as(?usize, 0), findUtf8ReplayStart("\xE0\xA0"));
|
||||
|
||||
// A malformed prefix rejected earlier doesn't hide the pending lead.
|
||||
try testing.expectEqual(@as(?usize, 2), findUtf8ReplayStart("\xE0\xA0\xF0"));
|
||||
|
||||
// Continuation bytes of a sequence that started in an earlier input have
|
||||
// no replay start of their own.
|
||||
try testing.expectEqual(@as(?usize, null), findUtf8ReplayStart("\x9F"));
|
||||
try testing.expectEqual(@as(?usize, null), findUtf8ReplayStart("\x9F\x98"));
|
||||
}
|
||||
|
||||
test "tracker retains and normalizes replay-safe bytes" {
|
||||
const testing = std.testing;
|
||||
var tracker = Tracker.init(testing.allocator, 64);
|
||||
defer tracker.deinit();
|
||||
|
||||
tracker.append(.vt, "committed\x1b[1\x07");
|
||||
tracker.append(.vt, ";2");
|
||||
try testing.expect(!tracker.broken);
|
||||
|
||||
var buf: [64]u8 = undefined;
|
||||
var writer: std.Io.Writer = .fixed(&buf);
|
||||
try tracker.write(&writer);
|
||||
try testing.expectEqualStrings("\x1b[1;2", writer.buffered());
|
||||
|
||||
// A feed with a later replay start drops the previous suffix entirely.
|
||||
tracker.append(.vt, "committed\x1b]0;t");
|
||||
var replaced_buf: [64]u8 = undefined;
|
||||
var replaced_writer: std.Io.Writer = .fixed(&replaced_buf);
|
||||
try tracker.write(&replaced_writer);
|
||||
try testing.expectEqualStrings("\x1b]0;t", replaced_writer.buffered());
|
||||
|
||||
// An incomplete UTF-8 codepoint seeds at its lead byte and grows with
|
||||
// continuation bytes from later feeds.
|
||||
tracker.append(.utf8, "committed\xF0");
|
||||
tracker.append(.utf8, "\x9F");
|
||||
var utf8_buf: [4]u8 = undefined;
|
||||
var utf8_writer: std.Io.Writer = .fixed(&utf8_buf);
|
||||
try tracker.write(&utf8_writer);
|
||||
try testing.expectEqualSlices(u8, "\xF0\x9F", utf8_writer.buffered());
|
||||
}
|
||||
|
||||
test "tracker cap, reset, and broken recovery" {
|
||||
const testing = std.testing;
|
||||
var tracker = Tracker.init(testing.allocator, 4);
|
||||
defer tracker.deinit();
|
||||
|
||||
tracker.append(.vt, "\x1b[123");
|
||||
try testing.expect(tracker.broken);
|
||||
|
||||
// Feeds without a replay start are dropped while broken.
|
||||
tracker.append(.vt, "4");
|
||||
try testing.expect(tracker.broken);
|
||||
try testing.expectEqual(@as(usize, 0), tracker.bytes.items.len);
|
||||
|
||||
// Suffixes that grow past the cap break tracking.
|
||||
tracker.reset();
|
||||
tracker.append(.vt, "\x1b[1");
|
||||
tracker.append(.vt, "23");
|
||||
try testing.expect(tracker.broken);
|
||||
|
||||
// A new replay start that fits recovers without a reset.
|
||||
tracker.append(.vt, "\x1b[");
|
||||
try testing.expect(!tracker.broken);
|
||||
var buf: [4]u8 = undefined;
|
||||
var writer: std.Io.Writer = .fixed(&buf);
|
||||
try tracker.write(&writer);
|
||||
try testing.expectEqualStrings("\x1b[", writer.buffered());
|
||||
|
||||
tracker.append(.vt, "\x1b[123");
|
||||
try testing.expect(tracker.broken);
|
||||
tracker.reset();
|
||||
try testing.expect(!tracker.broken);
|
||||
var empty_buf: [1]u8 = undefined;
|
||||
var empty_writer: std.Io.Writer = .fixed(&empty_buf);
|
||||
try tracker.write(&empty_writer);
|
||||
try testing.expectEqual(@as(usize, 0), empty_writer.end);
|
||||
}
|
||||
|
||||
test "tracker reports writer failure and defers allocation failure" {
|
||||
const testing = std.testing;
|
||||
var tracker = Tracker.init(testing.allocator, 64);
|
||||
defer tracker.deinit();
|
||||
tracker.append(.vt, "\x1b[");
|
||||
|
||||
var buf: [1]u8 = undefined;
|
||||
var writer: std.Io.Writer = .fixed(&buf);
|
||||
try testing.expectError(error.WriteFailed, tracker.write(&writer));
|
||||
|
||||
var failing = testing.FailingAllocator.init(testing.allocator, .{
|
||||
.fail_index = 0,
|
||||
});
|
||||
var failing_tracker = Tracker.init(failing.allocator(), 64);
|
||||
defer failing_tracker.deinit();
|
||||
try testing.expect(!failing_tracker.broken);
|
||||
|
||||
// The best-effort initial reservation failed, so the first required
|
||||
// retention retries allocation and marks the tracker broken.
|
||||
failing_tracker.append(.vt, "\x1b[");
|
||||
try testing.expect(failing_tracker.broken);
|
||||
}
|
||||
@@ -993,7 +993,7 @@ test "resize clears synchronized output on unchanged cell dimensions" {
|
||||
var t: Terminal = try .init(testing.io, testing.allocator, .{ .cols = 80, .rows = 24 });
|
||||
defer t.deinit(testing.allocator);
|
||||
|
||||
var s: Stream = .initAlloc(testing.allocator, .init(&t));
|
||||
var s: Stream = .init(.{ .allocator = testing.allocator, .handler = .init(&t) });
|
||||
defer s.deinit();
|
||||
|
||||
t.modes.set(.synchronized_output, true);
|
||||
@@ -1025,7 +1025,7 @@ test "resize reports mode 2048 geometry" {
|
||||
|
||||
var handler: Handler = .init(&t);
|
||||
handler.effects.write_pty = &S.writePty;
|
||||
var s: Stream = .initAlloc(testing.allocator, handler);
|
||||
var s: Stream = .init(.{ .allocator = testing.allocator, .handler = handler });
|
||||
defer s.deinit();
|
||||
|
||||
t.modes.set(.in_band_size_reports, true);
|
||||
@@ -1055,7 +1055,7 @@ test "resize suppresses mode 2048 reports" {
|
||||
defer t.deinit(testing.allocator);
|
||||
var handler: Handler = .init(&t);
|
||||
handler.effects.write_pty = &S.writePty;
|
||||
var s: Stream = .initAlloc(testing.allocator, handler);
|
||||
var s: Stream = .init(.{ .allocator = testing.allocator, .handler = handler });
|
||||
defer s.deinit();
|
||||
|
||||
// Disabled mode suppresses a report even with pixels and a callback.
|
||||
@@ -1079,10 +1079,10 @@ test "resize suppresses mode 2048 reports" {
|
||||
);
|
||||
defer readonly_terminal.deinit(testing.allocator);
|
||||
readonly_terminal.modes.set(.in_band_size_reports, true);
|
||||
var readonly_stream: Stream = .initAlloc(
|
||||
testing.allocator,
|
||||
.init(&readonly_terminal),
|
||||
);
|
||||
var readonly_stream: Stream = .init(.{
|
||||
.allocator = testing.allocator,
|
||||
.handler = .init(&readonly_terminal),
|
||||
});
|
||||
defer readonly_stream.deinit();
|
||||
try readonly_stream.handler.resize(.{
|
||||
.cols = 80,
|
||||
@@ -1109,7 +1109,7 @@ test "resize failure preserves terminal state and does not write" {
|
||||
|
||||
var handler: Handler = .init(&t);
|
||||
handler.effects.write_pty = &S.writePty;
|
||||
var s: Stream = .initAlloc(alloc, handler);
|
||||
var s: Stream = .init(.{ .allocator = alloc, .handler = handler });
|
||||
defer s.deinit();
|
||||
|
||||
t.modes.set(.synchronized_output, true);
|
||||
@@ -1147,15 +1147,15 @@ test "resize effects do not change canonical terminal state" {
|
||||
};
|
||||
var authoritative_handler: Handler = .init(&authoritative);
|
||||
authoritative_handler.effects.write_pty = &S.writePty;
|
||||
var authoritative_stream: Stream = .initAlloc(
|
||||
testing.allocator,
|
||||
authoritative_handler,
|
||||
);
|
||||
var authoritative_stream: Stream = .init(.{
|
||||
.allocator = testing.allocator,
|
||||
.handler = authoritative_handler,
|
||||
});
|
||||
defer authoritative_stream.deinit();
|
||||
var readonly_stream: Stream = .initAlloc(
|
||||
testing.allocator,
|
||||
.init(&readonly),
|
||||
);
|
||||
var readonly_stream: Stream = .init(.{
|
||||
.allocator = testing.allocator,
|
||||
.handler = .init(&readonly),
|
||||
});
|
||||
defer readonly_stream.deinit();
|
||||
|
||||
authoritative.modes.set(.in_band_size_reports, true);
|
||||
@@ -1183,7 +1183,7 @@ test "basic print" {
|
||||
var t: Terminal = try .init(testing.io, testing.allocator, .{ .cols = 10, .rows = 10 });
|
||||
defer t.deinit(testing.allocator);
|
||||
|
||||
var s: Stream = .initAlloc(testing.allocator, .init(&t));
|
||||
var s: Stream = .init(.{ .allocator = testing.allocator, .handler = .init(&t) });
|
||||
defer s.deinit();
|
||||
|
||||
s.nextSlice("Hello");
|
||||
@@ -1201,7 +1201,7 @@ test "semantic failure is sticky while processing continues" {
|
||||
var t: Terminal = try .init(testing.io, alloc, .{ .cols = 10, .rows = 2 });
|
||||
defer t.deinit(alloc);
|
||||
|
||||
var s: Stream = .initAlloc(alloc, .init(&t));
|
||||
var s: Stream = .init(.{ .allocator = alloc, .handler = .init(&t) });
|
||||
defer s.deinit();
|
||||
try testing.expect(!s.handler.semantic_failure);
|
||||
|
||||
@@ -1232,7 +1232,7 @@ test "cursor movement" {
|
||||
var t: Terminal = try .init(testing.io, testing.allocator, .{ .cols = 10, .rows = 10 });
|
||||
defer t.deinit(testing.allocator);
|
||||
|
||||
var s: Stream = .initAlloc(testing.allocator, .init(&t));
|
||||
var s: Stream = .init(.{ .allocator = testing.allocator, .handler = .init(&t) });
|
||||
defer s.deinit();
|
||||
|
||||
// Move cursor using escape sequences
|
||||
@@ -1250,7 +1250,7 @@ test "erase operations" {
|
||||
var t: Terminal = try .init(testing.io, testing.allocator, .{ .cols = 20, .rows = 10 });
|
||||
defer t.deinit(testing.allocator);
|
||||
|
||||
var s: Stream = .initAlloc(testing.allocator, .init(&t));
|
||||
var s: Stream = .init(.{ .allocator = testing.allocator, .handler = .init(&t) });
|
||||
defer s.deinit();
|
||||
|
||||
// Print some text
|
||||
@@ -1271,7 +1271,7 @@ test "tabs" {
|
||||
var t: Terminal = try .init(testing.io, testing.allocator, .{ .cols = 80, .rows = 10 });
|
||||
defer t.deinit(testing.allocator);
|
||||
|
||||
var s: Stream = .initAlloc(testing.allocator, .init(&t));
|
||||
var s: Stream = .init(.{ .allocator = testing.allocator, .handler = .init(&t) });
|
||||
defer s.deinit();
|
||||
|
||||
s.nextSlice("A\tB");
|
||||
@@ -1286,7 +1286,7 @@ test "modes" {
|
||||
var t: Terminal = try .init(testing.io, testing.allocator, .{ .cols = 80, .rows = 24 });
|
||||
defer t.deinit(testing.allocator);
|
||||
|
||||
var s: Stream = .initAlloc(testing.allocator, .init(&t));
|
||||
var s: Stream = .init(.{ .allocator = testing.allocator, .handler = .init(&t) });
|
||||
defer s.deinit();
|
||||
|
||||
// Test wraparound mode
|
||||
@@ -1301,7 +1301,7 @@ test "scrolling regions" {
|
||||
var t: Terminal = try .init(testing.io, testing.allocator, .{ .cols = 80, .rows = 24 });
|
||||
defer t.deinit(testing.allocator);
|
||||
|
||||
var s: Stream = .initAlloc(testing.allocator, .init(&t));
|
||||
var s: Stream = .init(.{ .allocator = testing.allocator, .handler = .init(&t) });
|
||||
defer s.deinit();
|
||||
|
||||
// Set scrolling region from line 5 to 20
|
||||
@@ -1316,7 +1316,7 @@ test "charsets" {
|
||||
var t: Terminal = try .init(testing.io, testing.allocator, .{ .cols = 80, .rows = 24 });
|
||||
defer t.deinit(testing.allocator);
|
||||
|
||||
var s: Stream = .initAlloc(testing.allocator, .init(&t));
|
||||
var s: Stream = .init(.{ .allocator = testing.allocator, .handler = .init(&t) });
|
||||
defer s.deinit();
|
||||
|
||||
// Configure G0 as DEC special graphics
|
||||
@@ -1332,7 +1332,7 @@ test "alt screen" {
|
||||
var t: Terminal = try .init(testing.io, testing.allocator, .{ .cols = 10, .rows = 5 });
|
||||
defer t.deinit(testing.allocator);
|
||||
|
||||
var s: Stream = .initAlloc(testing.allocator, .init(&t));
|
||||
var s: Stream = .init(.{ .allocator = testing.allocator, .handler = .init(&t) });
|
||||
defer s.deinit();
|
||||
|
||||
// Write to primary screen
|
||||
@@ -1359,7 +1359,7 @@ test "cursor save and restore" {
|
||||
var t: Terminal = try .init(testing.io, testing.allocator, .{ .cols = 80, .rows = 24 });
|
||||
defer t.deinit(testing.allocator);
|
||||
|
||||
var s: Stream = .initAlloc(testing.allocator, .init(&t));
|
||||
var s: Stream = .init(.{ .allocator = testing.allocator, .handler = .init(&t) });
|
||||
defer s.deinit();
|
||||
|
||||
// Move cursor to 10,15
|
||||
@@ -1385,7 +1385,7 @@ test "attributes" {
|
||||
var t: Terminal = try .init(testing.io, testing.allocator, .{ .cols = 80, .rows = 24 });
|
||||
defer t.deinit(testing.allocator);
|
||||
|
||||
var s: Stream = .initAlloc(testing.allocator, .init(&t));
|
||||
var s: Stream = .init(.{ .allocator = testing.allocator, .handler = .init(&t) });
|
||||
defer s.deinit();
|
||||
|
||||
// Set bold and write text
|
||||
@@ -1434,7 +1434,7 @@ test "DECRQSS responses" {
|
||||
|
||||
var handler: Handler = .init(&t);
|
||||
handler.effects.write_pty = &S.writePty;
|
||||
var s: Stream = .initAlloc(testing.allocator, handler);
|
||||
var s: Stream = .init(.{ .allocator = testing.allocator, .handler = handler });
|
||||
defer s.deinit();
|
||||
|
||||
// SGR
|
||||
@@ -1458,7 +1458,7 @@ test "DECRQSS without write effect is ignored" {
|
||||
);
|
||||
defer t.deinit(testing.allocator);
|
||||
|
||||
var s: Stream = .initAlloc(testing.allocator, .init(&t));
|
||||
var s: Stream = .init(.{ .allocator = testing.allocator, .handler = .init(&t) });
|
||||
defer s.deinit();
|
||||
|
||||
s.nextSlice("\x1BP$qm\x1B\\");
|
||||
@@ -1473,7 +1473,7 @@ test "DCS command memory is released" {
|
||||
);
|
||||
defer t.deinit(testing.allocator);
|
||||
|
||||
var s: Stream = .initAlloc(testing.allocator, .init(&t));
|
||||
var s: Stream = .init(.{ .allocator = testing.allocator, .handler = .init(&t) });
|
||||
|
||||
// A completed, unsupported command transfers its allocation to Command;
|
||||
// dcsCommand must release it even though stream_terminal ignores it.
|
||||
@@ -1489,7 +1489,7 @@ test "DECALN screen alignment" {
|
||||
var t: Terminal = try .init(testing.io, testing.allocator, .{ .cols = 10, .rows = 3 });
|
||||
defer t.deinit(testing.allocator);
|
||||
|
||||
var s: Stream = .initAlloc(testing.allocator, .init(&t));
|
||||
var s: Stream = .init(.{ .allocator = testing.allocator, .handler = .init(&t) });
|
||||
defer s.deinit();
|
||||
|
||||
// Run DECALN
|
||||
@@ -1509,7 +1509,7 @@ test "full reset" {
|
||||
var t: Terminal = try .init(testing.io, testing.allocator, .{ .cols = 80, .rows = 24 });
|
||||
defer t.deinit(testing.allocator);
|
||||
|
||||
var s: Stream = .initAlloc(testing.allocator, .init(&t));
|
||||
var s: Stream = .init(.{ .allocator = testing.allocator, .handler = .init(&t) });
|
||||
defer s.deinit();
|
||||
|
||||
// Make some changes
|
||||
@@ -1549,7 +1549,7 @@ test "glyph protocol APC with write_pty callback" {
|
||||
var handler: Handler = .init(&t);
|
||||
handler.effects.write_pty = &S.writePty;
|
||||
|
||||
var s: Stream = .initAlloc(testing.allocator, handler);
|
||||
var s: Stream = .init(.{ .allocator = testing.allocator, .handler = handler });
|
||||
defer s.deinit();
|
||||
|
||||
s.nextSlice("\x1B_25a1;s\x1B\\");
|
||||
@@ -1564,7 +1564,7 @@ test "ignores query actions" {
|
||||
var t: Terminal = try .init(testing.io, testing.allocator, .{ .cols = 80, .rows = 24 });
|
||||
defer t.deinit(testing.allocator);
|
||||
|
||||
var s: Stream = .initAlloc(testing.allocator, .init(&t));
|
||||
var s: Stream = .init(.{ .allocator = testing.allocator, .handler = .init(&t) });
|
||||
defer s.deinit();
|
||||
|
||||
// These should be ignored without error
|
||||
@@ -1592,7 +1592,7 @@ test "OSC 4 set and reset palette" {
|
||||
var t: Terminal = try .init(testing.io, testing.allocator, .{ .cols = 10, .rows = 10 });
|
||||
defer t.deinit(testing.allocator);
|
||||
|
||||
var s: Stream = .initAlloc(testing.allocator, .init(&t));
|
||||
var s: Stream = .init(.{ .allocator = testing.allocator, .handler = .init(&t) });
|
||||
defer s.deinit();
|
||||
|
||||
// Save default color
|
||||
@@ -1615,7 +1615,7 @@ test "OSC 104 reset all palette colors" {
|
||||
var t: Terminal = try .init(testing.io, testing.allocator, .{ .cols = 10, .rows = 10 });
|
||||
defer t.deinit(testing.allocator);
|
||||
|
||||
var s: Stream = .initAlloc(testing.allocator, .init(&t));
|
||||
var s: Stream = .init(.{ .allocator = testing.allocator, .handler = .init(&t) });
|
||||
defer s.deinit();
|
||||
|
||||
// Set multiple colors
|
||||
@@ -1640,7 +1640,7 @@ test "OSC 10 set and reset foreground color" {
|
||||
var t: Terminal = try .init(testing.io, testing.allocator, .{ .cols = 10, .rows = 10 });
|
||||
defer t.deinit(testing.allocator);
|
||||
|
||||
var s: Stream = .initAlloc(testing.allocator, .init(&t));
|
||||
var s: Stream = .init(.{ .allocator = testing.allocator, .handler = .init(&t) });
|
||||
defer s.deinit();
|
||||
|
||||
// Initially unset
|
||||
@@ -1662,7 +1662,7 @@ test "OSC 11 set and reset background color" {
|
||||
var t: Terminal = try .init(testing.io, testing.allocator, .{ .cols = 10, .rows = 10 });
|
||||
defer t.deinit(testing.allocator);
|
||||
|
||||
var s: Stream = .initAlloc(testing.allocator, .init(&t));
|
||||
var s: Stream = .init(.{ .allocator = testing.allocator, .handler = .init(&t) });
|
||||
defer s.deinit();
|
||||
|
||||
// Set background to green
|
||||
@@ -1681,7 +1681,7 @@ test "OSC 12 set and reset cursor color" {
|
||||
var t: Terminal = try .init(testing.io, testing.allocator, .{ .cols = 10, .rows = 10 });
|
||||
defer t.deinit(testing.allocator);
|
||||
|
||||
var s: Stream = .initAlloc(testing.allocator, .init(&t));
|
||||
var s: Stream = .init(.{ .allocator = testing.allocator, .handler = .init(&t) });
|
||||
defer s.deinit();
|
||||
|
||||
// Set cursor to blue
|
||||
@@ -1719,7 +1719,7 @@ test "OSC color query responses" {
|
||||
var handler: Handler = .init(&t);
|
||||
handler.effects.write_pty = &S.writePty;
|
||||
|
||||
var s: Stream = .initAlloc(testing.allocator, handler);
|
||||
var s: Stream = .init(.{ .allocator = testing.allocator, .handler = handler });
|
||||
defer s.deinit();
|
||||
|
||||
s.nextSlice("\x1b]10;?\x1b\\");
|
||||
@@ -1757,7 +1757,7 @@ test "kitty color protocol set palette" {
|
||||
var t: Terminal = try .init(testing.io, testing.allocator, .{ .cols = 10, .rows = 10 });
|
||||
defer t.deinit(testing.allocator);
|
||||
|
||||
var s: Stream = .initAlloc(testing.allocator, .init(&t));
|
||||
var s: Stream = .init(.{ .allocator = testing.allocator, .handler = .init(&t) });
|
||||
defer s.deinit();
|
||||
|
||||
// Set palette color 5 to magenta using kitty protocol
|
||||
@@ -1773,7 +1773,7 @@ test "kitty color protocol reset palette" {
|
||||
var t: Terminal = try .init(testing.io, testing.allocator, .{ .cols = 10, .rows = 10 });
|
||||
defer t.deinit(testing.allocator);
|
||||
|
||||
var s: Stream = .initAlloc(testing.allocator, .init(&t));
|
||||
var s: Stream = .init(.{ .allocator = testing.allocator, .handler = .init(&t) });
|
||||
defer s.deinit();
|
||||
|
||||
// Set and then reset palette color
|
||||
@@ -1790,7 +1790,7 @@ test "kitty color protocol set foreground" {
|
||||
var t: Terminal = try .init(testing.io, testing.allocator, .{ .cols = 10, .rows = 10 });
|
||||
defer t.deinit(testing.allocator);
|
||||
|
||||
var s: Stream = .initAlloc(testing.allocator, .init(&t));
|
||||
var s: Stream = .init(.{ .allocator = testing.allocator, .handler = .init(&t) });
|
||||
defer s.deinit();
|
||||
|
||||
// Set foreground using kitty protocol
|
||||
@@ -1805,7 +1805,7 @@ test "kitty color protocol set background" {
|
||||
var t: Terminal = try .init(testing.io, testing.allocator, .{ .cols = 10, .rows = 10 });
|
||||
defer t.deinit(testing.allocator);
|
||||
|
||||
var s: Stream = .initAlloc(testing.allocator, .init(&t));
|
||||
var s: Stream = .init(.{ .allocator = testing.allocator, .handler = .init(&t) });
|
||||
defer s.deinit();
|
||||
|
||||
// Set background using kitty protocol
|
||||
@@ -1820,7 +1820,7 @@ test "kitty color protocol set cursor" {
|
||||
var t: Terminal = try .init(testing.io, testing.allocator, .{ .cols = 10, .rows = 10 });
|
||||
defer t.deinit(testing.allocator);
|
||||
|
||||
var s: Stream = .initAlloc(testing.allocator, .init(&t));
|
||||
var s: Stream = .init(.{ .allocator = testing.allocator, .handler = .init(&t) });
|
||||
defer s.deinit();
|
||||
|
||||
// Set cursor using kitty protocol
|
||||
@@ -1835,7 +1835,7 @@ test "kitty color protocol reset foreground" {
|
||||
var t: Terminal = try .init(testing.io, testing.allocator, .{ .cols = 10, .rows = 10 });
|
||||
defer t.deinit(testing.allocator);
|
||||
|
||||
var s: Stream = .initAlloc(testing.allocator, .init(&t));
|
||||
var s: Stream = .init(.{ .allocator = testing.allocator, .handler = .init(&t) });
|
||||
defer s.deinit();
|
||||
|
||||
// Set and reset foreground
|
||||
@@ -1870,7 +1870,7 @@ test "kitty color protocol query responses" {
|
||||
var handler: Handler = .init(&t);
|
||||
handler.effects.write_pty = &S.writePty;
|
||||
|
||||
var s: Stream = .initAlloc(testing.allocator, handler);
|
||||
var s: Stream = .init(.{ .allocator = testing.allocator, .handler = handler });
|
||||
defer s.deinit();
|
||||
|
||||
s.nextSlice("\x1b]21;background=?\x1b\\");
|
||||
@@ -1891,7 +1891,7 @@ test "palette dirty flag set on color change" {
|
||||
var t: Terminal = try .init(testing.io, testing.allocator, .{ .cols = 10, .rows = 10 });
|
||||
defer t.deinit(testing.allocator);
|
||||
|
||||
var s: Stream = .initAlloc(testing.allocator, .init(&t));
|
||||
var s: Stream = .init(.{ .allocator = testing.allocator, .handler = .init(&t) });
|
||||
defer s.deinit();
|
||||
|
||||
// Clear dirty flag
|
||||
@@ -1916,7 +1916,7 @@ test "semantic prompt fresh line" {
|
||||
var t: Terminal = try .init(testing.io, testing.allocator, .{ .cols = 10, .rows = 10 });
|
||||
defer t.deinit(testing.allocator);
|
||||
|
||||
var s: Stream = .initAlloc(testing.allocator, .init(&t));
|
||||
var s: Stream = .init(.{ .allocator = testing.allocator, .handler = .init(&t) });
|
||||
defer s.deinit();
|
||||
|
||||
s.nextSlice("Hello");
|
||||
@@ -1929,7 +1929,7 @@ test "semantic prompt fresh line new prompt" {
|
||||
var t: Terminal = try .init(testing.io, testing.allocator, .{ .cols = 10, .rows = 10 });
|
||||
defer t.deinit(testing.allocator);
|
||||
|
||||
var s: Stream = .initAlloc(testing.allocator, .init(&t));
|
||||
var s: Stream = .init(.{ .allocator = testing.allocator, .handler = .init(&t) });
|
||||
defer s.deinit();
|
||||
|
||||
// Write some text and then send OSC 133;A (fresh_line_new_prompt)
|
||||
@@ -1953,7 +1953,7 @@ test "semantic prompt end of input, then start output" {
|
||||
var t: Terminal = try .init(testing.io, testing.allocator, .{ .cols = 10, .rows = 10 });
|
||||
defer t.deinit(testing.allocator);
|
||||
|
||||
var s: Stream = .initAlloc(testing.allocator, .init(&t));
|
||||
var s: Stream = .init(.{ .allocator = testing.allocator, .handler = .init(&t) });
|
||||
defer s.deinit();
|
||||
|
||||
// Write some text and then send OSC 133;A (fresh_line_new_prompt)
|
||||
@@ -1970,7 +1970,7 @@ test "semantic prompt prompt_start" {
|
||||
var t: Terminal = try .init(testing.io, testing.allocator, .{ .cols = 10, .rows = 10 });
|
||||
defer t.deinit(testing.allocator);
|
||||
|
||||
var s: Stream = .initAlloc(testing.allocator, .init(&t));
|
||||
var s: Stream = .init(.{ .allocator = testing.allocator, .handler = .init(&t) });
|
||||
defer s.deinit();
|
||||
|
||||
// Write some text
|
||||
@@ -1987,7 +1987,7 @@ test "semantic prompt new_command" {
|
||||
var t: Terminal = try .init(testing.io, testing.allocator, .{ .cols = 10, .rows = 10 });
|
||||
defer t.deinit(testing.allocator);
|
||||
|
||||
var s: Stream = .initAlloc(testing.allocator, .init(&t));
|
||||
var s: Stream = .init(.{ .allocator = testing.allocator, .handler = .init(&t) });
|
||||
defer s.deinit();
|
||||
|
||||
// Write some text
|
||||
@@ -2005,7 +2005,7 @@ test "semantic prompt new_command at column zero" {
|
||||
var t: Terminal = try .init(testing.io, testing.allocator, .{ .cols = 10, .rows = 10 });
|
||||
defer t.deinit(testing.allocator);
|
||||
|
||||
var s: Stream = .initAlloc(testing.allocator, .init(&t));
|
||||
var s: Stream = .init(.{ .allocator = testing.allocator, .handler = .init(&t) });
|
||||
defer s.deinit();
|
||||
|
||||
// OSC 133;N when already at column 0 should stay on same line
|
||||
@@ -2019,7 +2019,7 @@ test "semantic prompt end_prompt_start_input_terminate_eol clears on linefeed" {
|
||||
var t: Terminal = try .init(testing.io, testing.allocator, .{ .cols = 10, .rows = 10 });
|
||||
defer t.deinit(testing.allocator);
|
||||
|
||||
var s: Stream = .initAlloc(testing.allocator, .init(&t));
|
||||
var s: Stream = .init(.{ .allocator = testing.allocator, .handler = .init(&t) });
|
||||
defer s.deinit();
|
||||
|
||||
// Set input terminated by EOL
|
||||
@@ -2037,7 +2037,7 @@ test "bell effect callback" {
|
||||
|
||||
// Test bell with null callback (default readonly effects) doesn't crash
|
||||
{
|
||||
var s: Stream = .initAlloc(testing.allocator, .init(&t));
|
||||
var s: Stream = .init(.{ .allocator = testing.allocator, .handler = .init(&t) });
|
||||
defer s.deinit();
|
||||
|
||||
s.nextSlice("\x07");
|
||||
@@ -2064,7 +2064,7 @@ test "bell effect callback" {
|
||||
var handler: Handler = .init(&t);
|
||||
handler.effects.bell = &S.bell;
|
||||
|
||||
var s: Stream = .initAlloc(testing.allocator, handler);
|
||||
var s: Stream = .init(.{ .allocator = testing.allocator, .handler = handler });
|
||||
defer s.deinit();
|
||||
|
||||
s.nextSlice("\x07");
|
||||
@@ -2082,7 +2082,7 @@ test "desktop_notification effect callback" {
|
||||
// A null callback (the default readonly effects) silently ignores
|
||||
// notifications and leaves the terminal usable.
|
||||
{
|
||||
var s: Stream = .initAlloc(testing.allocator, .init(&t));
|
||||
var s: Stream = .init(.{ .allocator = testing.allocator, .handler = .init(&t) });
|
||||
defer s.deinit();
|
||||
|
||||
s.nextSlice("\x1B]9;Ignored\x1B\\AfterNotification");
|
||||
@@ -2114,7 +2114,7 @@ test "desktop_notification effect callback" {
|
||||
var handler: Handler = .init(&t);
|
||||
handler.effects.desktop_notification = &S.desktopNotification;
|
||||
|
||||
var s: Stream = .initAlloc(testing.allocator, handler);
|
||||
var s: Stream = .init(.{ .allocator = testing.allocator, .handler = handler });
|
||||
defer s.deinit();
|
||||
|
||||
// OSC 9 is split across writes and carries only a body.
|
||||
@@ -2138,7 +2138,7 @@ test "progress_report effect callback" {
|
||||
|
||||
// A null callback (the default readonly effects) silently ignores reports.
|
||||
{
|
||||
var s: Stream = .initAlloc(testing.allocator, .init(&t));
|
||||
var s: Stream = .init(.{ .allocator = testing.allocator, .handler = .init(&t) });
|
||||
defer s.deinit();
|
||||
s.nextSlice("\x1B]9;4;1;25\x1B\\");
|
||||
}
|
||||
@@ -2161,7 +2161,7 @@ test "progress_report effect callback" {
|
||||
var handler: Handler = .init(&t);
|
||||
handler.effects.progress_report = &S.progressReport;
|
||||
|
||||
var s: Stream = .initAlloc(testing.allocator, handler);
|
||||
var s: Stream = .init(.{ .allocator = testing.allocator, .handler = handler });
|
||||
defer s.deinit();
|
||||
|
||||
const cases = [_]struct {
|
||||
@@ -2194,7 +2194,7 @@ test "clipboard_write effect callback" {
|
||||
|
||||
// A null callback (the default readonly effects) silently ignores writes.
|
||||
{
|
||||
var s: Stream = .initAlloc(testing.allocator, .init(&t));
|
||||
var s: Stream = .init(.{ .allocator = testing.allocator, .handler = .init(&t) });
|
||||
defer s.deinit();
|
||||
|
||||
s.nextSlice("\x1B]52;c;aGVsbG8=\x1B\\");
|
||||
@@ -2246,7 +2246,7 @@ test "clipboard_write effect callback" {
|
||||
var handler: Handler = .init(&t);
|
||||
handler.effects.clipboard_write = &S.clipboardWrite;
|
||||
|
||||
var s: Stream = .initAlloc(testing.allocator, handler);
|
||||
var s: Stream = .init(.{ .allocator = testing.allocator, .handler = handler });
|
||||
defer s.deinit();
|
||||
|
||||
// Selectors are normalized and payloads are decoded before the callback.
|
||||
@@ -2323,7 +2323,7 @@ test "clipboard_write allocation failure is ignored" {
|
||||
var handler: Handler = .init(&t);
|
||||
handler.effects.clipboard_write = &S.clipboardWrite;
|
||||
|
||||
var s: Stream = .initAlloc(testing.allocator, handler);
|
||||
var s: Stream = .init(.{ .allocator = testing.allocator, .handler = handler });
|
||||
defer s.deinit();
|
||||
|
||||
// Only the decoded scratch data uses the terminal allocator here. Swap in
|
||||
@@ -2344,7 +2344,7 @@ test "request mode DECRQM with write_pty callback" {
|
||||
|
||||
// Without callback, DECRQM should not crash
|
||||
{
|
||||
var s: Stream = .initAlloc(testing.allocator, .init(&t));
|
||||
var s: Stream = .init(.{ .allocator = testing.allocator, .handler = .init(&t) });
|
||||
defer s.deinit();
|
||||
|
||||
// DECRQM for mode 7 (wraparound) — should be silently ignored
|
||||
@@ -2368,7 +2368,7 @@ test "request mode DECRQM with write_pty callback" {
|
||||
var handler: Handler = .init(&t);
|
||||
handler.effects.write_pty = &S.writePty;
|
||||
|
||||
var s: Stream = .initAlloc(testing.allocator, handler);
|
||||
var s: Stream = .init(.{ .allocator = testing.allocator, .handler = handler });
|
||||
defer s.deinit();
|
||||
|
||||
// Wraparound mode (7) is set by default
|
||||
@@ -2396,7 +2396,7 @@ test "stream: CSI W with intermediate but no params" {
|
||||
});
|
||||
defer t.deinit(testing.allocator);
|
||||
|
||||
var s: Stream = .initAlloc(testing.allocator, .init(&t));
|
||||
var s: Stream = .init(.{ .allocator = testing.allocator, .handler = .init(&t) });
|
||||
defer s.deinit();
|
||||
|
||||
s.nextSlice("\x1b[?W");
|
||||
@@ -2417,7 +2417,7 @@ test "window_title effect is called" {
|
||||
var handler: Handler = .init(&t);
|
||||
handler.effects.title_changed = &S.titleChanged;
|
||||
|
||||
var s: Stream = .initAlloc(testing.allocator, handler);
|
||||
var s: Stream = .init(.{ .allocator = testing.allocator, .handler = handler });
|
||||
defer s.deinit();
|
||||
|
||||
// Set window title via OSC 2
|
||||
@@ -2430,7 +2430,7 @@ test "window_title effect not called without callback" {
|
||||
var t: Terminal = try .init(testing.io, testing.allocator, .{ .cols = 80, .rows = 24 });
|
||||
defer t.deinit(testing.allocator);
|
||||
|
||||
var s: Stream = .initAlloc(testing.allocator, .init(&t));
|
||||
var s: Stream = .init(.{ .allocator = testing.allocator, .handler = .init(&t) });
|
||||
defer s.deinit();
|
||||
|
||||
// Should not crash when no callback is set
|
||||
@@ -2461,7 +2461,7 @@ test "window_title effect with empty title" {
|
||||
var handler: Handler = .init(&t);
|
||||
handler.effects.title_changed = &S.titleChanged;
|
||||
|
||||
var s: Stream = .initAlloc(testing.allocator, handler);
|
||||
var s: Stream = .init(.{ .allocator = testing.allocator, .handler = handler });
|
||||
defer s.deinit();
|
||||
|
||||
// Set empty window title
|
||||
@@ -2485,7 +2485,7 @@ test "kitty_keyboard_query" {
|
||||
var handler: Handler = .init(&t);
|
||||
handler.effects.write_pty = &S.writePty;
|
||||
|
||||
var s: Stream = .initAlloc(testing.allocator, handler);
|
||||
var s: Stream = .init(.{ .allocator = testing.allocator, .handler = handler });
|
||||
defer s.deinit();
|
||||
|
||||
// Default kitty keyboard flags should be 0
|
||||
@@ -2514,7 +2514,7 @@ test "xtversion default" {
|
||||
var handler: Handler = .init(&t);
|
||||
handler.effects.write_pty = &S.writePty;
|
||||
|
||||
var s: Stream = .initAlloc(testing.allocator, handler);
|
||||
var s: Stream = .init(.{ .allocator = testing.allocator, .handler = handler });
|
||||
defer s.deinit();
|
||||
|
||||
// Without xtversion effect set, should report "libghostty"
|
||||
@@ -2541,7 +2541,7 @@ test "xtversion with effect" {
|
||||
handler.effects.write_pty = &S.writePty;
|
||||
handler.effects.xtversion = &S.xtversion;
|
||||
|
||||
var s: Stream = .initAlloc(testing.allocator, handler);
|
||||
var s: Stream = .init(.{ .allocator = testing.allocator, .handler = handler });
|
||||
defer s.deinit();
|
||||
|
||||
s.nextSlice("\x1b[>0q");
|
||||
@@ -2567,7 +2567,7 @@ test "xtversion with empty string effect" {
|
||||
handler.effects.write_pty = &S.writePty;
|
||||
handler.effects.xtversion = &S.xtversion;
|
||||
|
||||
var s: Stream = .initAlloc(testing.allocator, handler);
|
||||
var s: Stream = .init(.{ .allocator = testing.allocator, .handler = handler });
|
||||
defer s.deinit();
|
||||
|
||||
// Empty string from effect should fall back to "libghostty"
|
||||
@@ -2594,7 +2594,7 @@ test "size report csi_14_t with effect" {
|
||||
handler.effects.write_pty = &S.writePty;
|
||||
handler.effects.size = &S.getSize;
|
||||
|
||||
var s: Stream = .initAlloc(testing.allocator, handler);
|
||||
var s: Stream = .init(.{ .allocator = testing.allocator, .handler = handler });
|
||||
defer s.deinit();
|
||||
|
||||
// CSI 14 t - report text area size in pixels
|
||||
@@ -2622,7 +2622,7 @@ test "size report csi_16_t with effect" {
|
||||
handler.effects.write_pty = &S.writePty;
|
||||
handler.effects.size = &S.getSize;
|
||||
|
||||
var s: Stream = .initAlloc(testing.allocator, handler);
|
||||
var s: Stream = .init(.{ .allocator = testing.allocator, .handler = handler });
|
||||
defer s.deinit();
|
||||
|
||||
// CSI 16 t - report cell size in pixels
|
||||
@@ -2650,7 +2650,7 @@ test "size report csi_18_t with effect" {
|
||||
handler.effects.write_pty = &S.writePty;
|
||||
handler.effects.size = &S.getSize;
|
||||
|
||||
var s: Stream = .initAlloc(testing.allocator, handler);
|
||||
var s: Stream = .init(.{ .allocator = testing.allocator, .handler = handler });
|
||||
defer s.deinit();
|
||||
|
||||
// CSI 18 t - report text area size in characters
|
||||
@@ -2674,7 +2674,7 @@ test "size report no effect callback" {
|
||||
var handler: Handler = .init(&t);
|
||||
handler.effects.write_pty = &S.writePty;
|
||||
|
||||
var s: Stream = .initAlloc(testing.allocator, handler);
|
||||
var s: Stream = .init(.{ .allocator = testing.allocator, .handler = handler });
|
||||
defer s.deinit();
|
||||
|
||||
// Without size effect, size reports should be silently ignored
|
||||
@@ -2697,7 +2697,7 @@ test "size report csi_21_t title" {
|
||||
var handler: Handler = .init(&t);
|
||||
handler.effects.write_pty = &S.writePty;
|
||||
|
||||
var s: Stream = .initAlloc(testing.allocator, handler);
|
||||
var s: Stream = .init(.{ .allocator = testing.allocator, .handler = handler });
|
||||
defer s.deinit();
|
||||
|
||||
// Set a title first
|
||||
@@ -2724,7 +2724,7 @@ test "enquiry no effect" {
|
||||
var handler: Handler = .init(&t);
|
||||
handler.effects.write_pty = &S.writePty;
|
||||
|
||||
var s: Stream = .initAlloc(testing.allocator, handler);
|
||||
var s: Stream = .init(.{ .allocator = testing.allocator, .handler = handler });
|
||||
defer s.deinit();
|
||||
|
||||
// ENQ without enquiry effect should not write anything
|
||||
@@ -2751,7 +2751,7 @@ test "enquiry with effect" {
|
||||
handler.effects.write_pty = &S.writePty;
|
||||
handler.effects.enquiry = &S.enquiry;
|
||||
|
||||
var s: Stream = .initAlloc(testing.allocator, handler);
|
||||
var s: Stream = .init(.{ .allocator = testing.allocator, .handler = handler });
|
||||
defer s.deinit();
|
||||
|
||||
s.nextSlice("\x05");
|
||||
@@ -2778,7 +2778,7 @@ test "enquiry with empty response" {
|
||||
handler.effects.write_pty = &S.writePty;
|
||||
handler.effects.enquiry = &S.enquiry;
|
||||
|
||||
var s: Stream = .initAlloc(testing.allocator, handler);
|
||||
var s: Stream = .init(.{ .allocator = testing.allocator, .handler = handler });
|
||||
defer s.deinit();
|
||||
|
||||
// Empty enquiry response should not write anything
|
||||
@@ -2803,7 +2803,7 @@ test "device status: operating status" {
|
||||
var handler: Handler = .init(&t);
|
||||
handler.effects.write_pty = &S.writePty;
|
||||
|
||||
var s: Stream = .initAlloc(testing.allocator, handler);
|
||||
var s: Stream = .init(.{ .allocator = testing.allocator, .handler = handler });
|
||||
defer s.deinit();
|
||||
|
||||
// CSI 5 n — operating status report
|
||||
@@ -2828,7 +2828,7 @@ test "device status: cursor position" {
|
||||
var handler: Handler = .init(&t);
|
||||
handler.effects.write_pty = &S.writePty;
|
||||
|
||||
var s: Stream = .initAlloc(testing.allocator, handler);
|
||||
var s: Stream = .init(.{ .allocator = testing.allocator, .handler = handler });
|
||||
defer s.deinit();
|
||||
|
||||
// Default position is 0,0 — reported as 1,1
|
||||
@@ -2858,7 +2858,7 @@ test "device status: cursor position with origin mode" {
|
||||
var handler: Handler = .init(&t);
|
||||
handler.effects.write_pty = &S.writePty;
|
||||
|
||||
var s: Stream = .initAlloc(testing.allocator, handler);
|
||||
var s: Stream = .init(.{ .allocator = testing.allocator, .handler = handler });
|
||||
defer s.deinit();
|
||||
|
||||
// Set scroll region rows 5-20
|
||||
@@ -2894,7 +2894,7 @@ test "device status: color scheme dark" {
|
||||
handler.effects.write_pty = &S.writePty;
|
||||
handler.effects.color_scheme = &S.colorScheme;
|
||||
|
||||
var s: Stream = .initAlloc(testing.allocator, handler);
|
||||
var s: Stream = .init(.{ .allocator = testing.allocator, .handler = handler });
|
||||
defer s.deinit();
|
||||
|
||||
// CSI ? 996 n — color scheme query
|
||||
@@ -2923,7 +2923,7 @@ test "device status: color scheme light" {
|
||||
handler.effects.write_pty = &S.writePty;
|
||||
handler.effects.color_scheme = &S.colorScheme;
|
||||
|
||||
var s: Stream = .initAlloc(testing.allocator, handler);
|
||||
var s: Stream = .init(.{ .allocator = testing.allocator, .handler = handler });
|
||||
defer s.deinit();
|
||||
|
||||
// CSI ? 996 n — color scheme query
|
||||
@@ -2948,7 +2948,7 @@ test "device status: color scheme without callback" {
|
||||
var handler: Handler = .init(&t);
|
||||
handler.effects.write_pty = &S.writePty;
|
||||
|
||||
var s: Stream = .initAlloc(testing.allocator, handler);
|
||||
var s: Stream = .init(.{ .allocator = testing.allocator, .handler = handler });
|
||||
defer s.deinit();
|
||||
|
||||
// Without color_scheme effect, query should be silently ignored
|
||||
@@ -2977,7 +2977,7 @@ test "visibility reports" {
|
||||
var handler: Handler = .init(&t);
|
||||
handler.effects.write_pty = &S.writePty;
|
||||
|
||||
var s: Stream = .initAlloc(testing.allocator, handler);
|
||||
var s: Stream = .init(.{ .allocator = testing.allocator, .handler = handler });
|
||||
defer s.deinit();
|
||||
|
||||
// Mode 2033 is supported and initially disabled.
|
||||
@@ -3011,7 +3011,7 @@ test "device status: readonly ignores all" {
|
||||
var t: Terminal = try .init(testing.io, testing.allocator, .{ .cols = 80, .rows = 24 });
|
||||
defer t.deinit(testing.allocator);
|
||||
|
||||
var s: Stream = .initAlloc(testing.allocator, .init(&t));
|
||||
var s: Stream = .init(.{ .allocator = testing.allocator, .handler = .init(&t) });
|
||||
defer s.deinit();
|
||||
|
||||
// All device status queries should be silently ignored without effects
|
||||
@@ -3048,7 +3048,7 @@ test "device attributes: primary DA" {
|
||||
handler.effects.write_pty = &S.writePty;
|
||||
handler.effects.device_attributes = &S.da;
|
||||
|
||||
var s: Stream = .initAlloc(testing.allocator, handler);
|
||||
var s: Stream = .init(.{ .allocator = testing.allocator, .handler = handler });
|
||||
defer s.deinit();
|
||||
|
||||
s.nextSlice("\x1B[c");
|
||||
@@ -3076,7 +3076,7 @@ test "device attributes: secondary DA" {
|
||||
handler.effects.write_pty = &S.writePty;
|
||||
handler.effects.device_attributes = &S.da;
|
||||
|
||||
var s: Stream = .initAlloc(testing.allocator, handler);
|
||||
var s: Stream = .init(.{ .allocator = testing.allocator, .handler = handler });
|
||||
defer s.deinit();
|
||||
|
||||
s.nextSlice("\x1B[>c");
|
||||
@@ -3104,7 +3104,7 @@ test "device attributes: tertiary DA" {
|
||||
handler.effects.write_pty = &S.writePty;
|
||||
handler.effects.device_attributes = &S.da;
|
||||
|
||||
var s: Stream = .initAlloc(testing.allocator, handler);
|
||||
var s: Stream = .init(.{ .allocator = testing.allocator, .handler = handler });
|
||||
defer s.deinit();
|
||||
|
||||
s.nextSlice("\x1B[=c");
|
||||
@@ -3115,7 +3115,7 @@ test "device attributes: readonly ignores" {
|
||||
var t: Terminal = try .init(testing.io, testing.allocator, .{ .cols = 80, .rows = 24 });
|
||||
defer t.deinit(testing.allocator);
|
||||
|
||||
var s: Stream = .initAlloc(testing.allocator, .init(&t));
|
||||
var s: Stream = .init(.{ .allocator = testing.allocator, .handler = .init(&t) });
|
||||
defer s.deinit();
|
||||
|
||||
// All DA queries should be silently ignored without effects
|
||||
@@ -3160,7 +3160,7 @@ test "device attributes: custom response" {
|
||||
handler.effects.write_pty = &S.writePty;
|
||||
handler.effects.device_attributes = &S.da;
|
||||
|
||||
var s: Stream = .initAlloc(testing.allocator, handler);
|
||||
var s: Stream = .init(.{ .allocator = testing.allocator, .handler = handler });
|
||||
defer s.deinit();
|
||||
|
||||
s.nextSlice("\x1B[c");
|
||||
@@ -3189,7 +3189,7 @@ test "kitty graphics APC response" {
|
||||
var handler: Handler = .init(&t);
|
||||
handler.effects.write_pty = &S.writePty;
|
||||
|
||||
var s: Stream = .initAlloc(testing.allocator, handler);
|
||||
var s: Stream = .init(.{ .allocator = testing.allocator, .handler = handler });
|
||||
defer s.deinit();
|
||||
|
||||
// Send a kitty graphics transmit command with image id 1
|
||||
@@ -3206,7 +3206,7 @@ test "kitty graphics via APC" {
|
||||
defer t.deinit(testing.allocator);
|
||||
|
||||
const handler: Handler = .init(&t);
|
||||
var s: Stream = .initAlloc(testing.allocator, handler);
|
||||
var s: Stream = .init(.{ .allocator = testing.allocator, .handler = handler });
|
||||
defer s.deinit();
|
||||
|
||||
// Send a kitty graphics transmit command via APC:
|
||||
@@ -3218,3 +3218,153 @@ test "kitty graphics via APC" {
|
||||
const img = storage.imageById(1).?;
|
||||
try testing.expectEqual(.rgb, img.format);
|
||||
}
|
||||
|
||||
test "continuation reconstructs standard stream without duplicate effects" {
|
||||
const S = struct {
|
||||
var bell_count: usize = 0;
|
||||
var title_count: usize = 0;
|
||||
var write_count: usize = 0;
|
||||
var notification_count: usize = 0;
|
||||
var clipboard_count: usize = 0;
|
||||
|
||||
fn bell(_: *Handler) void {
|
||||
bell_count += 1;
|
||||
}
|
||||
|
||||
fn titleChanged(_: *Handler) void {
|
||||
title_count += 1;
|
||||
}
|
||||
|
||||
fn writePty(_: *Handler, _: [:0]const u8) void {
|
||||
write_count += 1;
|
||||
}
|
||||
|
||||
fn desktopNotification(
|
||||
_: *Handler,
|
||||
_: Action.ShowDesktopNotification,
|
||||
) void {
|
||||
notification_count += 1;
|
||||
}
|
||||
|
||||
fn clipboardWrite(
|
||||
_: *Handler,
|
||||
_: clipboard.Write,
|
||||
) clipboard.WriteResult {
|
||||
clipboard_count += 1;
|
||||
return .success;
|
||||
}
|
||||
|
||||
fn reset() void {
|
||||
bell_count = 0;
|
||||
title_count = 0;
|
||||
write_count = 0;
|
||||
notification_count = 0;
|
||||
clipboard_count = 0;
|
||||
}
|
||||
};
|
||||
S.reset();
|
||||
|
||||
const committed = "A\n\x07" ++
|
||||
"\x1b]2;title\x1b\\" ++
|
||||
"\x1b[5n" ++
|
||||
"\x1b]9;body\x1b\\" ++
|
||||
"\x1b]52;c;aA==\x1b\\";
|
||||
|
||||
var source_terminal: Terminal = try .init(
|
||||
testing.io,
|
||||
testing.allocator,
|
||||
.{ .cols = 80, .rows = 24 },
|
||||
);
|
||||
defer source_terminal.deinit(testing.allocator);
|
||||
|
||||
var source_handler: Handler = .init(&source_terminal);
|
||||
source_handler.effects.bell = &S.bell;
|
||||
source_handler.effects.title_changed = &S.titleChanged;
|
||||
source_handler.effects.write_pty = &S.writePty;
|
||||
source_handler.effects.desktop_notification = &S.desktopNotification;
|
||||
source_handler.effects.clipboard_write = &S.clipboardWrite;
|
||||
var source = Stream.init(.{
|
||||
.allocator = testing.allocator,
|
||||
.handler = source_handler,
|
||||
.continuation_max_bytes = 1024,
|
||||
});
|
||||
defer source.deinit();
|
||||
|
||||
// Terminal mutation and all callbacks have already committed. The
|
||||
// unfinished CSI is the only input needed to recreate the stream state.
|
||||
source.nextSlice(committed ++ "\x1b[31");
|
||||
try testing.expectEqual(@as(usize, 1), S.bell_count);
|
||||
try testing.expectEqual(@as(usize, 1), S.title_count);
|
||||
try testing.expectEqual(@as(usize, 1), S.write_count);
|
||||
try testing.expectEqual(@as(usize, 1), S.notification_count);
|
||||
try testing.expectEqual(@as(usize, 1), S.clipboard_count);
|
||||
|
||||
var continuation_buf: [1024]u8 = undefined;
|
||||
var continuation_writer: std.Io.Writer = .fixed(&continuation_buf);
|
||||
try source.writeContinuation(&continuation_writer);
|
||||
try testing.expectEqualStrings("\x1b[31", continuation_writer.buffered());
|
||||
|
||||
var restored_terminal: Terminal = try .init(
|
||||
testing.io,
|
||||
testing.allocator,
|
||||
.{ .cols = 80, .rows = 24 },
|
||||
);
|
||||
defer restored_terminal.deinit(testing.allocator);
|
||||
|
||||
// Stand in for restoring the already-committed terminal snapshot.
|
||||
{
|
||||
var snapshot_stream: Stream = .init(.{
|
||||
.allocator = testing.allocator,
|
||||
.handler = .init(&restored_terminal),
|
||||
});
|
||||
defer snapshot_stream.deinit();
|
||||
snapshot_stream.nextSlice(committed);
|
||||
}
|
||||
|
||||
const before = try restored_terminal.plainString(testing.allocator);
|
||||
defer testing.allocator.free(before);
|
||||
const before_x = restored_terminal.screens.active.cursor.x;
|
||||
const before_y = restored_terminal.screens.active.cursor.y;
|
||||
const before_style = restored_terminal.screens.active.cursor.style_id;
|
||||
const before_title = restored_terminal.getTitle().?;
|
||||
|
||||
var restored_handler: Handler = .init(&restored_terminal);
|
||||
restored_handler.effects.bell = &S.bell;
|
||||
restored_handler.effects.title_changed = &S.titleChanged;
|
||||
restored_handler.effects.write_pty = &S.writePty;
|
||||
restored_handler.effects.desktop_notification = &S.desktopNotification;
|
||||
restored_handler.effects.clipboard_write = &S.clipboardWrite;
|
||||
var restored = Stream.init(.{
|
||||
.allocator = testing.allocator,
|
||||
.handler = restored_handler,
|
||||
.continuation_max_bytes = 1024,
|
||||
});
|
||||
defer restored.deinit();
|
||||
|
||||
S.reset();
|
||||
restored.nextSlice(continuation_writer.buffered());
|
||||
try testing.expectEqual(@as(usize, 0), S.bell_count);
|
||||
try testing.expectEqual(@as(usize, 0), S.title_count);
|
||||
try testing.expectEqual(@as(usize, 0), S.write_count);
|
||||
try testing.expectEqual(@as(usize, 0), S.notification_count);
|
||||
try testing.expectEqual(@as(usize, 0), S.clipboard_count);
|
||||
const after = try restored_terminal.plainString(testing.allocator);
|
||||
defer testing.allocator.free(after);
|
||||
try testing.expectEqualStrings(before, after);
|
||||
try testing.expectEqual(before_x, restored_terminal.screens.active.cursor.x);
|
||||
try testing.expectEqual(before_y, restored_terminal.screens.active.cursor.y);
|
||||
try testing.expectEqual(before_style, restored_terminal.screens.active.cursor.style_id);
|
||||
try testing.expectEqualStrings(before_title, restored_terminal.getTitle().?);
|
||||
|
||||
source.nextSlice("mB");
|
||||
restored.nextSlice("mB");
|
||||
const source_text = try source_terminal.plainString(testing.allocator);
|
||||
defer testing.allocator.free(source_text);
|
||||
const restored_text = try restored_terminal.plainString(testing.allocator);
|
||||
defer testing.allocator.free(restored_text);
|
||||
try testing.expectEqualStrings(source_text, restored_text);
|
||||
try testing.expectEqual(
|
||||
source_terminal.screens.active.cursor.style_id,
|
||||
restored_terminal.screens.active.cursor.style_id,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -307,7 +307,10 @@ pub fn init(self: *Termio, alloc: Allocator, opts: termio.Options) !void {
|
||||
.size = opts.size,
|
||||
.backend = backend,
|
||||
.mailbox = opts.mailbox,
|
||||
.terminal_stream = .initAlloc(alloc, handler),
|
||||
.terminal_stream = .init(.{
|
||||
.allocator = alloc,
|
||||
.handler = handler,
|
||||
}),
|
||||
.thread_enter_state = thread_enter_state,
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user