mirror of
https://github.com/ghostty-org/ghostty.git
synced 2025-07-17 09:16:11 +03:00
677 lines
28 KiB
Zig
677 lines
28 KiB
Zig
const std = @import("std");
|
|
const testing = std.testing;
|
|
const Parser = @import("Parser.zig");
|
|
const ansi = @import("ansi.zig");
|
|
const charsets = @import("charsets.zig");
|
|
const csi = @import("csi.zig");
|
|
const sgr = @import("sgr.zig");
|
|
const trace = @import("tracy").trace;
|
|
|
|
const log = std.log.scoped(.stream);
|
|
|
|
/// Returns a type that can process a stream of tty control characters.
|
|
/// This will call various callback functions on type T. Type T only has to
|
|
/// implement the callbacks it cares about; any unimplemented callbacks will
|
|
/// logged at runtime.
|
|
///
|
|
/// To figure out what callbacks exist, search the source for "hasDecl". This
|
|
/// isn't ideal but for now that's the best approach.
|
|
///
|
|
/// This is implemented this way because we purposely do NOT want dynamic
|
|
/// dispatch for performance reasons. The way this is implemented forces
|
|
/// comptime resolution for all function calls.
|
|
pub fn Stream(comptime Handler: type) type {
|
|
return struct {
|
|
const Self = @This();
|
|
|
|
// We use T with @hasDecl so it needs to be a struct. Unwrap the
|
|
// pointer if we were given one.
|
|
const T = switch (@typeInfo(Handler)) {
|
|
.Pointer => |p| p.child,
|
|
else => Handler,
|
|
};
|
|
|
|
handler: Handler,
|
|
parser: Parser = .{},
|
|
|
|
/// Process a string of characters.
|
|
pub fn nextSlice(self: *Self, c: []const u8) !void {
|
|
const tracy = trace(@src());
|
|
defer tracy.end();
|
|
for (c) |single| try self.next(single);
|
|
}
|
|
|
|
/// Process the next character and call any callbacks if necessary.
|
|
pub fn next(self: *Self, c: u8) !void {
|
|
const tracy = trace(@src());
|
|
tracy.value(@intCast(u64, c));
|
|
defer tracy.end();
|
|
|
|
//log.debug("char: {x}", .{c});
|
|
const actions = self.parser.next(c);
|
|
for (actions) |action_opt| {
|
|
// if (action_opt) |action| {
|
|
// if (action != .print) {
|
|
// log.info("action: {}", .{action});
|
|
// }
|
|
// }
|
|
switch (action_opt orelse continue) {
|
|
.print => |p| if (@hasDecl(T, "print")) try self.handler.print(p),
|
|
.execute => |code| try self.execute(code),
|
|
.csi_dispatch => |csi| try self.csiDispatch(csi),
|
|
.esc_dispatch => |esc| try self.escDispatch(esc),
|
|
.osc_dispatch => |cmd| log.warn("unhandled OSC: {}", .{cmd}),
|
|
.dcs_hook => |dcs| log.warn("unhandled DCS hook: {}", .{dcs}),
|
|
.dcs_put => |code| log.warn("unhandled DCS put: {}", .{code}),
|
|
.dcs_unhook => log.warn("unhandled DCS unhook", .{}),
|
|
}
|
|
}
|
|
}
|
|
|
|
pub fn execute(self: *Self, c: u8) !void {
|
|
// log.warn("C0: {}", .{c});
|
|
switch (@intToEnum(ansi.C0, c)) {
|
|
.NUL => {},
|
|
|
|
.ENQ => if (@hasDecl(T, "enquiry"))
|
|
try self.handler.enquiry()
|
|
else
|
|
log.warn("unimplemented execute: {x}", .{c}),
|
|
|
|
.BEL => if (@hasDecl(T, "bell"))
|
|
try self.handler.bell()
|
|
else
|
|
log.warn("unimplemented execute: {x}", .{c}),
|
|
|
|
.BS => if (@hasDecl(T, "backspace"))
|
|
try self.handler.backspace()
|
|
else
|
|
log.warn("unimplemented execute: {x}", .{c}),
|
|
|
|
.HT => if (@hasDecl(T, "horizontalTab"))
|
|
try self.handler.horizontalTab()
|
|
else
|
|
log.warn("unimplemented execute: {x}", .{c}),
|
|
|
|
.LF => if (@hasDecl(T, "linefeed"))
|
|
try self.handler.linefeed()
|
|
else
|
|
log.warn("unimplemented execute: {x}", .{c}),
|
|
|
|
// VT is same as LF
|
|
.VT => if (@hasDecl(T, "linefeed"))
|
|
try self.handler.linefeed()
|
|
else
|
|
log.warn("unimplemented execute: {x}", .{c}),
|
|
|
|
.CR => if (@hasDecl(T, "carriageReturn"))
|
|
try self.handler.carriageReturn()
|
|
else
|
|
log.warn("unimplemented execute: {x}", .{c}),
|
|
|
|
.SO => if (@hasDecl(T, "invokeCharset"))
|
|
try self.handler.invokeCharset(.GL, .G1, false)
|
|
else
|
|
log.warn("unimplemented invokeCharset: {x}", .{c}),
|
|
|
|
.SI => if (@hasDecl(T, "invokeCharset"))
|
|
try self.handler.invokeCharset(.GL, .G0, false)
|
|
else
|
|
log.warn("unimplemented invokeCharset: {x}", .{c}),
|
|
}
|
|
}
|
|
|
|
fn csiDispatch(self: *Self, input: Parser.Action.CSI) !void {
|
|
// Handles aliases first
|
|
const action = switch (input.final) {
|
|
// Alias for set cursor position
|
|
'f' => blk: {
|
|
var copy = input;
|
|
copy.final = 'H';
|
|
break :blk copy;
|
|
},
|
|
|
|
else => input,
|
|
};
|
|
|
|
switch (action.final) {
|
|
// CUU - Cursor Up
|
|
'A' => if (@hasDecl(T, "setCursorUp")) try self.handler.setCursorUp(
|
|
switch (action.params.len) {
|
|
0 => 1,
|
|
1 => action.params[0],
|
|
else => {
|
|
log.warn("invalid cursor up command: {}", .{action});
|
|
return;
|
|
},
|
|
},
|
|
) else log.warn("unimplemented CSI callback: {}", .{action}),
|
|
|
|
// CUD - Cursor Down
|
|
'B' => if (@hasDecl(T, "setCursorDown")) try self.handler.setCursorDown(
|
|
switch (action.params.len) {
|
|
0 => 1,
|
|
1 => action.params[0],
|
|
else => {
|
|
log.warn("invalid cursor down command: {}", .{action});
|
|
return;
|
|
},
|
|
},
|
|
) else log.warn("unimplemented CSI callback: {}", .{action}),
|
|
|
|
// CUF - Cursor Right
|
|
'C' => if (@hasDecl(T, "setCursorRight")) try self.handler.setCursorRight(
|
|
switch (action.params.len) {
|
|
0 => 1,
|
|
1 => action.params[0],
|
|
else => {
|
|
log.warn("invalid cursor right command: {}", .{action});
|
|
return;
|
|
},
|
|
},
|
|
) else log.warn("unimplemented CSI callback: {}", .{action}),
|
|
|
|
// CUB - Cursor Left
|
|
'D' => if (@hasDecl(T, "setCursorLeft")) try self.handler.setCursorLeft(
|
|
switch (action.params.len) {
|
|
0 => 1,
|
|
1 => action.params[0],
|
|
else => {
|
|
log.warn("invalid cursor left command: {}", .{action});
|
|
return;
|
|
},
|
|
},
|
|
) else log.warn("unimplemented CSI callback: {}", .{action}),
|
|
|
|
// HPA - Cursor Horizontal Position Absolute
|
|
// TODO: test
|
|
'G', '`' => if (@hasDecl(T, "setCursorCol")) switch (action.params.len) {
|
|
0 => try self.handler.setCursorCol(1),
|
|
1 => try self.handler.setCursorCol(action.params[0]),
|
|
else => log.warn("invalid HPA command: {}", .{action}),
|
|
} else log.warn("unimplemented CSI callback: {}", .{action}),
|
|
|
|
// CUP - Set Cursor Position.
|
|
// TODO: test
|
|
'H' => if (@hasDecl(T, "setCursorPos")) switch (action.params.len) {
|
|
0 => try self.handler.setCursorPos(1, 1),
|
|
1 => try self.handler.setCursorPos(action.params[0], 1),
|
|
2 => try self.handler.setCursorPos(action.params[0], action.params[1]),
|
|
else => log.warn("invalid CUP command: {}", .{action}),
|
|
} else log.warn("unimplemented CSI callback: {}", .{action}),
|
|
|
|
// Erase Display
|
|
// TODO: test
|
|
'J' => if (@hasDecl(T, "eraseDisplay")) try self.handler.eraseDisplay(
|
|
switch (action.params.len) {
|
|
0 => .below,
|
|
1 => mode: {
|
|
// TODO: use meta to get enum max
|
|
if (action.params[0] > 3) {
|
|
log.warn("invalid erase display command: {}", .{action});
|
|
return;
|
|
}
|
|
|
|
break :mode @intToEnum(
|
|
csi.EraseDisplay,
|
|
action.params[0],
|
|
);
|
|
},
|
|
else => {
|
|
log.warn("invalid erase display command: {}", .{action});
|
|
return;
|
|
},
|
|
},
|
|
) else log.warn("unimplemented CSI callback: {}", .{action}),
|
|
|
|
// Erase Line
|
|
'K' => if (@hasDecl(T, "eraseLine")) try self.handler.eraseLine(
|
|
switch (action.params.len) {
|
|
0 => .right,
|
|
1 => mode: {
|
|
// TODO: use meta to get enum max
|
|
if (action.params[0] > 3) {
|
|
log.warn("invalid erase line command: {}", .{action});
|
|
return;
|
|
}
|
|
|
|
break :mode @intToEnum(
|
|
csi.EraseLine,
|
|
action.params[0],
|
|
);
|
|
},
|
|
else => {
|
|
log.warn("invalid erase line command: {}", .{action});
|
|
return;
|
|
},
|
|
},
|
|
) else log.warn("unimplemented CSI callback: {}", .{action}),
|
|
|
|
// IL - Insert Lines
|
|
// TODO: test
|
|
'L' => if (@hasDecl(T, "insertLines")) switch (action.params.len) {
|
|
0 => try self.handler.insertLines(1),
|
|
1 => try self.handler.insertLines(action.params[0]),
|
|
else => log.warn("invalid IL command: {}", .{action}),
|
|
} else log.warn("unimplemented CSI callback: {}", .{action}),
|
|
|
|
// DL - Delete Lines
|
|
// TODO: test
|
|
'M' => if (@hasDecl(T, "deleteLines")) switch (action.params.len) {
|
|
0 => try self.handler.deleteLines(1),
|
|
1 => try self.handler.deleteLines(action.params[0]),
|
|
else => log.warn("invalid DL command: {}", .{action}),
|
|
} else log.warn("unimplemented CSI callback: {}", .{action}),
|
|
|
|
// Delete Character (DCH)
|
|
'P' => if (@hasDecl(T, "deleteChars")) try self.handler.deleteChars(
|
|
switch (action.params.len) {
|
|
0 => 1,
|
|
1 => action.params[0],
|
|
else => {
|
|
log.warn("invalid delete characters command: {}", .{action});
|
|
return;
|
|
},
|
|
},
|
|
) else log.warn("unimplemented CSI callback: {}", .{action}),
|
|
|
|
// Scroll Up (SD)
|
|
'S' => if (@hasDecl(T, "scrollUp")) try self.handler.scrollUp(
|
|
switch (action.params.len) {
|
|
0 => 1,
|
|
1 => action.params[0],
|
|
else => {
|
|
log.warn("invalid scroll up command: {}", .{action});
|
|
return;
|
|
},
|
|
},
|
|
) else log.warn("unimplemented CSI callback: {}", .{action}),
|
|
|
|
// Scroll Down (SD)
|
|
'T' => if (@hasDecl(T, "scrollDown")) try self.handler.scrollDown(
|
|
switch (action.params.len) {
|
|
0 => 1,
|
|
1 => action.params[0],
|
|
else => {
|
|
log.warn("invalid scroll down command: {}", .{action});
|
|
return;
|
|
},
|
|
},
|
|
) else log.warn("unimplemented CSI callback: {}", .{action}),
|
|
|
|
// Erase Characters (ECH)
|
|
'X' => if (@hasDecl(T, "eraseChars")) try self.handler.eraseChars(
|
|
switch (action.params.len) {
|
|
0 => 1,
|
|
1 => action.params[0],
|
|
else => {
|
|
log.warn("invalid erase characters command: {}", .{action});
|
|
return;
|
|
},
|
|
},
|
|
) else log.warn("unimplemented CSI callback: {}", .{action}),
|
|
|
|
// c - Device Attributes (DA1)
|
|
'c' => if (@hasDecl(T, "deviceAttributes")) {
|
|
const req: ansi.DeviceAttributeReq = switch (action.intermediates.len) {
|
|
0 => ansi.DeviceAttributeReq.primary,
|
|
1 => switch (action.intermediates[0]) {
|
|
'>' => ansi.DeviceAttributeReq.secondary,
|
|
'=' => ansi.DeviceAttributeReq.tertiary,
|
|
else => null,
|
|
},
|
|
else => @as(?ansi.DeviceAttributeReq, null),
|
|
} orelse {
|
|
log.warn("invalid device attributes command: {}", .{action});
|
|
return;
|
|
};
|
|
|
|
try self.handler.deviceAttributes(req, action.params);
|
|
} else log.warn("unimplemented CSI callback: {}", .{action}),
|
|
|
|
// VPA - Cursor Vertical Position Absolute
|
|
'd' => if (@hasDecl(T, "setCursorRow")) try self.handler.setCursorRow(
|
|
switch (action.params.len) {
|
|
0 => 1,
|
|
1 => action.params[0],
|
|
else => {
|
|
log.warn("invalid VPA command: {}", .{action});
|
|
return;
|
|
},
|
|
},
|
|
) else log.warn("unimplemented CSI callback: {}", .{action}),
|
|
|
|
// TBC - Tab Clear
|
|
// TODO: test
|
|
'g' => if (@hasDecl(T, "tabClear")) try self.handler.tabClear(
|
|
switch (action.params.len) {
|
|
1 => @intToEnum(csi.TabClear, action.params[0]),
|
|
else => {
|
|
log.warn("invalid tab clear command: {}", .{action});
|
|
return;
|
|
},
|
|
},
|
|
) else log.warn("unimplemented CSI callback: {}", .{action}),
|
|
|
|
// SM - Set Mode
|
|
'h' => if (@hasDecl(T, "setMode")) {
|
|
for (action.params) |mode|
|
|
try self.handler.setMode(@intToEnum(ansi.Mode, mode), true);
|
|
} else log.warn("unimplemented CSI callback: {}", .{action}),
|
|
|
|
// RM - Reset Mode
|
|
'l' => if (@hasDecl(T, "setMode")) {
|
|
for (action.params) |mode|
|
|
try self.handler.setMode(@intToEnum(ansi.Mode, mode), false);
|
|
} else log.warn("unimplemented CSI callback: {}", .{action}),
|
|
|
|
// SGR - Select Graphic Rendition
|
|
'm' => if (@hasDecl(T, "setAttribute")) {
|
|
var p: sgr.Parser = .{ .params = action.params };
|
|
while (p.next()) |attr| try self.handler.setAttribute(attr);
|
|
} else log.warn("unimplemented CSI callback: {}", .{action}),
|
|
|
|
// CPR - Request Cursor Postion Report
|
|
// TODO: test
|
|
'n' => if (@hasDecl(T, "deviceStatusReport")) try self.handler.deviceStatusReport(
|
|
switch (action.params.len) {
|
|
1 => @intToEnum(ansi.DeviceStatusReq, action.params[0]),
|
|
else => {
|
|
log.warn("invalid erase characters command: {}", .{action});
|
|
return;
|
|
},
|
|
},
|
|
) else log.warn("unimplemented CSI callback: {}", .{action}),
|
|
|
|
// DECSCUSR - Select Cursor Style
|
|
// TODO: test
|
|
'q' => if (@hasDecl(T, "setCursorStyle")) try self.handler.setCursorStyle(
|
|
switch (action.params.len) {
|
|
0 => ansi.CursorStyle.default,
|
|
1 => @intToEnum(ansi.CursorStyle, action.params[0]),
|
|
else => {
|
|
log.warn("invalid set curor style command: {}", .{action});
|
|
return;
|
|
},
|
|
},
|
|
) else log.warn("unimplemented CSI callback: {}", .{action}),
|
|
|
|
// DECSTBM - Set Top and Bottom Margins
|
|
// TODO: test
|
|
'r' => if (@hasDecl(T, "setTopAndBottomMargin")) switch (action.params.len) {
|
|
0 => try self.handler.setTopAndBottomMargin(0, 0),
|
|
1 => try self.handler.setTopAndBottomMargin(action.params[0], 0),
|
|
2 => try self.handler.setTopAndBottomMargin(action.params[0], action.params[1]),
|
|
else => log.warn("invalid DECSTBM command: {}", .{action}),
|
|
} else log.warn("unimplemented CSI callback: {}", .{action}),
|
|
|
|
else => if (@hasDecl(T, "csiUnimplemented"))
|
|
try self.handler.csiUnimplemented(action)
|
|
else
|
|
log.warn("unimplemented CSI action: {}", .{action}),
|
|
|
|
// ICH - Insert Blanks
|
|
// TODO: test
|
|
'@' => if (@hasDecl(T, "insertBlanks")) switch (action.params.len) {
|
|
0 => try self.handler.insertBlanks(1),
|
|
1 => try self.handler.insertBlanks(action.params[0]),
|
|
else => log.warn("invalid ICH command: {}", .{action}),
|
|
} else log.warn("unimplemented CSI callback: {}", .{action}),
|
|
|
|
// DECSASD - Select Active Status Display
|
|
'}' => {
|
|
const success = decsasd: {
|
|
// Verify we're getting a DECSASD command
|
|
if (action.intermediates.len != 1 or action.intermediates[0] != '$')
|
|
break :decsasd false;
|
|
if (action.params.len != 1)
|
|
break :decsasd false;
|
|
if (!@hasDecl(T, "setActiveStatusDisplay"))
|
|
break :decsasd false;
|
|
|
|
try self.handler.setActiveStatusDisplay(
|
|
@intToEnum(ansi.StatusDisplay, action.params[0]),
|
|
);
|
|
break :decsasd true;
|
|
};
|
|
|
|
if (!success) log.warn("unimplemented CSI callback: {}", .{action});
|
|
},
|
|
}
|
|
}
|
|
|
|
fn configureCharset(
|
|
self: Self,
|
|
intermediates: []const u8,
|
|
set: charsets.Charset,
|
|
) !void {
|
|
if (intermediates.len != 1) {
|
|
log.warn("invalid charset intermediate: {any}", .{intermediates});
|
|
return;
|
|
}
|
|
|
|
const slot: charsets.Slots = switch (intermediates[0]) {
|
|
// TODO: support slots '-', '.', '/'
|
|
|
|
'(' => .G0,
|
|
')' => .G1,
|
|
'*' => .G2,
|
|
'+' => .G3,
|
|
else => {
|
|
log.warn("invalid charset intermediate: {any}", .{intermediates});
|
|
return;
|
|
},
|
|
};
|
|
|
|
if (@hasDecl(T, "configureCharset")) {
|
|
try self.handler.configureCharset(slot, set);
|
|
return;
|
|
}
|
|
|
|
log.warn("unimplemented configureCharset callback slot={} set={}", .{
|
|
slot,
|
|
set,
|
|
});
|
|
}
|
|
|
|
fn escDispatch(
|
|
self: *Self,
|
|
action: Parser.Action.ESC,
|
|
) !void {
|
|
switch (action.final) {
|
|
// Charsets
|
|
'B' => try self.configureCharset(action.intermediates, .ascii),
|
|
'A' => try self.configureCharset(action.intermediates, .british),
|
|
'0' => try self.configureCharset(action.intermediates, .dec_special),
|
|
|
|
// DECSC - Save Cursor
|
|
'7' => if (@hasDecl(T, "saveCursor")) switch (action.intermediates.len) {
|
|
0 => try self.handler.saveCursor(),
|
|
else => {
|
|
log.warn("invalid command: {}", .{action});
|
|
return;
|
|
},
|
|
} else log.warn("unimplemented ESC callback: {}", .{action}),
|
|
|
|
'8' => blk: {
|
|
switch (action.intermediates.len) {
|
|
// DECRC - Restore Cursor
|
|
0 => if (@hasDecl(T, "restoreCursor")) {
|
|
try self.handler.restoreCursor();
|
|
break :blk {};
|
|
} else log.warn("unimplemented restore cursor callback: {}", .{action}),
|
|
|
|
1 => switch (action.intermediates[0]) {
|
|
// DECALN - Fill Screen with E
|
|
'#' => if (@hasDecl(T, "decaln")) {
|
|
try self.handler.decaln();
|
|
break :blk {};
|
|
} else log.warn("unimplemented ESC callback: {}", .{action}),
|
|
|
|
else => {},
|
|
},
|
|
|
|
else => {}, // fall through
|
|
}
|
|
|
|
log.warn("unimplemented ESC action: {}", .{action});
|
|
},
|
|
|
|
// IND - Index
|
|
'D' => if (@hasDecl(T, "index")) switch (action.intermediates.len) {
|
|
0 => try self.handler.index(),
|
|
else => {
|
|
log.warn("invalid index command: {}", .{action});
|
|
return;
|
|
},
|
|
} else log.warn("unimplemented ESC callback: {}", .{action}),
|
|
|
|
// NEL - Next Line
|
|
'E' => if (@hasDecl(T, "nextLine")) switch (action.intermediates.len) {
|
|
0 => try self.handler.nextLine(),
|
|
else => {
|
|
log.warn("invalid next line command: {}", .{action});
|
|
return;
|
|
},
|
|
} else log.warn("unimplemented ESC callback: {}", .{action}),
|
|
|
|
// HTS - Horizontal Tab Set
|
|
'H' => if (@hasDecl(T, "tabSet"))
|
|
try self.handler.tabSet()
|
|
else
|
|
log.warn("unimplemented tab set callback: {}", .{action}),
|
|
|
|
// RI - Reverse Index
|
|
'M' => if (@hasDecl(T, "reverseIndex")) switch (action.intermediates.len) {
|
|
0 => try self.handler.reverseIndex(),
|
|
else => {
|
|
log.warn("invalid reverse index command: {}", .{action});
|
|
return;
|
|
},
|
|
} else log.warn("unimplemented ESC callback: {}", .{action}),
|
|
|
|
// SS2 - Single Shift 2
|
|
'N' => if (@hasDecl(T, "invokeCharset")) switch (action.intermediates.len) {
|
|
0 => try self.handler.invokeCharset(.GL, .G2, true),
|
|
else => {
|
|
log.warn("invalid single shift 2 command: {}", .{action});
|
|
return;
|
|
},
|
|
} else log.warn("unimplemented invokeCharset: {}", .{action}),
|
|
|
|
// SS3 - Single Shift 3
|
|
'O' => if (@hasDecl(T, "invokeCharset")) switch (action.intermediates.len) {
|
|
0 => try self.handler.invokeCharset(.GL, .G3, true),
|
|
else => {
|
|
log.warn("invalid single shift 3 command: {}", .{action});
|
|
return;
|
|
},
|
|
} else log.warn("unimplemented invokeCharset: {}", .{action}),
|
|
|
|
// LS2 - Locking Shift 2
|
|
'n' => if (@hasDecl(T, "invokeCharset")) switch (action.intermediates.len) {
|
|
0 => try self.handler.invokeCharset(.GL, .G2, false),
|
|
else => {
|
|
log.warn("invalid single shift 2 command: {}", .{action});
|
|
return;
|
|
},
|
|
} else log.warn("unimplemented invokeCharset: {}", .{action}),
|
|
|
|
// LS3 - Locking Shift 3
|
|
'o' => if (@hasDecl(T, "invokeCharset")) switch (action.intermediates.len) {
|
|
0 => try self.handler.invokeCharset(.GL, .G3, false),
|
|
else => {
|
|
log.warn("invalid single shift 3 command: {}", .{action});
|
|
return;
|
|
},
|
|
} else log.warn("unimplemented invokeCharset: {}", .{action}),
|
|
|
|
// LS1R - Locking Shift 1 Right
|
|
'~' => if (@hasDecl(T, "invokeCharset")) switch (action.intermediates.len) {
|
|
0 => try self.handler.invokeCharset(.GR, .G1, false),
|
|
else => {
|
|
log.warn("invalid locking shift 1 right command: {}", .{action});
|
|
return;
|
|
},
|
|
} else log.warn("unimplemented invokeCharset: {}", .{action}),
|
|
|
|
// LS2R - Locking Shift 2 Right
|
|
'}' => if (@hasDecl(T, "invokeCharset")) switch (action.intermediates.len) {
|
|
0 => try self.handler.invokeCharset(.GR, .G2, false),
|
|
else => {
|
|
log.warn("invalid locking shift 2 right command: {}", .{action});
|
|
return;
|
|
},
|
|
} else log.warn("unimplemented invokeCharset: {}", .{action}),
|
|
|
|
// LS3R - Locking Shift 3 Right
|
|
'|' => if (@hasDecl(T, "invokeCharset")) switch (action.intermediates.len) {
|
|
0 => try self.handler.invokeCharset(.GR, .G3, false),
|
|
else => {
|
|
log.warn("invalid locking shift 3 right command: {}", .{action});
|
|
return;
|
|
},
|
|
} else log.warn("unimplemented invokeCharset: {}", .{action}),
|
|
|
|
else => if (@hasDecl(T, "escUnimplemented"))
|
|
try self.handler.escUnimplemented(action)
|
|
else
|
|
log.warn("unimplemented ESC action: {}", .{action}),
|
|
}
|
|
}
|
|
};
|
|
}
|
|
|
|
test "stream: print" {
|
|
const H = struct {
|
|
c: ?u21 = 0,
|
|
|
|
pub fn print(self: *@This(), c: u21) !void {
|
|
self.c = c;
|
|
}
|
|
};
|
|
|
|
var s: Stream(H) = .{ .handler = .{} };
|
|
try s.next('x');
|
|
try testing.expectEqual(@as(u21, 'x'), s.handler.c.?);
|
|
}
|
|
|
|
test "stream: cursor right (CUF)" {
|
|
const H = struct {
|
|
amount: u16 = 0,
|
|
|
|
pub fn setCursorRight(self: *@This(), v: u16) !void {
|
|
self.amount = v;
|
|
}
|
|
};
|
|
|
|
var s: Stream(H) = .{ .handler = .{} };
|
|
try s.nextSlice("\x1B[C");
|
|
try testing.expectEqual(@as(u16, 1), s.handler.amount);
|
|
|
|
try s.nextSlice("\x1B[5C");
|
|
try testing.expectEqual(@as(u16, 5), s.handler.amount);
|
|
|
|
s.handler.amount = 0;
|
|
try s.nextSlice("\x1B[5;4C");
|
|
try testing.expectEqual(@as(u16, 0), s.handler.amount);
|
|
}
|
|
|
|
test "stream: set mode (SM) and reset mode (RM)" {
|
|
const H = struct {
|
|
mode: ansi.Mode = @intToEnum(ansi.Mode, 0),
|
|
|
|
pub fn setMode(self: *@This(), mode: ansi.Mode, v: bool) !void {
|
|
self.mode = @intToEnum(ansi.Mode, 0);
|
|
if (v) self.mode = mode;
|
|
}
|
|
};
|
|
|
|
var s: Stream(H) = .{ .handler = .{} };
|
|
try s.nextSlice("\x1B[?6h");
|
|
try testing.expectEqual(@as(ansi.Mode, .origin), s.handler.mode);
|
|
|
|
try s.nextSlice("\x1B[?6l");
|
|
try testing.expectEqual(@intToEnum(ansi.Mode, 0), s.handler.mode);
|
|
}
|