mirror of
https://github.com/ghostty-org/ghostty.git
synced 2026-04-14 11:35:48 +00:00
Merge remote-tracking branch 'upstream/main' into harfbuzz-positions
This commit is contained in:
@@ -63,24 +63,26 @@ typedef enum {
|
||||
GHOSTTY_OSC_COMMAND_INVALID = 0,
|
||||
GHOSTTY_OSC_COMMAND_CHANGE_WINDOW_TITLE = 1,
|
||||
GHOSTTY_OSC_COMMAND_CHANGE_WINDOW_ICON = 2,
|
||||
GHOSTTY_OSC_COMMAND_PROMPT_START = 3,
|
||||
GHOSTTY_OSC_COMMAND_PROMPT_END = 4,
|
||||
GHOSTTY_OSC_COMMAND_END_OF_INPUT = 5,
|
||||
GHOSTTY_OSC_COMMAND_END_OF_COMMAND = 6,
|
||||
GHOSTTY_OSC_COMMAND_CLIPBOARD_CONTENTS = 7,
|
||||
GHOSTTY_OSC_COMMAND_REPORT_PWD = 8,
|
||||
GHOSTTY_OSC_COMMAND_MOUSE_SHAPE = 9,
|
||||
GHOSTTY_OSC_COMMAND_COLOR_OPERATION = 10,
|
||||
GHOSTTY_OSC_COMMAND_KITTY_COLOR_PROTOCOL = 11,
|
||||
GHOSTTY_OSC_COMMAND_SHOW_DESKTOP_NOTIFICATION = 12,
|
||||
GHOSTTY_OSC_COMMAND_HYPERLINK_START = 13,
|
||||
GHOSTTY_OSC_COMMAND_HYPERLINK_END = 14,
|
||||
GHOSTTY_OSC_COMMAND_CONEMU_SLEEP = 15,
|
||||
GHOSTTY_OSC_COMMAND_CONEMU_SHOW_MESSAGE_BOX = 16,
|
||||
GHOSTTY_OSC_COMMAND_CONEMU_CHANGE_TAB_TITLE = 17,
|
||||
GHOSTTY_OSC_COMMAND_CONEMU_PROGRESS_REPORT = 18,
|
||||
GHOSTTY_OSC_COMMAND_CONEMU_WAIT_INPUT = 19,
|
||||
GHOSTTY_OSC_COMMAND_CONEMU_GUIMACRO = 20,
|
||||
GHOSTTY_OSC_COMMAND_SEMANTIC_PROMPT = 3,
|
||||
GHOSTTY_OSC_COMMAND_CLIPBOARD_CONTENTS = 4,
|
||||
GHOSTTY_OSC_COMMAND_REPORT_PWD = 5,
|
||||
GHOSTTY_OSC_COMMAND_MOUSE_SHAPE = 6,
|
||||
GHOSTTY_OSC_COMMAND_COLOR_OPERATION = 7,
|
||||
GHOSTTY_OSC_COMMAND_KITTY_COLOR_PROTOCOL = 8,
|
||||
GHOSTTY_OSC_COMMAND_SHOW_DESKTOP_NOTIFICATION = 9,
|
||||
GHOSTTY_OSC_COMMAND_HYPERLINK_START = 10,
|
||||
GHOSTTY_OSC_COMMAND_HYPERLINK_END = 11,
|
||||
GHOSTTY_OSC_COMMAND_CONEMU_SLEEP = 12,
|
||||
GHOSTTY_OSC_COMMAND_CONEMU_SHOW_MESSAGE_BOX = 13,
|
||||
GHOSTTY_OSC_COMMAND_CONEMU_CHANGE_TAB_TITLE = 14,
|
||||
GHOSTTY_OSC_COMMAND_CONEMU_PROGRESS_REPORT = 15,
|
||||
GHOSTTY_OSC_COMMAND_CONEMU_WAIT_INPUT = 16,
|
||||
GHOSTTY_OSC_COMMAND_CONEMU_GUIMACRO = 17,
|
||||
GHOSTTY_OSC_COMMAND_CONEMU_RUN_PROCESS = 18,
|
||||
GHOSTTY_OSC_COMMAND_CONEMU_OUTPUT_ENVIRONMENT_VARIABLE = 19,
|
||||
GHOSTTY_OSC_COMMAND_CONEMU_XTERM_EMULATION = 20,
|
||||
GHOSTTY_OSC_COMMAND_CONEMU_COMMENT = 21,
|
||||
GHOSTTY_OSC_COMMAND_KITTY_TEXT_SIZING = 22,
|
||||
} GhosttyOscCommandType;
|
||||
|
||||
/**
|
||||
|
||||
@@ -733,6 +733,7 @@ pub const Application = extern struct {
|
||||
.toggle_split_zoom => return Action.toggleSplitZoom(target),
|
||||
.show_on_screen_keyboard => return Action.showOnScreenKeyboard(target),
|
||||
.command_finished => return Action.commandFinished(target, value),
|
||||
.readonly => return Action.setReadonly(target, value),
|
||||
|
||||
.start_search => Action.startSearch(target, value),
|
||||
.end_search => Action.endSearch(target),
|
||||
@@ -753,7 +754,6 @@ pub const Application = extern struct {
|
||||
.check_for_updates,
|
||||
.undo,
|
||||
.redo,
|
||||
.readonly,
|
||||
=> {
|
||||
log.warn("unimplemented action={}", .{action});
|
||||
return false;
|
||||
@@ -2678,6 +2678,15 @@ const Action = struct {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn setReadonly(target: apprt.Target, value: apprt.Action.Value(.readonly)) bool {
|
||||
switch (target) {
|
||||
.app => return false,
|
||||
.surface => |surface| {
|
||||
return surface.rt_surface.gobj().setReadonly(value);
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
pub fn keySequence(target: apprt.Target, value: apprt.Action.Value(.key_sequence)) bool {
|
||||
switch (target) {
|
||||
.app => {
|
||||
|
||||
@@ -400,6 +400,25 @@ pub const Surface = extern struct {
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
pub const readonly = struct {
|
||||
pub const name = "readonly";
|
||||
const impl = gobject.ext.defineProperty(
|
||||
name,
|
||||
Self,
|
||||
bool,
|
||||
.{
|
||||
.default = false,
|
||||
.accessor = gobject.ext.typedAccessor(
|
||||
Self,
|
||||
bool,
|
||||
.{
|
||||
.getter = getReadonly,
|
||||
},
|
||||
),
|
||||
},
|
||||
);
|
||||
};
|
||||
};
|
||||
|
||||
pub const signals = struct {
|
||||
@@ -1106,6 +1125,20 @@ pub const Surface = extern struct {
|
||||
return true;
|
||||
}
|
||||
|
||||
/// Get the readonly state from the core surface.
|
||||
pub fn getReadonly(self: *Self) bool {
|
||||
const priv: *Private = self.private();
|
||||
const surface = priv.core_surface orelse return false;
|
||||
return surface.readonly;
|
||||
}
|
||||
|
||||
/// Notify anyone interested that the readonly status has changed.
|
||||
pub fn setReadonly(self: *Self, _: apprt.Action.Value(.readonly)) bool {
|
||||
self.as(gobject.Object).notifyByPspec(properties.readonly.impl.param_spec);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/// Key press event (press or release).
|
||||
///
|
||||
/// At a high level, we want to construct an `input.KeyEvent` and
|
||||
@@ -3480,6 +3513,7 @@ pub const Surface = extern struct {
|
||||
properties.@"title-override".impl,
|
||||
properties.zoom.impl,
|
||||
properties.@"is-split".impl,
|
||||
properties.readonly.impl,
|
||||
|
||||
// For Gtk.Scrollable
|
||||
properties.hadjustment.impl,
|
||||
|
||||
@@ -134,6 +134,16 @@ label.resize-overlay {
|
||||
border-style: solid;
|
||||
}
|
||||
|
||||
.surface .readonly_overlay {
|
||||
/* Should be the equivalent of the following SwiftUI color: */
|
||||
/* Color(hue: 0.08, saturation: 0.5, brightness: 0.8) */
|
||||
color: hsl(25 50 75);
|
||||
padding: 8px 8px 8px 8px;
|
||||
margin: 8px 8px 8px 8px;
|
||||
border-radius: 6px 6px 6px 6px;
|
||||
outline-style: solid;
|
||||
outline-width: 1px;
|
||||
}
|
||||
/*
|
||||
* Command Palette
|
||||
*/
|
||||
|
||||
@@ -71,6 +71,39 @@ Overlay terminal_page {
|
||||
}
|
||||
};
|
||||
|
||||
[overlay]
|
||||
Revealer {
|
||||
reveal-child: bind template.readonly;
|
||||
transition-type: crossfade;
|
||||
transition-duration: 500;
|
||||
// Revealers take up the full size, we need this to not capture events.
|
||||
can-focus: false;
|
||||
can-target: false;
|
||||
focusable: false;
|
||||
|
||||
Box readonly_overlay {
|
||||
styles [
|
||||
"readonly_overlay",
|
||||
]
|
||||
|
||||
// TODO: the tooltip doesn't actually work, but keep it here for now so
|
||||
// that we can get the tooltip text translated.
|
||||
has-tooltip: true;
|
||||
tooltip-text: _("This terminal is in read-only mode. You can still view, select, and scroll through the content, but no input events will be sent to the running application.");
|
||||
halign: end;
|
||||
valign: start;
|
||||
spacing: 6;
|
||||
|
||||
Image {
|
||||
icon-name: "changes-prevent-symbolic";
|
||||
}
|
||||
|
||||
Label {
|
||||
label: _("Read-only");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[overlay]
|
||||
ProgressBar progress_bar_overlay {
|
||||
styles [
|
||||
|
||||
@@ -41,74 +41,8 @@ pub const Command = union(Key) {
|
||||
/// in the log.
|
||||
change_window_icon: [:0]const u8,
|
||||
|
||||
/// First do a fresh-line. Then start a new command, and enter prompt mode:
|
||||
/// Subsequent text (until a OSC "133;B" or OSC "133;I" command) is a
|
||||
/// prompt string (as if followed by OSC 133;P;k=i\007). Note: I've noticed
|
||||
/// not all shells will send the prompt end code.
|
||||
prompt_start: struct {
|
||||
/// "aid" is an optional "application identifier" that helps disambiguate
|
||||
/// nested shell sessions. It can be anything but is usually a process ID.
|
||||
aid: ?[:0]const u8 = null,
|
||||
/// "kind" tells us which kind of semantic prompt sequence this is:
|
||||
/// - primary: normal, left-aligned first-line prompt (initial, default)
|
||||
/// - continuation: an editable continuation line
|
||||
/// - secondary: a non-editable continuation line
|
||||
/// - right: a right-aligned prompt that may need adjustment during reflow
|
||||
kind: enum { primary, continuation, secondary, right } = .primary,
|
||||
/// If true, the shell will not redraw the prompt on resize so don't erase it.
|
||||
/// See: https://sw.kovidgoyal.net/kitty/shell-integration/#notes-for-shell-developers
|
||||
redraw: bool = true,
|
||||
/// Use a special key instead of arrow keys to move the cursor on
|
||||
/// mouse click. Useful if arrow keys have side-effets like triggering
|
||||
/// auto-complete. The shell integration script should bind the special
|
||||
/// key as needed.
|
||||
/// See: https://sw.kovidgoyal.net/kitty/shell-integration/#notes-for-shell-developers
|
||||
special_key: bool = false,
|
||||
/// If true, the shell is capable of handling mouse click events.
|
||||
/// Ghostty will then send a click event to the shell when the user
|
||||
/// clicks somewhere in the prompt. The shell can then move the cursor
|
||||
/// to that position or perform some other appropriate action. If false,
|
||||
/// Ghostty may generate a number of fake key events to move the cursor
|
||||
/// which is not very robust.
|
||||
/// See: https://sw.kovidgoyal.net/kitty/shell-integration/#notes-for-shell-developers
|
||||
click_events: bool = false,
|
||||
},
|
||||
|
||||
/// End of prompt and start of user input, terminated by a OSC "133;C"
|
||||
/// or another prompt (OSC "133;P").
|
||||
prompt_end: void,
|
||||
|
||||
/// The OSC "133;C" command can be used to explicitly end
|
||||
/// the input area and begin the output area. However, some applications
|
||||
/// don't provide a convenient way to emit that command.
|
||||
/// That is why we also specify an implicit way to end the input area
|
||||
/// at the end of the line. In the case of multiple input lines: If the
|
||||
/// cursor is on a fresh (empty) line and we see either OSC "133;P" or
|
||||
/// OSC "133;I" then this is the start of a continuation input line.
|
||||
/// If we see anything else, it is the start of the output area (or end
|
||||
/// of command).
|
||||
end_of_input: struct {
|
||||
/// The command line that the user entered.
|
||||
/// See: https://sw.kovidgoyal.net/kitty/shell-integration/#notes-for-shell-developers
|
||||
cmdline: ?[:0]const u8 = null,
|
||||
},
|
||||
|
||||
/// End of current command.
|
||||
///
|
||||
/// The exit-code need not be specified if there are no options,
|
||||
/// or if the command was cancelled (no OSC "133;C"), such as by typing
|
||||
/// an interrupt/cancel character (typically ctrl-C) during line-editing.
|
||||
/// Otherwise, it must be an integer code, where 0 means the command
|
||||
/// succeeded, and other values indicate failure. In additing to the
|
||||
/// exit-code there may be an err= option, which non-legacy terminals
|
||||
/// should give precedence to. The err=_value_ option is more general:
|
||||
/// an empty string is success, and any non-empty value (which need not
|
||||
/// be an integer) is an error code. So to indicate success both ways you
|
||||
/// could send OSC "133;D;0;err=\007", though `OSC "133;D;0\007" is shorter.
|
||||
end_of_command: struct {
|
||||
exit_code: ?u8 = null,
|
||||
// TODO: err option
|
||||
},
|
||||
/// Semantic prompt command: https://gitlab.freedesktop.org/Per_Bothner/specifications/blob/master/proposals/semantic-prompts.md
|
||||
semantic_prompt: SemanticPrompt,
|
||||
|
||||
/// Set or get clipboard contents. If data is null, then the current
|
||||
/// clipboard contents are sent to the pty. If data is set, this
|
||||
@@ -218,6 +152,8 @@ pub const Command = union(Key) {
|
||||
/// Kitty text sizing protocol (OSC 66)
|
||||
kitty_text_sizing: parsers.kitty_text_sizing.OSC,
|
||||
|
||||
pub const SemanticPrompt = parsers.semantic_prompt.Command;
|
||||
|
||||
pub const Key = LibEnum(
|
||||
if (build_options.c_abi) .c else .zig,
|
||||
// NOTE: Order matters, see LibEnum documentation.
|
||||
@@ -225,10 +161,7 @@ pub const Command = union(Key) {
|
||||
"invalid",
|
||||
"change_window_title",
|
||||
"change_window_icon",
|
||||
"prompt_start",
|
||||
"prompt_end",
|
||||
"end_of_input",
|
||||
"end_of_command",
|
||||
"semantic_prompt",
|
||||
"clipboard_contents",
|
||||
"report_pwd",
|
||||
"mouse_shape",
|
||||
@@ -406,6 +339,7 @@ pub const Parser = struct {
|
||||
@"119",
|
||||
@"133",
|
||||
@"777",
|
||||
@"1337",
|
||||
};
|
||||
|
||||
pub fn init(alloc: ?Allocator) Parser {
|
||||
@@ -459,15 +393,12 @@ pub const Parser = struct {
|
||||
.conemu_sleep,
|
||||
.conemu_wait_input,
|
||||
.conemu_xterm_emulation,
|
||||
.end_of_command,
|
||||
.end_of_input,
|
||||
.hyperlink_end,
|
||||
.hyperlink_start,
|
||||
.invalid,
|
||||
.mouse_shape,
|
||||
.prompt_end,
|
||||
.prompt_start,
|
||||
.report_pwd,
|
||||
.semantic_prompt,
|
||||
.show_desktop_notification,
|
||||
.kitty_text_sizing,
|
||||
=> {},
|
||||
@@ -663,8 +594,20 @@ pub const Parser = struct {
|
||||
else => self.state = .invalid,
|
||||
},
|
||||
|
||||
.@"0",
|
||||
.@"133",
|
||||
=> switch (c) {
|
||||
';' => self.writeToFixed(),
|
||||
'7' => self.state = .@"1337",
|
||||
else => self.state = .invalid,
|
||||
},
|
||||
|
||||
.@"1337",
|
||||
=> switch (c) {
|
||||
';' => self.writeToFixed(),
|
||||
else => self.state = .invalid,
|
||||
},
|
||||
|
||||
.@"0",
|
||||
.@"22",
|
||||
.@"777",
|
||||
.@"8",
|
||||
@@ -741,6 +684,8 @@ pub const Parser = struct {
|
||||
.@"133" => parsers.semantic_prompt.parse(self, terminator_ch),
|
||||
|
||||
.@"777" => parsers.rxvt_extension.parse(self, terminator_ch),
|
||||
|
||||
.@"1337" => parsers.iterm2.parse(self, terminator_ch),
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
@@ -5,6 +5,7 @@ pub const change_window_title = @import("parsers/change_window_title.zig");
|
||||
pub const clipboard_operation = @import("parsers/clipboard_operation.zig");
|
||||
pub const color = @import("parsers/color.zig");
|
||||
pub const hyperlink = @import("parsers/hyperlink.zig");
|
||||
pub const iterm2 = @import("parsers/iterm2.zig");
|
||||
pub const kitty_color = @import("parsers/kitty_color.zig");
|
||||
pub const kitty_text_sizing = @import("parsers/kitty_text_sizing.zig");
|
||||
pub const mouse_shape = @import("parsers/mouse_shape.zig");
|
||||
@@ -14,16 +15,5 @@ pub const rxvt_extension = @import("parsers/rxvt_extension.zig");
|
||||
pub const semantic_prompt = @import("parsers/semantic_prompt.zig");
|
||||
|
||||
test {
|
||||
_ = change_window_icon;
|
||||
_ = change_window_title;
|
||||
_ = clipboard_operation;
|
||||
_ = color;
|
||||
_ = hyperlink;
|
||||
_ = kitty_color;
|
||||
_ = kitty_text_sizing;
|
||||
_ = mouse_shape;
|
||||
_ = osc9;
|
||||
_ = report_pwd;
|
||||
_ = rxvt_extension;
|
||||
_ = semantic_prompt;
|
||||
std.testing.refAllDecls(@This());
|
||||
}
|
||||
|
||||
435
src/terminal/osc/parsers/iterm2.zig
Normal file
435
src/terminal/osc/parsers/iterm2.zig
Normal file
@@ -0,0 +1,435 @@
|
||||
const std = @import("std");
|
||||
|
||||
const assert = @import("../../../quirks.zig").inlineAssert;
|
||||
const simd = @import("../../../simd/main.zig");
|
||||
|
||||
const Parser = @import("../../osc.zig").Parser;
|
||||
const Command = @import("../../osc.zig").Command;
|
||||
|
||||
const log = std.log.scoped(.osc_iterm2);
|
||||
|
||||
const Key = enum {
|
||||
AddAnnotation,
|
||||
AddHiddenAnnotation,
|
||||
Block,
|
||||
Button,
|
||||
ClearCapturedOutput,
|
||||
ClearScrollback,
|
||||
Copy,
|
||||
CopyToClipboard,
|
||||
CurrentDir,
|
||||
CursorShape,
|
||||
Custom,
|
||||
Disinter,
|
||||
EndCopy,
|
||||
File,
|
||||
FileEnd,
|
||||
FilePart,
|
||||
HighlightCursorLine,
|
||||
MultipartFile,
|
||||
OpenURL,
|
||||
PopKeyLabels,
|
||||
PushKeyLabels,
|
||||
RemoteHost,
|
||||
ReportCellSize,
|
||||
ReportVariable,
|
||||
RequestAttention,
|
||||
RequestUpload,
|
||||
SetBackgroundImageFile,
|
||||
SetBadgeFormat,
|
||||
SetColors,
|
||||
SetKeyLabel,
|
||||
SetMark,
|
||||
SetProfile,
|
||||
SetUserVar,
|
||||
ShellIntegrationVersion,
|
||||
StealFocus,
|
||||
UnicodeVersion,
|
||||
};
|
||||
|
||||
// Instead of using `std.meta.stringToEnum` we set up a StaticStringMap so
|
||||
// that we can get ASCII case-insensitive lookups.
|
||||
const Map = std.StaticStringMapWithEql(Key, std.ascii.eqlIgnoreCase);
|
||||
const map: Map = .initComptime(
|
||||
map: {
|
||||
const fields = @typeInfo(Key).@"enum".fields;
|
||||
var tmp: [fields.len]struct { [:0]const u8, Key } = undefined;
|
||||
for (fields, 0..) |field, i| {
|
||||
tmp[i] = .{ field.name, @enumFromInt(field.value) };
|
||||
}
|
||||
break :map tmp;
|
||||
},
|
||||
);
|
||||
|
||||
/// Parse OSC 1337
|
||||
/// https://iterm2.com/documentation-escape-codes.html
|
||||
pub fn parse(parser: *Parser, _: ?u8) ?*Command {
|
||||
assert(parser.state == .@"1337");
|
||||
|
||||
const writer = parser.writer orelse {
|
||||
parser.state = .invalid;
|
||||
return null;
|
||||
};
|
||||
writer.writeByte(0) catch {
|
||||
parser.state = .invalid;
|
||||
return null;
|
||||
};
|
||||
const data = writer.buffered();
|
||||
|
||||
const key_str: [:0]u8, const value_: ?[:0]u8 = kv: {
|
||||
const index = std.mem.indexOfScalar(u8, data, '=') orelse {
|
||||
break :kv .{ data[0 .. data.len - 1 :0], null };
|
||||
};
|
||||
data[index] = 0;
|
||||
break :kv .{ data[0..index :0], data[index + 1 .. data.len - 1 :0] };
|
||||
};
|
||||
|
||||
const key = map.get(key_str) orelse {
|
||||
parser.command = .invalid;
|
||||
return null;
|
||||
};
|
||||
|
||||
switch (key) {
|
||||
.Copy => {
|
||||
var value = value_ orelse {
|
||||
parser.command = .invalid;
|
||||
return null;
|
||||
};
|
||||
|
||||
// Sending a blank entry to clear the clipboard is an OSC 52-ism,
|
||||
// make sure that is invalid here.
|
||||
if (value.len == 0) {
|
||||
parser.command = .invalid;
|
||||
return null;
|
||||
}
|
||||
|
||||
// base64 value must be prefixed by a colon
|
||||
if (value[0] != ':') {
|
||||
parser.command = .invalid;
|
||||
return null;
|
||||
}
|
||||
|
||||
value = value[1..value.len :0];
|
||||
|
||||
// Sending a blank entry to clear the clipboard is an OSC 52-ism,
|
||||
// make sure that is invalid here.
|
||||
if (value.len == 0) {
|
||||
parser.command = .invalid;
|
||||
return null;
|
||||
}
|
||||
|
||||
// Sending a '?' to query the clipboard is an OSC 52-ism, make sure
|
||||
// that is invalid here.
|
||||
if (value.len == 1 and value[0] == '?') {
|
||||
parser.command = .invalid;
|
||||
return null;
|
||||
}
|
||||
|
||||
// It would be better to check for valid base64 data here, but that
|
||||
// would mean parsing the base64 data twice in the "normal" case.
|
||||
|
||||
parser.command = .{
|
||||
.clipboard_contents = .{
|
||||
.kind = 'c',
|
||||
.data = value,
|
||||
},
|
||||
};
|
||||
return &parser.command;
|
||||
},
|
||||
|
||||
.CurrentDir => {
|
||||
const value = value_ orelse {
|
||||
parser.command = .invalid;
|
||||
return null;
|
||||
};
|
||||
if (value.len == 0) {
|
||||
parser.command = .invalid;
|
||||
return null;
|
||||
}
|
||||
parser.command = .{
|
||||
.report_pwd = .{
|
||||
.value = value,
|
||||
},
|
||||
};
|
||||
return &parser.command;
|
||||
},
|
||||
|
||||
.AddAnnotation,
|
||||
.AddHiddenAnnotation,
|
||||
.Block,
|
||||
.Button,
|
||||
.ClearCapturedOutput,
|
||||
.ClearScrollback,
|
||||
.CopyToClipboard,
|
||||
.CursorShape,
|
||||
.Custom,
|
||||
.Disinter,
|
||||
.EndCopy,
|
||||
.File,
|
||||
.FileEnd,
|
||||
.FilePart,
|
||||
.HighlightCursorLine,
|
||||
.MultipartFile,
|
||||
.OpenURL,
|
||||
.PopKeyLabels,
|
||||
.PushKeyLabels,
|
||||
.RemoteHost,
|
||||
.ReportCellSize,
|
||||
.ReportVariable,
|
||||
.RequestAttention,
|
||||
.RequestUpload,
|
||||
.SetBackgroundImageFile,
|
||||
.SetBadgeFormat,
|
||||
.SetColors,
|
||||
.SetKeyLabel,
|
||||
.SetMark,
|
||||
.SetProfile,
|
||||
.SetUserVar,
|
||||
.ShellIntegrationVersion,
|
||||
.StealFocus,
|
||||
.UnicodeVersion,
|
||||
=> {
|
||||
log.debug("unimplemented OSC 1337: {t}", .{key});
|
||||
parser.command = .invalid;
|
||||
return null;
|
||||
},
|
||||
}
|
||||
return &parser.command;
|
||||
}
|
||||
|
||||
test "OSC: 1337: test valid unimplemented key with no value" {
|
||||
const testing = std.testing;
|
||||
|
||||
var p: Parser = .init(testing.allocator);
|
||||
defer p.deinit();
|
||||
|
||||
const input = "1337;SetBadgeFormat";
|
||||
for (input) |ch| p.next(ch);
|
||||
|
||||
try testing.expect(p.end('\x1b') == null);
|
||||
}
|
||||
|
||||
test "OSC: 1337: test valid unimplemented key with empty value" {
|
||||
const testing = std.testing;
|
||||
|
||||
var p: Parser = .init(testing.allocator);
|
||||
defer p.deinit();
|
||||
|
||||
const input = "1337;SetBadgeFormat=";
|
||||
for (input) |ch| p.next(ch);
|
||||
|
||||
try testing.expect(p.end('\x1b') == null);
|
||||
}
|
||||
|
||||
test "OSC: 1337: test valid unimplemented key with non-empty value" {
|
||||
const testing = std.testing;
|
||||
|
||||
var p: Parser = .init(testing.allocator);
|
||||
defer p.deinit();
|
||||
|
||||
const input = "1337;SetBadgeFormat=abc123";
|
||||
for (input) |ch| p.next(ch);
|
||||
|
||||
try testing.expect(p.end('\x1b') == null);
|
||||
}
|
||||
|
||||
test "OSC: 1337: test valid key with lower case and with no value" {
|
||||
const testing = std.testing;
|
||||
|
||||
var p: Parser = .init(testing.allocator);
|
||||
defer p.deinit();
|
||||
|
||||
const input = "1337;setbadgeformat";
|
||||
for (input) |ch| p.next(ch);
|
||||
|
||||
try testing.expect(p.end('\x1b') == null);
|
||||
}
|
||||
|
||||
test "OSC: 1337: test valid key with lower case and with empty value" {
|
||||
const testing = std.testing;
|
||||
|
||||
var p: Parser = .init(testing.allocator);
|
||||
defer p.deinit();
|
||||
|
||||
const input = "1337;setbadgeformat=";
|
||||
for (input) |ch| p.next(ch);
|
||||
|
||||
try testing.expect(p.end('\x1b') == null);
|
||||
}
|
||||
|
||||
test "OSC: 1337: test valid key with lower case and with non-empty value" {
|
||||
const testing = std.testing;
|
||||
|
||||
var p: Parser = .init(testing.allocator);
|
||||
defer p.deinit();
|
||||
|
||||
const input = "1337;setbadgeformat=abc123";
|
||||
for (input) |ch| p.next(ch);
|
||||
|
||||
try testing.expect(p.end('\x1b') == null);
|
||||
}
|
||||
|
||||
test "OSC: 1337: test invalid key with no value" {
|
||||
const testing = std.testing;
|
||||
|
||||
var p: Parser = .init(testing.allocator);
|
||||
defer p.deinit();
|
||||
|
||||
const input = "1337;BobrKurwa";
|
||||
for (input) |ch| p.next(ch);
|
||||
|
||||
try testing.expect(p.end('\x1b') == null);
|
||||
}
|
||||
|
||||
test "OSC: 1337: test invalid key with empty value" {
|
||||
const testing = std.testing;
|
||||
|
||||
var p: Parser = .init(testing.allocator);
|
||||
defer p.deinit();
|
||||
|
||||
const input = "1337;BobrKurwa=";
|
||||
for (input) |ch| p.next(ch);
|
||||
|
||||
try testing.expect(p.end('\x1b') == null);
|
||||
}
|
||||
|
||||
test "OSC: 1337: test invalid key with non-empty value" {
|
||||
const testing = std.testing;
|
||||
|
||||
var p: Parser = .init(testing.allocator);
|
||||
defer p.deinit();
|
||||
|
||||
const input = "1337;BobrKurwa=abc123";
|
||||
for (input) |ch| p.next(ch);
|
||||
|
||||
try testing.expect(p.end('\x1b') == null);
|
||||
}
|
||||
|
||||
test "OSC: 1337: test Copy with no value" {
|
||||
const testing = std.testing;
|
||||
|
||||
var p: Parser = .init(testing.allocator);
|
||||
defer p.deinit();
|
||||
|
||||
const input = "1337;Copy";
|
||||
for (input) |ch| p.next(ch);
|
||||
|
||||
try testing.expect(p.end('\x1b') == null);
|
||||
}
|
||||
|
||||
test "OSC: 1337: test Copy with empty value" {
|
||||
const testing = std.testing;
|
||||
|
||||
var p: Parser = .init(testing.allocator);
|
||||
defer p.deinit();
|
||||
|
||||
const input = "1337;Copy=";
|
||||
for (input) |ch| p.next(ch);
|
||||
|
||||
try testing.expect(p.end('\x1b') == null);
|
||||
}
|
||||
|
||||
test "OSC: 1337: test Copy with only prefix colon" {
|
||||
const testing = std.testing;
|
||||
|
||||
var p: Parser = .init(testing.allocator);
|
||||
defer p.deinit();
|
||||
|
||||
const input = "1337;Copy=:";
|
||||
for (input) |ch| p.next(ch);
|
||||
|
||||
try testing.expect(p.end('\x1b') == null);
|
||||
}
|
||||
|
||||
test "OSC: 1337: test Copy with question mark" {
|
||||
const testing = std.testing;
|
||||
|
||||
var p: Parser = .init(testing.allocator);
|
||||
defer p.deinit();
|
||||
|
||||
const input = "1337;Copy=:?";
|
||||
for (input) |ch| p.next(ch);
|
||||
|
||||
try testing.expect(p.end('\x1b') == null);
|
||||
}
|
||||
|
||||
test "OSC: 1337: test Copy with non-empty value that is invalid base64" {
|
||||
// For performance reasons, we don't check for valid base64 data
|
||||
// right now.
|
||||
return error.SkipZigTest;
|
||||
|
||||
// const testing = std.testing;
|
||||
|
||||
// var p: Parser = .init(testing.allocator);
|
||||
// defer p.deinit();
|
||||
|
||||
// const input = "1337;Copy=:abc123";
|
||||
// for (input) |ch| p.next(ch);
|
||||
|
||||
// try testing.expect(p.end('\x1b') == null);
|
||||
}
|
||||
|
||||
test "OSC: 1337: test Copy with non-empty value that is valid base64 but not prefixed with a colon" {
|
||||
const testing = std.testing;
|
||||
|
||||
var p: Parser = .init(testing.allocator);
|
||||
defer p.deinit();
|
||||
|
||||
const input = "1337;Copy=YWJjMTIz";
|
||||
for (input) |ch| p.next(ch);
|
||||
|
||||
try testing.expect(p.end('\x1b') == null);
|
||||
}
|
||||
|
||||
test "OSC: 1337: test Copy with non-empty value that is valid base64" {
|
||||
const testing = std.testing;
|
||||
|
||||
var p: Parser = .init(testing.allocator);
|
||||
defer p.deinit();
|
||||
|
||||
const input = "1337;Copy=:YWJjMTIz";
|
||||
for (input) |ch| p.next(ch);
|
||||
|
||||
const cmd = p.end('\x1b').?.*;
|
||||
try testing.expect(cmd == .clipboard_contents);
|
||||
try testing.expectEqual('c', cmd.clipboard_contents.kind);
|
||||
try testing.expectEqualStrings("YWJjMTIz", cmd.clipboard_contents.data);
|
||||
}
|
||||
|
||||
test "OSC: 1337: test CurrentDir with no value" {
|
||||
const testing = std.testing;
|
||||
|
||||
var p: Parser = .init(testing.allocator);
|
||||
defer p.deinit();
|
||||
|
||||
const input = "1337;CurrentDir";
|
||||
for (input) |ch| p.next(ch);
|
||||
|
||||
try testing.expect(p.end('\x1b') == null);
|
||||
}
|
||||
|
||||
test "OSC: 1337: test CurrentDir with empty value" {
|
||||
const testing = std.testing;
|
||||
|
||||
var p: Parser = .init(testing.allocator);
|
||||
defer p.deinit();
|
||||
|
||||
const input = "1337;CurrentDir=";
|
||||
for (input) |ch| p.next(ch);
|
||||
|
||||
try testing.expect(p.end('\x1b') == null);
|
||||
}
|
||||
|
||||
test "OSC: 1337: test CurrentDir with non-empty value" {
|
||||
const testing = std.testing;
|
||||
|
||||
var p: Parser = .init(testing.allocator);
|
||||
defer p.deinit();
|
||||
|
||||
const input = "1337;CurrentDir=abc123";
|
||||
for (input) |ch| p.next(ch);
|
||||
|
||||
const cmd = p.end('\x1b').?.*;
|
||||
try testing.expect(cmd == .report_pwd);
|
||||
try testing.expectEqualStrings("abc123", cmd.report_pwd.value);
|
||||
}
|
||||
@@ -98,9 +98,7 @@ pub fn parse(parser: *Parser, _: ?u8) ?*Command {
|
||||
},
|
||||
// OSC 9;12 mark prompt start
|
||||
'2' => {
|
||||
parser.command = .{
|
||||
.prompt_start = .{},
|
||||
};
|
||||
parser.command = .{ .semantic_prompt = .init(.fresh_line_new_prompt) };
|
||||
return &parser.command;
|
||||
},
|
||||
else => break :conemu,
|
||||
@@ -1125,7 +1123,7 @@ test "OSC: 9;12: ConEmu mark prompt start 1" {
|
||||
for (input) |ch| p.next(ch);
|
||||
|
||||
const cmd = p.end('\x1b').?.*;
|
||||
try testing.expect(cmd == .prompt_start);
|
||||
try testing.expect(cmd == .semantic_prompt);
|
||||
}
|
||||
|
||||
test "OSC: 9;12: ConEmu mark prompt start 2" {
|
||||
@@ -1138,5 +1136,5 @@ test "OSC: 9;12: ConEmu mark prompt start 2" {
|
||||
for (input) |ch| p.next(ch);
|
||||
|
||||
const cmd = p.end('\x1b').?.*;
|
||||
try testing.expect(cmd == .prompt_start);
|
||||
try testing.expect(cmd == .semantic_prompt);
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -111,8 +111,6 @@ pub const Action = union(Key) {
|
||||
apc_start,
|
||||
apc_end,
|
||||
apc_put: u8,
|
||||
prompt_end,
|
||||
end_of_input,
|
||||
end_hyperlink,
|
||||
active_status_display: ansi.StatusDisplay,
|
||||
decaln,
|
||||
@@ -122,14 +120,12 @@ pub const Action = union(Key) {
|
||||
progress_report: osc.Command.ProgressReport,
|
||||
start_hyperlink: StartHyperlink,
|
||||
clipboard_contents: ClipboardContents,
|
||||
prompt_start: PromptStart,
|
||||
prompt_continuation: PromptContinuation,
|
||||
end_of_command: EndOfCommand,
|
||||
mouse_shape: MouseShape,
|
||||
configure_charset: ConfigureCharset,
|
||||
set_attribute: sgr.Attribute,
|
||||
kitty_color_report: kitty.color.OSC,
|
||||
color_operation: ColorOperation,
|
||||
semantic_prompt: SemanticPrompt,
|
||||
|
||||
pub const Key = lib.Enum(
|
||||
lib_target,
|
||||
@@ -212,8 +208,6 @@ pub const Action = union(Key) {
|
||||
"apc_start",
|
||||
"apc_end",
|
||||
"apc_put",
|
||||
"prompt_end",
|
||||
"end_of_input",
|
||||
"end_hyperlink",
|
||||
"active_status_display",
|
||||
"decaln",
|
||||
@@ -223,14 +217,12 @@ pub const Action = union(Key) {
|
||||
"progress_report",
|
||||
"start_hyperlink",
|
||||
"clipboard_contents",
|
||||
"prompt_start",
|
||||
"prompt_continuation",
|
||||
"end_of_command",
|
||||
"mouse_shape",
|
||||
"configure_charset",
|
||||
"set_attribute",
|
||||
"kitty_color_report",
|
||||
"color_operation",
|
||||
"semantic_prompt",
|
||||
},
|
||||
);
|
||||
|
||||
@@ -391,47 +383,6 @@ pub const Action = union(Key) {
|
||||
}
|
||||
};
|
||||
|
||||
pub const PromptStart = struct {
|
||||
aid: ?[]const u8,
|
||||
redraw: bool,
|
||||
|
||||
pub const C = extern struct {
|
||||
aid: lib.String,
|
||||
redraw: bool,
|
||||
};
|
||||
|
||||
pub fn cval(self: PromptStart) PromptStart.C {
|
||||
return .{
|
||||
.aid = .init(self.aid orelse ""),
|
||||
.redraw = self.redraw,
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
pub const PromptContinuation = struct {
|
||||
aid: ?[]const u8,
|
||||
|
||||
pub const C = lib.String;
|
||||
|
||||
pub fn cval(self: PromptContinuation) PromptContinuation.C {
|
||||
return .init(self.aid orelse "");
|
||||
}
|
||||
};
|
||||
|
||||
pub const EndOfCommand = struct {
|
||||
exit_code: ?u8,
|
||||
|
||||
pub const C = extern struct {
|
||||
exit_code: i16,
|
||||
};
|
||||
|
||||
pub fn cval(self: EndOfCommand) EndOfCommand.C {
|
||||
return .{
|
||||
.exit_code = if (self.exit_code) |code| @intCast(code) else -1,
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
pub const ConfigureCharset = lib.Struct(lib_target, struct {
|
||||
slot: charsets.Slots,
|
||||
charset: charsets.Charset,
|
||||
@@ -448,6 +399,8 @@ pub const Action = union(Key) {
|
||||
return {};
|
||||
}
|
||||
};
|
||||
|
||||
pub const SemanticPrompt = osc.Command.SemanticPrompt;
|
||||
};
|
||||
|
||||
/// Returns a type that can process a stream of tty control characters.
|
||||
@@ -1988,10 +1941,9 @@ pub fn Stream(comptime Handler: type) type {
|
||||
// 4. hyperlink_start
|
||||
// 5. report_pwd
|
||||
// 6. color_operation
|
||||
// 7. prompt_start
|
||||
// 8. prompt_end
|
||||
// 7. semantic_prompt
|
||||
//
|
||||
// Together, these 8 commands make up about 96% of all
|
||||
// Together, these 7 commands make up about 96% of all
|
||||
// OSC commands encountered in real world scenarios.
|
||||
//
|
||||
// Additionally, within the prongs, unlikely branch
|
||||
@@ -2003,6 +1955,11 @@ pub fn Stream(comptime Handler: type) type {
|
||||
// ref: https://github.com/qwerasd205/asciinema-stats
|
||||
|
||||
switch (cmd) {
|
||||
.semantic_prompt => |sp| {
|
||||
@branchHint(.likely);
|
||||
try self.handler.vt(.semantic_prompt, sp);
|
||||
},
|
||||
|
||||
.change_window_title => |title| {
|
||||
@branchHint(.likely);
|
||||
if (!std.unicode.utf8ValidateSlice(title)) {
|
||||
@@ -2026,30 +1983,6 @@ pub fn Stream(comptime Handler: type) type {
|
||||
});
|
||||
},
|
||||
|
||||
.prompt_start => |v| {
|
||||
@branchHint(.likely);
|
||||
switch (v.kind) {
|
||||
.primary, .right => try self.handler.vt(.prompt_start, .{
|
||||
.aid = v.aid,
|
||||
.redraw = v.redraw,
|
||||
}),
|
||||
.continuation, .secondary => try self.handler.vt(.prompt_continuation, .{
|
||||
.aid = v.aid,
|
||||
}),
|
||||
}
|
||||
},
|
||||
|
||||
.prompt_end => {
|
||||
@branchHint(.likely);
|
||||
try self.handler.vt(.prompt_end, {});
|
||||
},
|
||||
|
||||
.end_of_input => try self.handler.vt(.end_of_input, {}),
|
||||
|
||||
.end_of_command => |end| {
|
||||
try self.handler.vt(.end_of_command, .{ .exit_code = end.exit_code });
|
||||
},
|
||||
|
||||
.report_pwd => |v| {
|
||||
@branchHint(.likely);
|
||||
try self.handler.vt(.report_pwd, .{ .url = v.value });
|
||||
|
||||
@@ -153,14 +153,7 @@ pub const Handler = struct {
|
||||
.full_reset => self.terminal.fullReset(),
|
||||
.start_hyperlink => try self.terminal.screens.active.startHyperlink(value.uri, value.id),
|
||||
.end_hyperlink => self.terminal.screens.active.endHyperlink(),
|
||||
.prompt_start => {
|
||||
self.terminal.screens.active.cursor.page_row.semantic_prompt = .prompt;
|
||||
self.terminal.flags.shell_redraws_prompt = value.redraw;
|
||||
},
|
||||
.prompt_continuation => self.terminal.screens.active.cursor.page_row.semantic_prompt = .prompt_continuation,
|
||||
.prompt_end => self.terminal.markSemanticPrompt(.input),
|
||||
.end_of_input => self.terminal.markSemanticPrompt(.command),
|
||||
.end_of_command => self.terminal.screens.active.cursor.page_row.semantic_prompt = .input,
|
||||
.semantic_prompt => self.semanticPrompt(value),
|
||||
.mouse_shape => self.terminal.mouse_shape = value,
|
||||
.color_operation => try self.colorOperation(value.op, &value.requests),
|
||||
.kitty_color_report => try self.kittyColorOperation(value),
|
||||
@@ -216,6 +209,42 @@ pub const Handler = struct {
|
||||
}
|
||||
}
|
||||
|
||||
fn semanticPrompt(
|
||||
self: *Handler,
|
||||
cmd: Action.SemanticPrompt,
|
||||
) void {
|
||||
switch (cmd.action) {
|
||||
.fresh_line_new_prompt => {
|
||||
const kind = cmd.readOption(.prompt_kind) orelse .initial;
|
||||
switch (kind) {
|
||||
.initial, .right => {
|
||||
self.terminal.screens.active.cursor.page_row.semantic_prompt = .prompt;
|
||||
if (cmd.readOption(.redraw)) |redraw| {
|
||||
self.terminal.flags.shell_redraws_prompt = redraw;
|
||||
}
|
||||
},
|
||||
.continuation, .secondary => {
|
||||
self.terminal.screens.active.cursor.page_row.semantic_prompt = .prompt_continuation;
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
.end_prompt_start_input => self.terminal.markSemanticPrompt(.input),
|
||||
.end_input_start_output => self.terminal.markSemanticPrompt(.command),
|
||||
.end_command => self.terminal.screens.active.cursor.page_row.semantic_prompt = .input,
|
||||
|
||||
// All of these commands weren't previously handled by our
|
||||
// semantic prompt code. I am PR-ing the parser separate from the
|
||||
// handling so we just ignore these like we did before, even
|
||||
// though we should handle them eventually.
|
||||
.end_prompt_start_input_terminate_eol,
|
||||
.fresh_line,
|
||||
.new_command,
|
||||
.prompt_start,
|
||||
=> {},
|
||||
}
|
||||
}
|
||||
|
||||
fn setMode(self: *Handler, mode: modes.Mode, enabled: bool) !void {
|
||||
// Set the mode on the terminal
|
||||
self.terminal.modes.set(mode, enabled);
|
||||
|
||||
@@ -311,8 +311,6 @@ pub const StreamHandler = struct {
|
||||
},
|
||||
.kitty_color_report => try self.kittyColorReport(value),
|
||||
.color_operation => try self.colorOperation(value.op, &value.requests, value.terminator),
|
||||
.prompt_end => try self.promptEnd(),
|
||||
.end_of_input => try self.endOfInput(),
|
||||
.end_hyperlink => try self.endHyperlink(),
|
||||
.active_status_display => self.terminal.status_display = value,
|
||||
.decaln => try self.decaln(),
|
||||
@@ -322,9 +320,7 @@ pub const StreamHandler = struct {
|
||||
.progress_report => self.progressReport(value),
|
||||
.start_hyperlink => try self.startHyperlink(value.uri, value.id),
|
||||
.clipboard_contents => try self.clipboardContents(value.kind, value.data),
|
||||
.prompt_start => self.promptStart(value.aid, value.redraw),
|
||||
.prompt_continuation => self.promptContinuation(value.aid),
|
||||
.end_of_command => self.endOfCommand(value.exit_code),
|
||||
.semantic_prompt => self.semanticPrompt(value),
|
||||
.mouse_shape => try self.setMouseShape(value),
|
||||
.configure_charset => self.configureCharset(value.slot, value.charset),
|
||||
.set_attribute => {
|
||||
@@ -1070,28 +1066,53 @@ pub const StreamHandler = struct {
|
||||
});
|
||||
}
|
||||
|
||||
inline fn promptStart(self: *StreamHandler, aid: ?[]const u8, redraw: bool) void {
|
||||
_ = aid;
|
||||
self.terminal.markSemanticPrompt(.prompt);
|
||||
self.terminal.flags.shell_redraws_prompt = redraw;
|
||||
}
|
||||
fn semanticPrompt(
|
||||
self: *StreamHandler,
|
||||
cmd: Stream.Action.SemanticPrompt,
|
||||
) void {
|
||||
switch (cmd.action) {
|
||||
.fresh_line_new_prompt => {
|
||||
const kind = cmd.readOption(.prompt_kind) orelse .initial;
|
||||
switch (kind) {
|
||||
.initial, .right => {
|
||||
self.terminal.markSemanticPrompt(.prompt);
|
||||
if (cmd.readOption(.redraw)) |redraw| {
|
||||
self.terminal.flags.shell_redraws_prompt = redraw;
|
||||
}
|
||||
},
|
||||
.continuation, .secondary => {
|
||||
self.terminal.markSemanticPrompt(.prompt_continuation);
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
inline fn promptContinuation(self: *StreamHandler, aid: ?[]const u8) void {
|
||||
_ = aid;
|
||||
self.terminal.markSemanticPrompt(.prompt_continuation);
|
||||
}
|
||||
.end_prompt_start_input => self.terminal.markSemanticPrompt(.input),
|
||||
.end_input_start_output => {
|
||||
self.terminal.markSemanticPrompt(.command);
|
||||
self.surfaceMessageWriter(.start_command);
|
||||
},
|
||||
.end_command => {
|
||||
// The specification seems to not specify the type but
|
||||
// other terminals accept 32-bits, but exit codes are really
|
||||
// bytes, so we just do our best here.
|
||||
const code: u8 = code: {
|
||||
const raw: i32 = cmd.readOption(.exit_code) orelse 0;
|
||||
break :code std.math.cast(u8, raw) orelse 1;
|
||||
};
|
||||
|
||||
pub inline fn promptEnd(self: *StreamHandler) !void {
|
||||
self.terminal.markSemanticPrompt(.input);
|
||||
}
|
||||
self.surfaceMessageWriter(.{ .stop_command = code });
|
||||
},
|
||||
|
||||
pub inline fn endOfInput(self: *StreamHandler) !void {
|
||||
self.terminal.markSemanticPrompt(.command);
|
||||
self.surfaceMessageWriter(.start_command);
|
||||
}
|
||||
|
||||
inline fn endOfCommand(self: *StreamHandler, exit_code: ?u8) void {
|
||||
self.surfaceMessageWriter(.{ .stop_command = exit_code });
|
||||
// All of these commands weren't previously handled by our
|
||||
// semantic prompt code. I am PR-ing the parser separate from the
|
||||
// handling so we just ignore these like we did before, even
|
||||
// though we should handle them eventually.
|
||||
.end_prompt_start_input_terminate_eol,
|
||||
.fresh_line,
|
||||
.new_command,
|
||||
.prompt_start,
|
||||
=> {},
|
||||
}
|
||||
}
|
||||
|
||||
fn reportPwd(self: *StreamHandler, url: []const u8) !void {
|
||||
|
||||
Reference in New Issue
Block a user