diff --git a/src/Surface.zig b/src/Surface.zig
index c3fd1e9d4..575a7e822 100644
--- a/src/Surface.zig
+++ b/src/Surface.zig
@@ -145,6 +145,7 @@ const DerivedConfig = struct {
mouse_shift_capture: configpkg.MouseShiftCapture,
macos_non_native_fullscreen: configpkg.NonNativeFullscreen,
macos_option_as_alt: configpkg.OptionAsAlt,
+ vt_kam_allowed: bool,
window_padding_x: u32,
window_padding_y: u32,
@@ -166,6 +167,7 @@ const DerivedConfig = struct {
.mouse_shift_capture = config.@"mouse-shift-capture",
.macos_non_native_fullscreen = config.@"macos-non-native-fullscreen",
.macos_option_as_alt = config.@"macos-option-as-alt",
+ .vt_kam_allowed = config.@"vt-kam-allowed",
.window_padding_x = config.@"window-padding-x",
.window_padding_y = config.@"window-padding-y",
@@ -985,6 +987,13 @@ pub fn keyCallback(
if (consumed and performed) return true;
}
+ // If we allow KAM and KAM is enabled then we do nothing.
+ if (self.config.vt_kam_allowed) {
+ self.renderer_state.mutex.lock();
+ defer self.renderer_state.mutex.unlock();
+ if (self.io.terminal.modes.get(.disable_keyboard)) return true;
+ }
+
// If this input event has text, then we hide the mouse if configured.
if (self.config.mouse_hide_while_typing and
!self.mouse.hidden and
diff --git a/src/config/Config.zig b/src/config/Config.zig
index c21564f42..82733bfba 100644
--- a/src/config/Config.zig
+++ b/src/config/Config.zig
@@ -461,6 +461,14 @@ keybind: Keybinds = .{},
/// The default value is "16-bit".
@"osc-color-report-format": OSCColorReportFormat = .@"16-bit",
+/// If true, allows the "KAM" mode (ANSI mode 2) to be used within
+/// the terminal. KAM disables keyboard input at the request of the
+/// application. This is not a common feature and is not recommended
+/// to be enabled. This will not be documented further because
+/// if you know you need KAM, you know. If you don't know if you
+/// need KAM, you don't need it.
+@"vt-kam-allowed": bool = false,
+
/// If anything other than false, fullscreen mode on macOS will not use the
/// native fullscreen, but make the window fullscreen without animations and
/// using a new space. It's faster than the native fullscreen mode since it
diff --git a/src/terminal/Terminal.zig b/src/terminal/Terminal.zig
index bbca660cb..9332c5479 100644
--- a/src/terminal/Terminal.zig
+++ b/src/terminal/Terminal.zig
@@ -1573,6 +1573,11 @@ pub fn insertBlanks(self: *Terminal, count: usize) void {
// This is the index of the final copyable value that we need to copy.
const copyable_end = start + copyable - 1;
+ // If our last cell we're shifting is wide, then we need to clear
+ // it to be empty so we don't split the multi-cell char.
+ const cell = row.getCellPtr(copyable_end);
+ if (cell.attrs.wide) cell.char = 0;
+
// Shift count cells. We have to do this backwards since we're not
// allocated new space, otherwise we'll copy duplicates.
var i: usize = 0;
@@ -3949,6 +3954,23 @@ test "Terminal: insertBlanks shift off screen" {
}
}
+test "Terminal: insertBlanks split multi-cell character" {
+ const alloc = testing.allocator;
+ var t = try init(alloc, 5, 10);
+ defer t.deinit(alloc);
+
+ for ("123") |c| try t.print(c);
+ try t.print('橋');
+ t.setCursorPos(1, 1);
+ t.insertBlanks(1);
+
+ {
+ var str = try t.plainString(testing.allocator);
+ defer testing.allocator.free(str);
+ try testing.expectEqualStrings(" 123", str);
+ }
+}
+
test "Terminal: insertBlanks inside left/right scroll region" {
const alloc = testing.allocator;
var t = try init(alloc, 10, 10);
@@ -4073,6 +4095,24 @@ test "Terminal: insert mode with wide characters at end" {
}
}
+test "Terminal: insert mode pushing off wide character" {
+ const alloc = testing.allocator;
+ var t = try init(alloc, 5, 2);
+ defer t.deinit(alloc);
+
+ for ("123") |c| try t.print(c);
+ try t.print('😀'); // 0x1F600
+ t.modes.set(.insert, true);
+ t.setCursorPos(1, 1);
+ try t.print('X');
+
+ {
+ var str = try t.plainString(testing.allocator);
+ defer testing.allocator.free(str);
+ try testing.expectEqualStrings("X123", str);
+ }
+}
+
test "Terminal: cursorIsAtPrompt" {
const alloc = testing.allocator;
var t = try init(alloc, 3, 2);
diff --git a/src/terminal/modes.zig b/src/terminal/modes.zig
index 25a020e9d..0e70dd53c 100644
--- a/src/terminal/modes.zig
+++ b/src/terminal/modes.zig
@@ -147,6 +147,7 @@ pub fn modeFromInt(v: u16, ansi: bool) ?Mode {
}
fn entryForMode(comptime mode: Mode) ModeEntry {
+ @setEvalBranchQuota(10_000);
const name = @tagName(mode);
for (entries) |entry| {
if (std.mem.eql(u8, entry.name, name)) return entry;
@@ -170,7 +171,9 @@ const ModeEntry = struct {
/// valuable to redocument them all here.
const entries: []const ModeEntry = &.{
// ANSI
+ .{ .name = "disable_keyboard", .value = 2, .ansi = true }, // KAM
.{ .name = "insert", .value = 4, .ansi = true },
+ .{ .name = "send_receive_mode", .value = 12, .ansi = true, .default = true }, // SRM
// DEC
.{ .name = "cursor_keys", .value = 1 }, // DECCKM
@@ -212,7 +215,7 @@ test modeFromInt {
try testing.expect(modeFromInt(4, true).? == .insert);
try testing.expect(modeFromInt(9, true) == null);
try testing.expect(modeFromInt(9, false).? == .mouse_event_x10);
- try testing.expect(modeFromInt(12, true) == null);
+ try testing.expect(modeFromInt(14, true) == null);
}
test ModeState {
diff --git a/website/app/vt/modes/insert/page.mdx b/website/app/vt/modes/insert/page.mdx
new file mode 100644
index 000000000..1a1b6b9a0
--- /dev/null
+++ b/website/app/vt/modes/insert/page.mdx
@@ -0,0 +1,89 @@
+import VTMode from "@/components/VTMode";
+
+# Insert
+
+