Merge pull request #639 from mitchellh/il

xterm audit: insert line (IL), delete line (DL)
This commit is contained in:
Mitchell Hashimoto
2023-10-08 21:40:32 -07:00
committed by GitHub
4 changed files with 506 additions and 14 deletions

View File

@ -498,6 +498,27 @@ pub const Row = struct {
} }
} }
/// Copy a single cell from column x in src to column x in this row.
pub fn copyCell(self: Row, src: Row, x: usize) !void {
const dst_cell = self.getCellPtr(x);
const src_cell = src.getCellPtr(x);
// If our destination has graphemes, we have to clear them.
if (dst_cell.attrs.grapheme) self.clearGraphemes(x);
dst_cell.* = src_cell.*;
// If the source doesn't have any graphemes, then we can just copy.
if (!src_cell.attrs.grapheme) return;
// Source cell has graphemes. Copy them.
const src_key = src.getId() + x + 1;
const src_data = src.screen.graphemes.get(src_key) orelse return;
const dst_key = self.getId() + x + 1;
const dst_gop = try self.screen.graphemes.getOrPut(self.screen.alloc, dst_key);
dst_gop.value_ptr.* = try src_data.copy(self.screen.alloc);
self.storage[0].header.flags.grapheme = true;
}
/// Copy the row src into this row. The row can be from another screen. /// Copy the row src into this row. The row can be from another screen.
pub fn copyRow(self: Row, src: Row) !void { pub fn copyRow(self: Row, src: Row) !void {
// If we have graphemes, clear first to unset them. // If we have graphemes, clear first to unset them.

View File

@ -1564,8 +1564,14 @@ pub fn insertLines(self: *Terminal, count: usize) !void {
// Rare, but happens // Rare, but happens
if (count == 0) return; if (count == 0) return;
// If the cursor is outside the scroll region we do nothing.
if (self.screen.cursor.y < self.scrolling_region.top or
self.screen.cursor.y > self.scrolling_region.bottom or
self.screen.cursor.x < self.scrolling_region.left or
self.screen.cursor.x > self.scrolling_region.right) return;
// Move the cursor to the left margin // Move the cursor to the left margin
self.screen.cursor.x = 0; self.screen.cursor.x = self.scrolling_region.left;
self.screen.cursor.pending_wrap = false; self.screen.cursor.pending_wrap = false;
// Remaining rows from our cursor // Remaining rows from our cursor
@ -1582,14 +1588,21 @@ pub fn insertLines(self: *Terminal, count: usize) !void {
// Ensure we have the lines populated to the end // Ensure we have the lines populated to the end
while (y > top) : (y -= 1) { while (y > top) : (y -= 1) {
try self.screen.copyRow(.{ .active = y }, .{ .active = y - adjusted_count }); const src = self.screen.getRow(.{ .active = y - adjusted_count });
const dst = self.screen.getRow(.{ .active = y });
for (self.scrolling_region.left..self.scrolling_region.right + 1) |x| {
try dst.copyCell(src, x);
}
} }
// Insert count blank lines // Insert count blank lines
y = self.screen.cursor.y; y = self.screen.cursor.y;
while (y < self.screen.cursor.y + adjusted_count) : (y += 1) { while (y < self.screen.cursor.y + adjusted_count) : (y += 1) {
const row = self.screen.getRow(.{ .active = y }); const row = self.screen.getRow(.{ .active = y });
row.clear(self.screen.cursor.pen); row.fillSlice(.{
.bg = self.screen.cursor.pen.bg,
.attrs = .{ .has_bg = self.screen.cursor.pen.attrs.has_bg },
}, self.scrolling_region.left, self.scrolling_region.right + 1);
} }
} }
@ -1613,23 +1626,58 @@ pub fn deleteLines(self: *Terminal, count: usize) !void {
const tracy = trace(@src()); const tracy = trace(@src());
defer tracy.end(); defer tracy.end();
// If our cursor is outside of the scroll region, do nothing. // If the cursor is outside the scroll region we do nothing.
if (self.screen.cursor.y < self.scrolling_region.top or if (self.screen.cursor.y < self.scrolling_region.top or
self.screen.cursor.y > self.scrolling_region.bottom) self.screen.cursor.y > self.scrolling_region.bottom or
self.screen.cursor.x < self.scrolling_region.left or
self.screen.cursor.x > self.scrolling_region.right) return;
// Move the cursor to the left margin
self.screen.cursor.x = self.scrolling_region.left;
self.screen.cursor.pending_wrap = false;
// If this is a full line margin then we can do a faster scroll.
if (self.scrolling_region.left == 0 and
self.scrolling_region.right == self.cols - 1)
{ {
self.screen.scrollRegionUp(
.{ .active = self.screen.cursor.y },
.{ .active = self.scrolling_region.bottom },
@min(count, self.scrolling_region.bottom - self.screen.cursor.y),
);
return; return;
} }
// Move the cursor to the left margin // Left/right margin is set, we need to do a slower scroll.
self.screen.cursor.x = 0; // Remaining rows from our cursor in the region, 1-indexed.
self.screen.cursor.pending_wrap = false; const rem = self.scrolling_region.bottom - self.screen.cursor.y + 1;
// Perform the scroll // If our count is greater than the remaining amount, we can just
self.screen.scrollRegionUp( // clear the region using insertLines.
.{ .active = self.screen.cursor.y }, if (count >= rem) {
.{ .active = self.scrolling_region.bottom }, try self.insertLines(count);
@min(count, self.scrolling_region.bottom - self.screen.cursor.y), return;
); }
// The amount of lines we need to scroll up.
const scroll_amount = rem - count;
const scroll_top = self.scrolling_region.bottom - scroll_amount;
for (self.screen.cursor.y..scroll_top + 1) |y| {
const src = self.screen.getRow(.{ .active = y + count });
const dst = self.screen.getRow(.{ .active = y });
for (self.scrolling_region.left..self.scrolling_region.right + 1) |x| {
try dst.copyCell(src, x);
}
}
// Insert blank lines
for (scroll_top + 1..self.scrolling_region.bottom + 1) |y| {
const row = self.screen.getRow(.{ .active = y });
row.fillSlice(.{
.bg = self.screen.cursor.pen.bg,
.attrs = .{ .has_bg = self.screen.cursor.pen.attrs.has_bg },
}, self.scrolling_region.left, self.scrolling_region.right + 1);
}
} }
/// Scroll the text down by one row. /// Scroll the text down by one row.
@ -2648,6 +2696,171 @@ test "Terminal: deleteLines resets wrap" {
} }
} }
test "Terminal: deleteLines simple" {
const alloc = testing.allocator;
var t = try init(alloc, 5, 5);
defer t.deinit(alloc);
try t.printString("ABC");
t.carriageReturn();
try t.linefeed();
try t.printString("DEF");
t.carriageReturn();
try t.linefeed();
try t.printString("GHI");
t.setCursorPos(2, 2);
try t.deleteLines(1);
{
var str = try t.plainString(testing.allocator);
defer testing.allocator.free(str);
try testing.expectEqualStrings("ABC\nGHI", str);
}
}
test "Terminal: deleteLines left/right scroll region" {
const alloc = testing.allocator;
var t = try init(alloc, 10, 10);
defer t.deinit(alloc);
try t.printString("ABC123");
t.carriageReturn();
try t.linefeed();
try t.printString("DEF456");
t.carriageReturn();
try t.linefeed();
try t.printString("GHI789");
t.scrolling_region.left = 1;
t.scrolling_region.right = 3;
t.setCursorPos(2, 2);
try t.deleteLines(1);
{
var str = try t.plainString(testing.allocator);
defer testing.allocator.free(str);
try testing.expectEqualStrings("ABC123\nDHI756\nG 89", str);
}
}
test "Terminal: deleteLines left/right scroll region high count" {
const alloc = testing.allocator;
var t = try init(alloc, 10, 10);
defer t.deinit(alloc);
try t.printString("ABC123");
t.carriageReturn();
try t.linefeed();
try t.printString("DEF456");
t.carriageReturn();
try t.linefeed();
try t.printString("GHI789");
t.scrolling_region.left = 1;
t.scrolling_region.right = 3;
t.setCursorPos(2, 2);
try t.deleteLines(100);
{
var str = try t.plainString(testing.allocator);
defer testing.allocator.free(str);
try testing.expectEqualStrings("ABC123\nD 56\nG 89", str);
}
}
test "Terminal: insertLines simple" {
const alloc = testing.allocator;
var t = try init(alloc, 5, 5);
defer t.deinit(alloc);
try t.printString("ABC");
t.carriageReturn();
try t.linefeed();
try t.printString("DEF");
t.carriageReturn();
try t.linefeed();
try t.printString("GHI");
t.setCursorPos(2, 2);
try t.insertLines(1);
{
var str = try t.plainString(testing.allocator);
defer testing.allocator.free(str);
try testing.expectEqualStrings("ABC\n\nDEF\nGHI", str);
}
}
test "Terminal: insertLines outside of scroll region" {
const alloc = testing.allocator;
var t = try init(alloc, 5, 5);
defer t.deinit(alloc);
try t.printString("ABC");
t.carriageReturn();
try t.linefeed();
try t.printString("DEF");
t.carriageReturn();
try t.linefeed();
try t.printString("GHI");
t.setScrollingRegion(3, 4);
t.setCursorPos(2, 2);
try t.insertLines(1);
{
var str = try t.plainString(testing.allocator);
defer testing.allocator.free(str);
try testing.expectEqualStrings("ABC\nDEF\nGHI", str);
}
}
test "Terminal: insertLines top/bottom scroll region" {
const alloc = testing.allocator;
var t = try init(alloc, 5, 5);
defer t.deinit(alloc);
try t.printString("ABC");
t.carriageReturn();
try t.linefeed();
try t.printString("DEF");
t.carriageReturn();
try t.linefeed();
try t.printString("GHI");
t.carriageReturn();
try t.linefeed();
try t.printString("123");
t.setScrollingRegion(1, 3);
t.setCursorPos(2, 2);
try t.insertLines(1);
{
var str = try t.plainString(testing.allocator);
defer testing.allocator.free(str);
try testing.expectEqualStrings("ABC\n\nDEF\n123", str);
}
}
test "Terminal: insertLines left/right scroll region" {
const alloc = testing.allocator;
var t = try init(alloc, 10, 10);
defer t.deinit(alloc);
try t.printString("ABC123");
t.carriageReturn();
try t.linefeed();
try t.printString("DEF456");
t.carriageReturn();
try t.linefeed();
try t.printString("GHI789");
t.scrolling_region.left = 1;
t.scrolling_region.right = 3;
t.setCursorPos(2, 2);
try t.insertLines(1);
{
var str = try t.plainString(testing.allocator);
defer testing.allocator.free(str);
try testing.expectEqualStrings("ABC123\nD 56\nGEF489\n HI7", str);
}
}
test "Terminal: insertLines" { test "Terminal: insertLines" {
const alloc = testing.allocator; const alloc = testing.allocator;
var t = try init(alloc, 2, 5); var t = try init(alloc, 2, 5);
@ -2964,6 +3177,32 @@ test "Terminal: index bottom of primary screen" {
} }
} }
test "Terminal: index bottom of primary screen background sgr" {
const alloc = testing.allocator;
var t = try init(alloc, 5, 5);
defer t.deinit(alloc);
const pen: Screen.Cell = .{
.bg = .{ .r = 0xFF, .g = 0x00, .b = 0x00 },
.attrs = .{ .has_bg = true },
};
t.setCursorPos(5, 1);
try t.print('A');
t.screen.cursor.pen = pen;
try t.index();
{
var str = try t.plainString(testing.allocator);
defer testing.allocator.free(str);
try testing.expectEqualStrings("\n\n\nA", str);
for (0..5) |x| {
const cell = t.screen.getCell(.active, 4, x);
try testing.expectEqual(pen, cell);
}
}
}
test "Terminal: index inside scroll region" { test "Terminal: index inside scroll region" {
const alloc = testing.allocator; const alloc = testing.allocator;
var t = try init(alloc, 5, 5); var t = try init(alloc, 5, 5);

113
website/app/vt/dl/page.mdx Normal file
View File

@ -0,0 +1,113 @@
import VTSequence from "@/components/VTSequence";
# Delete Line (DL)
<VTSequence sequence={["CSI", "Pn", "M"]} />
Deletes `n` lines at the current cursor position and shifts existing
lines up.
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.
If the current cursor position is outside of the current scroll region,
this sequence does nothing. The cursor is outside of the current scroll
region if it is above the [top margin](#TODO), below the [bottom margin](#TODO),
left of the [left margin](#TODO), or right of the [right margin](#TODO).
This sequence unsets the pending wrap state.
This sequence moves the cursor column to the left margin.
Remove the top `n` lines of the current scroll region, and shift existing
lines up. The space created at the bottom of the scroll region should be
blank with the background color set according to the current SGR state.
If a [left margin](#TODO) or [right margin](#TODO) is set, only the cells
within and including the margins are deleted or shifted.
Other existing contents to the left of the left margin or right of the
right margin remains untouched.
If a multi-cell character would be split, erase the full multi-cell
character. For example, if "橋" is printed to the left of the left margin
and shifting the line down as a result of DL would split the character,
the cell should be erased.
## Validation
### DL V-1: Simple Delete Line
```bash
printf "\033[1;1H" # move to top-left
printf "\033[0J" # clear screen
printf "ABC\n"
printf "DEF\n"
printf "GHI\n"
printf "\033[2;2H"
printf "\033[M"
```
```
|ABC_____|
|GHI_____|
```
### DL V-2: Cursor Outside of Scroll Region
```bash
printf "\033[1;1H" # move to top-left
printf "\033[0J" # clear screen
printf "ABC\n"
printf "DEF\n"
printf "GHI\n"
printf "\033[3;4r" # scroll region top/bottom
printf "\033[2;2H"
printf "\033[M"
```
```
|ABC_____|
|DEF_____|
|GHI_____|
```
### DL V-3: Top/Bottom Scroll Regions
```bash
printf "\033[1;1H" # move to top-left
printf "\033[0J" # clear screen
printf "ABC\n"
printf "DEF\n"
printf "GHI\n"
printf "123\n"
printf "\033[1;3r" # scroll region top/bottom
printf "\033[2;2H"
printf "\033[M"
```
```
|ABC_____|
|GHI_____|
|________|
|123_____|
```
### DL V-4: Left/Right Scroll Regions
```bash
printf "\033[1;1H" # move to top-left
printf "\033[0J" # clear screen
printf "ABC123\n"
printf "DEF456\n"
printf "GHI789\n"
printf "\033[?69h" # enable left/right margins
printf "\033[2;4s" # scroll region left/right
printf "\033[2;2H"
printf "\033[M"
```
```
|ABC123__|
|DHI756__|
|G___89__|
```

119
website/app/vt/il/page.mdx Normal file
View File

@ -0,0 +1,119 @@
import VTSequence from "@/components/VTSequence";
# Insert Line (IL)
<VTSequence sequence={["CSI", "Pn", "L"]} />
Inserts `n` lines at the current cursor position and shifts existing
lines down.
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.
If the current cursor position is outside of the current scroll region,
this sequence does nothing. The cursor is outside of the current scroll
region if it is above the [top margin](#TODO), below the [bottom margin](#TODO),
left of the [left margin](#TODO), or right of the [right margin](#TODO).
This sequence unsets the pending wrap state.
This sequence moves the cursor column to the left margin.
From the current cursor row down `n` lines, insert blank lines colored
with a background color according to the current SGR state. When a line is
inserted, shift all existing content down one line. The bottommost row
is the bottom margin. If content is shifted beyond the bottom margin,
it is lost and the existing content beyond the bottom margin is preserved
and not shifted.
If a [left margin](#TODO) or [right margin](#TODO) is set, only the cells
within and including the margins are blanked (when inserted) or shifted.
Other existing contents to the left of the left margin or right of the
right margin remains untouched.
If a multi-cell character would be split, erase the full multi-cell
character. For example, if "橋" is printed to the left of the left margin
and shifting the line down as a result of IL would split the character,
the cell should be erased.
## Validation
### IL V-1: Simple Insert Line
```bash
printf "\033[1;1H" # move to top-left
printf "\033[0J" # clear screen
printf "ABC\n"
printf "DEF\n"
printf "GHI\n"
printf "\033[2;2H"
printf "\033[L"
```
```
|ABC_____|
|c_______|
|DEF_____|
|GHI_____|
```
### IL V-2: Cursor Outside of Scroll Region
```bash
printf "\033[1;1H" # move to top-left
printf "\033[0J" # clear screen
printf "ABC\n"
printf "DEF\n"
printf "GHI\n"
printf "\033[3;4r" # scroll region top/bottom
printf "\033[2;2H"
printf "\033[L"
```
```
|ABC_____|
|DEF_____|
|GHI_____|
```
### IL V-3: Top/Bottom Scroll Regions
```bash
printf "\033[1;1H" # move to top-left
printf "\033[0J" # clear screen
printf "ABC\n"
printf "DEF\n"
printf "GHI\n"
printf "123\n"
printf "\033[1;3r" # scroll region top/bottom
printf "\033[2;2H"
printf "\033[L"
```
```
|ABC_____|
|_c______|
|DEF_____|
|123_____|
```
### IL V-4: Left/Right Scroll Regions
```bash
printf "\033[1;1H" # move to top-left
printf "\033[0J" # clear screen
printf "ABC123\n"
printf "DEF456\n"
printf "GHI789\n"
printf "\033[?69h" # enable left/right margins
printf "\033[2;4s" # scroll region left/right
printf "\033[2;2H"
printf "\033[L"
```
```
|ABC123__|
|Dc__56__|
|GEF489__|
|_HI7____|
```