mirror of
https://github.com/ghostty-org/ghostty.git
synced 2025-07-16 00:36:07 +03:00
selection of wide chars and copy/paste works
This commit is contained in:
@ -25,8 +25,10 @@ const Screen = @This();
|
||||
// one day.
|
||||
|
||||
const std = @import("std");
|
||||
const utf8proc = @import("utf8proc");
|
||||
const assert = std.debug.assert;
|
||||
const Allocator = std.mem.Allocator;
|
||||
|
||||
const color = @import("color.zig");
|
||||
const point = @import("point.zig");
|
||||
const Selection = @import("Selection.zig");
|
||||
@ -941,6 +943,10 @@ pub fn selectionString(self: Screen, alloc: Allocator, sel: Selection) ![:0]cons
|
||||
i += 1;
|
||||
}
|
||||
|
||||
// Skip spacers
|
||||
if (cell.attrs.wide_spacer_head == 1 or
|
||||
cell.attrs.wide_spacer_tail == 1) continue;
|
||||
|
||||
const char = if (cell.char > 0) cell.char else ' ';
|
||||
i += try std.unicode.utf8Encode(@intCast(u21, char), buf[i..]);
|
||||
}
|
||||
@ -964,6 +970,10 @@ pub fn selectionString(self: Screen, alloc: Allocator, sel: Selection) ![:0]cons
|
||||
}
|
||||
}
|
||||
|
||||
// Skip spacers
|
||||
if (cell.attrs.wide_spacer_head == 1 or
|
||||
cell.attrs.wide_spacer_tail == 1) continue;
|
||||
|
||||
const char = if (cell.char > 0) cell.char else ' ';
|
||||
i += try std.unicode.utf8Encode(@intCast(u21, char), buf[i..]);
|
||||
}
|
||||
@ -979,7 +989,7 @@ pub fn selectionString(self: Screen, alloc: Allocator, sel: Selection) ![:0]cons
|
||||
/// Returns the slices that make up the selection, in order. There are at most
|
||||
/// two parts to handle the ring buffer. If the selection fits in one contiguous
|
||||
/// slice, then the second slice will have a length of zero.
|
||||
fn selectionSlices(self: Screen, sel: Selection) struct {
|
||||
fn selectionSlices(self: Screen, sel_raw: Selection) struct {
|
||||
// Top offset can be used to determine if a newline is required by
|
||||
// seeing if the cell index plus the offset cleanly divides by screen cols.
|
||||
top_offset: usize,
|
||||
@ -988,10 +998,35 @@ fn selectionSlices(self: Screen, sel: Selection) struct {
|
||||
} {
|
||||
// Note: this function is tested via selectionString
|
||||
|
||||
assert(sel.start.y < self.totalRows());
|
||||
assert(sel.end.y < self.totalRows());
|
||||
assert(sel.start.x < self.cols);
|
||||
assert(sel.end.x < self.cols);
|
||||
assert(sel_raw.start.y < self.totalRows());
|
||||
assert(sel_raw.end.y < self.totalRows());
|
||||
assert(sel_raw.start.x < self.cols);
|
||||
assert(sel_raw.end.x < self.cols);
|
||||
|
||||
const sel = sel: {
|
||||
var sel = sel_raw;
|
||||
|
||||
// If the end of our selection is a wide char leader, include the
|
||||
// first part of the next line.
|
||||
if (sel.end.x == self.cols - 1) {
|
||||
const row = self.getRow(.{ .screen = sel.end.y });
|
||||
if (row[sel.end.x].attrs.wide_spacer_head == 1) {
|
||||
sel.end.y += 1;
|
||||
sel.end.x = 0;
|
||||
}
|
||||
}
|
||||
|
||||
// If the start of our selection is a wide char spacer, include the
|
||||
// wide char.
|
||||
if (sel.start.x > 0) {
|
||||
const row = self.getRow(.{ .screen = sel.start.y });
|
||||
if (row[sel.start.x].attrs.wide_spacer_tail == 1) {
|
||||
sel.end.x -= 1;
|
||||
}
|
||||
}
|
||||
|
||||
break :sel sel;
|
||||
};
|
||||
|
||||
// Get the true "top" and "bottom"
|
||||
const sel_top = sel.topLeft();
|
||||
@ -1053,7 +1088,10 @@ pub fn testString(self: Screen, alloc: Allocator, tag: RowIndexTag) ![]const u8
|
||||
fn testWriteString(self: *Screen, text: []const u8) void {
|
||||
var y: usize = 0;
|
||||
var x: usize = 0;
|
||||
for (text) |c| {
|
||||
|
||||
const view = std.unicode.Utf8View.init(text) catch unreachable;
|
||||
var iter = view.iterator();
|
||||
while (iter.nextCodepoint()) |c| {
|
||||
// Explicit newline forces a new row
|
||||
if (c == '\n') {
|
||||
y += 1;
|
||||
@ -1082,7 +1120,39 @@ fn testWriteString(self: *Screen, text: []const u8) void {
|
||||
row = self.getRow(.{ .active = y });
|
||||
}
|
||||
|
||||
row[x].char = @intCast(u32, c);
|
||||
// If our character is double-width, handle it.
|
||||
const width = utf8proc.charwidth(c);
|
||||
assert(width == 1 or width == 2);
|
||||
switch (width) {
|
||||
1 => row[x].char = @intCast(u32, c),
|
||||
|
||||
2 => {
|
||||
if (x == self.cols - 1) {
|
||||
row[x].char = ' ';
|
||||
row[x].attrs.wide_spacer_head = 1;
|
||||
|
||||
// wrap
|
||||
row[x].attrs.wrap = 1;
|
||||
y += 1;
|
||||
x = 0;
|
||||
if (y >= self.rows) {
|
||||
y -= 1;
|
||||
self.scroll(.{ .delta = 1 });
|
||||
}
|
||||
row = self.getRow(.{ .active = y });
|
||||
}
|
||||
|
||||
row[x].char = @intCast(u32, c);
|
||||
row[x].attrs.wide = 1;
|
||||
|
||||
x += 1;
|
||||
row[x].char = ' ';
|
||||
row[x].attrs.wide_spacer_tail = 1;
|
||||
},
|
||||
|
||||
else => unreachable,
|
||||
}
|
||||
|
||||
x += 1;
|
||||
}
|
||||
}
|
||||
@ -1468,6 +1538,66 @@ test "Screen: selectionString wrap around" {
|
||||
}
|
||||
}
|
||||
|
||||
test "Screen: selectionString wide char" {
|
||||
const testing = std.testing;
|
||||
const alloc = testing.allocator;
|
||||
|
||||
var s = try init(alloc, 3, 5, 0);
|
||||
defer s.deinit(alloc);
|
||||
const str = "1A⚡";
|
||||
s.testWriteString(str);
|
||||
|
||||
{
|
||||
var contents = try s.selectionString(alloc, .{
|
||||
.start = .{ .x = 0, .y = 0 },
|
||||
.end = .{ .x = 3, .y = 0 },
|
||||
});
|
||||
defer alloc.free(contents);
|
||||
const expected = str;
|
||||
try testing.expectEqualStrings(expected, contents);
|
||||
}
|
||||
|
||||
{
|
||||
var contents = try s.selectionString(alloc, .{
|
||||
.start = .{ .x = 0, .y = 0 },
|
||||
.end = .{ .x = 2, .y = 0 },
|
||||
});
|
||||
defer alloc.free(contents);
|
||||
const expected = str;
|
||||
try testing.expectEqualStrings(expected, contents);
|
||||
}
|
||||
|
||||
{
|
||||
var contents = try s.selectionString(alloc, .{
|
||||
.start = .{ .x = 3, .y = 0 },
|
||||
.end = .{ .x = 3, .y = 0 },
|
||||
});
|
||||
defer alloc.free(contents);
|
||||
const expected = "⚡";
|
||||
try testing.expectEqualStrings(expected, contents);
|
||||
}
|
||||
}
|
||||
|
||||
test "Screen: selectionString wide char with header" {
|
||||
const testing = std.testing;
|
||||
const alloc = testing.allocator;
|
||||
|
||||
var s = try init(alloc, 3, 5, 0);
|
||||
defer s.deinit(alloc);
|
||||
const str = "1ABC⚡";
|
||||
s.testWriteString(str);
|
||||
|
||||
{
|
||||
var contents = try s.selectionString(alloc, .{
|
||||
.start = .{ .x = 0, .y = 0 },
|
||||
.end = .{ .x = 4, .y = 0 },
|
||||
});
|
||||
defer alloc.free(contents);
|
||||
const expected = str;
|
||||
try testing.expectEqualStrings(expected, contents);
|
||||
}
|
||||
}
|
||||
|
||||
test "Screen: resize more rows no scrollback" {
|
||||
const testing = std.testing;
|
||||
const alloc = testing.allocator;
|
||||
|
@ -49,11 +49,11 @@ pub fn Stream(comptime Handler: type) type {
|
||||
//log.debug("char: {x}", .{c});
|
||||
const actions = self.parser.next(c);
|
||||
for (actions) |action_opt| {
|
||||
if (action_opt) |action| {
|
||||
if (action != .print) {
|
||||
log.info("action: {}", .{action});
|
||||
}
|
||||
}
|
||||
// if (action_opt) |action| {
|
||||
// if (action != .print) {
|
||||
// log.info("action: {}", .{action});
|
||||
// }
|
||||
// }
|
||||
switch (action_opt orelse continue) {
|
||||
.print => |p| if (@hasDecl(T, "print")) try self.handler.print(p),
|
||||
.execute => |code| try self.execute(code),
|
||||
|
Reference in New Issue
Block a user