Skip to content

Commit

Permalink
Merge pull request #628 from mitchellh/xt-ich
Browse files Browse the repository at this point in the history
xterm audit: insert characters (ICH)
  • Loading branch information
mitchellh authored Oct 7, 2023
2 parents 18e5f2e + 78d69c6 commit 5ef5899
Show file tree
Hide file tree
Showing 2 changed files with 248 additions and 6 deletions.
126 changes: 120 additions & 6 deletions src/terminal/Terminal.zig
Original file line number Diff line number Diff line change
Expand Up @@ -1581,8 +1581,20 @@ pub fn insertBlanks(self: *Terminal, count: usize) void {
// Unset pending wrap state without wrapping
self.screen.cursor.pending_wrap = false;

// If our cursor is outside the margins then do nothing. We DO reset
// wrap state still so this must remain below the above logic.
if (self.screen.cursor.x < self.scrolling_region.left or
self.screen.cursor.x > self.scrolling_region.right) return;

// The limit we can shift to is our right margin. We add 1 since the
// math around this is 1-indexed.
const right_limit = self.scrolling_region.right + 1;

// If our count is larger than the remaining amount, we just erase right.
if (count > self.cols - self.screen.cursor.x) {
// We only do this if we can erase the entire line (no right margin).
if (right_limit == self.cols and
count > right_limit - self.screen.cursor.x)
{
self.eraseLine(.right, false);
return;
}
Expand All @@ -1597,7 +1609,7 @@ pub fn insertBlanks(self: *Terminal, count: usize) void {
// This is the number of spaces we have left to shift existing data.
// If count is bigger than the available space left after the cursor,
// we may have no space at all for copying.
const copyable = self.screen.cols - pivot;
const copyable = right_limit - pivot;
if (copyable > 0) {
// This is the index of the final copyable value that we need to copy.
const copyable_end = start + copyable - 1;
Expand All @@ -1606,14 +1618,19 @@ pub fn insertBlanks(self: *Terminal, count: usize) void {
// allocated new space, otherwise we'll copy duplicates.
var i: usize = 0;
while (i < copyable) : (i += 1) {
const to = self.screen.cols - 1 - i;
const to = right_limit - 1 - i;
const from = copyable_end - i;
row.getCellPtr(to).* = row.getCell(from);
const src = row.getCell(from);
const dst = row.getCellPtr(to);
dst.* = src;
}
}

// Insert zero
row.fillSlice(.{}, start, pivot);
// Insert blanks. The blanks preserve the background color.
row.fillSlice(.{
.bg = self.screen.cursor.pen.bg,
.attrs = .{ .has_bg = self.screen.cursor.pen.attrs.has_bg },
}, start, pivot);
}

/// Insert amount lines at the current cursor row. The contents of the line
Expand Down Expand Up @@ -3128,6 +3145,103 @@ test "Terminal: insertBlanks more than size" {
}
}

test "Terminal: insertBlanks no scroll region, fits" {
const alloc = testing.allocator;
var t = try init(alloc, 10, 10);
defer t.deinit(alloc);

for ("ABC") |c| try t.print(c);
t.setCursorPos(1, 1);
t.insertBlanks(2);

{
var str = try t.plainString(testing.allocator);
defer testing.allocator.free(str);
try testing.expectEqualStrings(" ABC", str);
}
}

test "Terminal: insertBlanks preserves background sgr" {
const alloc = testing.allocator;
var t = try init(alloc, 10, 10);
defer t.deinit(alloc);

const pen: Screen.Cell = .{
.bg = .{ .r = 0xFF, .g = 0x00, .b = 0x00 },
.attrs = .{ .has_bg = true },
};

for ("ABC") |c| try t.print(c);
t.setCursorPos(1, 1);
t.screen.cursor.pen = pen;
t.insertBlanks(2);

{
var str = try t.plainString(testing.allocator);
defer testing.allocator.free(str);
try testing.expectEqualStrings(" ABC", str);
const cell = t.screen.getCell(.active, 0, 0);
try testing.expectEqual(pen, cell);
}
}

test "Terminal: insertBlanks shift off screen" {
const alloc = testing.allocator;
var t = try init(alloc, 5, 10);
defer t.deinit(alloc);

for (" ABC") |c| try t.print(c);
t.setCursorPos(1, 3);
t.insertBlanks(2);
try t.print('X');

{
var str = try t.plainString(testing.allocator);
defer testing.allocator.free(str);
try testing.expectEqualStrings(" X A", str);
}
}

test "Terminal: insertBlanks inside left/right scroll region" {
const alloc = testing.allocator;
var t = try init(alloc, 10, 10);
defer t.deinit(alloc);

t.scrolling_region.left = 2;
t.scrolling_region.right = 4;
t.setCursorPos(1, 3);
for ("ABC") |c| try t.print(c);
t.setCursorPos(1, 3);
t.insertBlanks(2);
try t.print('X');

{
var str = try t.plainString(testing.allocator);
defer testing.allocator.free(str);
try testing.expectEqualStrings(" X A", str);
}
}

test "Terminal: insertBlanks outside left/right scroll region" {
const alloc = testing.allocator;
var t = try init(alloc, 10, 10);
defer t.deinit(alloc);

t.scrolling_region.left = 2;
t.scrolling_region.right = 4;
t.setCursorPos(1, 3);
for ("ABC") |c| try t.print(c);
t.setCursorPos(1, 1);
t.insertBlanks(2);
try t.print('X');

{
var str = try t.plainString(testing.allocator);
defer testing.allocator.free(str);
try testing.expectEqualStrings("X ABC", str);
}
}

test "Terminal: insert mode with space" {
const alloc = testing.allocator;
var t = try init(alloc, 10, 2);
Expand Down
128 changes: 128 additions & 0 deletions website/app/vt/ich/page.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
import VTSequence from "@/components/VTSequence";

# Insert Character (ICH)

<VTSequence sequence={["CSI", "Pn", "@"]} />

Insert `n` blank characters at the current cursor position and shift
existing cell contents right.

The parameter `n` must be an integer greater than or equal to 1. If `n` is less than
or equal to 0, adjust `n` to be 1. If `n` is omitted, `n` defaults to 1.

This sequence always unsets the pending wrap state.

If the cursor position is outside of the [left and right margins](#TODO),
this sequence does not change the screen, but the pending wrap state is
still reset.

Existing cells shifted beyond the right margin are deleted. Inserted cells
are blank with the background color colored according to the current SGR state.

If a multi-cell character (such as "橋") is shifted so that the cell is split
in half, the multi-cell character can either be clipped or erased. Typical
behavior is to clip at the right edge of the screen and erase at a right
margin, but either behavior is acceptable.

## Validation

### ICH V-1: No Scroll Region, Fits on Screen

```bash
printf "ABC"
printf "\033[1G"
printf "\033[2@"
```

```
|XcABC_____|
```

### ICH V-2: SGR State

```bash
printf "ABC"
printf "\033[1G"
printf "\033[41m"
printf "\033[2@"
printf "X"
```

```
|c_ABC_____|
```

The `c_` cells should both have a red background. The `ABC` cells should
remain unchanged in style.

### ICH V-3: Shifting Content Off the Screen

```bash
cols=$(tput cols)
printf "\033[${cols}G"
printf "\033[2D"
printf "ABC"
printf "\033[2D"
printf "\033[2@"
printf "X"
```

```
|_______XcA|
```

### ICH V-4: Inside Left/Right Scroll Region

```bash
printf "\033[1;1H" # move to top-left
printf "\033[0J" # clear screen
printf "\033[?69h" # enable left/right margins
printf "\033[3;5s" # scroll region left/right
printf "\033[3G"
printf "ABC"
printf "\033[3G"
printf "\033[2@"
printf "X"
```

```
|__XcA_____|
```

### ICH V-5: Outside Left/Right Scroll Region

```bash
printf "\033[1;1H" # move to top-left
printf "\033[0J" # clear screen
printf "\033[?69h" # enable left/right margins
printf "\033[3;5s" # scroll region left/right
printf "\033[3G"
printf "ABC"
printf "\033[1G"
printf "\033[2@"
printf "X"
```

```
|XcABC_____|
```

### ICH V-6: Split Wide Character

```bash
cols=$(tput cols)
printf "\033[${cols}G"
printf "\033[1D"
printf ""
printf "\033[2D"
printf "\033[@"
printf "X"
```

```
|_______Xc_|
```

In this case, it is valid for the last cell to be blank or to clip the
multi-cell character. xterm clips the character but many other terminals
erase the cell.

0 comments on commit 5ef5899

Please sign in to comment.