mirror of
https://github.com/ghostty-org/ghostty.git
synced 2025-07-23 20:26:09 +03:00
terminal: handle wide character print at edge with wraparound disabled
Fixes #1343 If a wide character is found at the right edge of a terminal screen and can't be printed without wrapping, the wide character is ignored. This matches xterm behavior. The one funky behavior is with grapheme clustering enabled and VS16 emoji. For VS16, we act as if VS16 was never received. This is not specified in any way and no other terminal handles this correctly at the time of authoring this so we're just making this up because it seems most sensible.
This commit is contained in:
@ -749,7 +749,18 @@ pub fn print(self: *Terminal, c: u21) !void {
|
||||
// same grapheme. Otherwise, we can stay in this cell.
|
||||
const Prev = struct { cell: *Screen.Cell, x: usize };
|
||||
const prev: Prev = prev: {
|
||||
const x = self.screen.cursor.x - 1;
|
||||
const x = x: {
|
||||
// If we have wraparound, then we always use the prev col
|
||||
if (self.modes.get(.wraparound)) break :x self.screen.cursor.x - 1;
|
||||
|
||||
// If we do not have wraparound, the logic is trickier. If
|
||||
// we're not on the last column, then we just use the previous
|
||||
// column. Otherwise, we need to check if there is text to
|
||||
// figure out if we're attaching to the prev or current.
|
||||
if (self.screen.cursor.x != right_limit - 1) break :x self.screen.cursor.x - 1;
|
||||
const current = row.getCellPtr(self.screen.cursor.x);
|
||||
break :x self.screen.cursor.x - @intFromBool(current.char == 0);
|
||||
};
|
||||
const immediate = row.getCellPtr(x);
|
||||
|
||||
// If the previous cell is a wide spacer tail, then we actually
|
||||
@ -811,6 +822,7 @@ pub fn print(self: *Terminal, c: u21) !void {
|
||||
// to insert spacers and wrap. Then we just print the wide
|
||||
// char as normal.
|
||||
if (prev.x == right_limit - 1) {
|
||||
if (!self.modes.get(.wraparound)) return;
|
||||
const spacer_head = self.printCell(' ');
|
||||
spacer_head.attrs.wide_spacer_head = true;
|
||||
try self.printWrap();
|
||||
@ -925,6 +937,11 @@ pub fn print(self: *Terminal, c: u21) !void {
|
||||
// to insert spacers and wrap. Then we just print the wide
|
||||
// char as normal.
|
||||
if (self.screen.cursor.x == right_limit - 1) {
|
||||
// If we don't have wraparound enabled then we don't print
|
||||
// this character at all and don't move the cursor. This is
|
||||
// how xterm behaves.
|
||||
if (!self.modes.get(.wraparound)) return;
|
||||
|
||||
const spacer_head = self.printCell(' ');
|
||||
spacer_head.attrs.wide_spacer_head = true;
|
||||
try self.printWrap();
|
||||
@ -2528,6 +2545,92 @@ test "Terminal: soft wrap with semantic prompt" {
|
||||
}
|
||||
}
|
||||
|
||||
test "Terminal: disabled wraparound with wide char and one space" {
|
||||
var t = try init(testing.allocator, 5, 5);
|
||||
defer t.deinit(testing.allocator);
|
||||
|
||||
t.modes.set(.wraparound, false);
|
||||
|
||||
// This puts our cursor at the end and there is NO SPACE for a
|
||||
// wide character.
|
||||
try t.printString("AAAA");
|
||||
try t.print(0x1F6A8); // Police car light
|
||||
try testing.expectEqual(@as(usize, 0), t.screen.cursor.y);
|
||||
try testing.expectEqual(@as(usize, 4), t.screen.cursor.x);
|
||||
|
||||
{
|
||||
const str = try t.plainString(testing.allocator);
|
||||
defer testing.allocator.free(str);
|
||||
try testing.expectEqualStrings("AAAA", str);
|
||||
}
|
||||
|
||||
// Make sure we printed nothing
|
||||
const row = t.screen.getRow(.{ .screen = 0 });
|
||||
{
|
||||
const cell = row.getCell(4);
|
||||
try testing.expectEqual(@as(u32, 0), cell.char);
|
||||
try testing.expect(!cell.attrs.wide);
|
||||
}
|
||||
}
|
||||
|
||||
test "Terminal: disabled wraparound with wide char and no space" {
|
||||
var t = try init(testing.allocator, 5, 5);
|
||||
defer t.deinit(testing.allocator);
|
||||
|
||||
t.modes.set(.wraparound, false);
|
||||
|
||||
// This puts our cursor at the end and there is NO SPACE for a
|
||||
// wide character.
|
||||
try t.printString("AAAAA");
|
||||
try t.print(0x1F6A8); // Police car light
|
||||
try testing.expectEqual(@as(usize, 0), t.screen.cursor.y);
|
||||
try testing.expectEqual(@as(usize, 4), t.screen.cursor.x);
|
||||
|
||||
{
|
||||
const str = try t.plainString(testing.allocator);
|
||||
defer testing.allocator.free(str);
|
||||
try testing.expectEqualStrings("AAAAA", str);
|
||||
}
|
||||
|
||||
// Make sure we printed nothing
|
||||
const row = t.screen.getRow(.{ .screen = 0 });
|
||||
{
|
||||
const cell = row.getCell(4);
|
||||
try testing.expectEqual(@as(u32, 'A'), cell.char);
|
||||
try testing.expect(!cell.attrs.wide);
|
||||
}
|
||||
}
|
||||
|
||||
test "Terminal: disabled wraparound with wide grapheme and half space" {
|
||||
var t = try init(testing.allocator, 5, 5);
|
||||
defer t.deinit(testing.allocator);
|
||||
|
||||
t.modes.set(.grapheme_cluster, true);
|
||||
t.modes.set(.wraparound, false);
|
||||
|
||||
// This puts our cursor at the end and there is NO SPACE for a
|
||||
// wide character.
|
||||
try t.printString("AAAA");
|
||||
try t.print(0x2764); // Heart
|
||||
try t.print(0xFE0F); // VS16 to make wide
|
||||
try testing.expectEqual(@as(usize, 0), t.screen.cursor.y);
|
||||
try testing.expectEqual(@as(usize, 4), t.screen.cursor.x);
|
||||
|
||||
{
|
||||
const str = try t.plainString(testing.allocator);
|
||||
defer testing.allocator.free(str);
|
||||
try testing.expectEqualStrings("AAAA❤", str);
|
||||
}
|
||||
|
||||
// Make sure we printed nothing
|
||||
const row = t.screen.getRow(.{ .screen = 0 });
|
||||
{
|
||||
const cell = row.getCell(4);
|
||||
try testing.expectEqual(@as(u32, '❤'), cell.char);
|
||||
try testing.expect(!cell.attrs.wide);
|
||||
}
|
||||
}
|
||||
|
||||
test "Terminal: print writes to bottom if scrolled" {
|
||||
var t = try init(testing.allocator, 5, 2);
|
||||
defer t.deinit(testing.allocator);
|
||||
|
Reference in New Issue
Block a user