mirror of
https://github.com/ghostty-org/ghostty.git
synced 2025-07-17 01:06:08 +03:00
copying selection trims trailing whitespace
This is configurable with `clipboard-trim-trailing-spaces`. This also fixes a bug where debug builds would crash when copying blank lines. This never affected release builds.
This commit is contained in:
@ -987,6 +987,7 @@ fn keyCallback(
|
|||||||
var buf = win.io.terminal.screen.selectionString(
|
var buf = win.io.terminal.screen.selectionString(
|
||||||
win.alloc,
|
win.alloc,
|
||||||
sel,
|
sel,
|
||||||
|
win.config.@"clipboard-trim-trailing-spaces",
|
||||||
) catch |err| {
|
) catch |err| {
|
||||||
log.err("error reading selection string err={}", .{err});
|
log.err("error reading selection string err={}", .{err});
|
||||||
return;
|
return;
|
||||||
|
@ -148,6 +148,10 @@ pub const Config = struct {
|
|||||||
@"clipboard-read": bool = false,
|
@"clipboard-read": bool = false,
|
||||||
@"clipboard-write": bool = true,
|
@"clipboard-write": bool = true,
|
||||||
|
|
||||||
|
/// Trims trailing whitespace on data that is copied to the clipboard.
|
||||||
|
/// This does not affect data sent to the clipboard via "clipboard-write".
|
||||||
|
@"clipboard-trim-trailing-spaces": bool = true,
|
||||||
|
|
||||||
/// The time in milliseconds between clicks to consider a click a repeat
|
/// The time in milliseconds between clicks to consider a click a repeat
|
||||||
/// (double, triple, etc.) or an entirely new single click. A value of
|
/// (double, triple, etc.) or an entirely new single click. A value of
|
||||||
/// zero will use a platform-specific default. The default on macOS
|
/// zero will use a platform-specific default. The default on macOS
|
||||||
|
@ -1454,7 +1454,12 @@ fn scrollDelta(self: *Screen, delta: isize, grow: bool) !void {
|
|||||||
/// Returns the raw text associated with a selection. This will unwrap
|
/// Returns the raw text associated with a selection. This will unwrap
|
||||||
/// soft-wrapped edges. The returned slice is owned by the caller and allocated
|
/// soft-wrapped edges. The returned slice is owned by the caller and allocated
|
||||||
/// using alloc, not the allocator associated with the screen (unless they match).
|
/// using alloc, not the allocator associated with the screen (unless they match).
|
||||||
pub fn selectionString(self: *Screen, alloc: Allocator, sel: Selection) ![:0]const u8 {
|
pub fn selectionString(
|
||||||
|
self: *Screen,
|
||||||
|
alloc: Allocator,
|
||||||
|
sel: Selection,
|
||||||
|
trim: bool,
|
||||||
|
) ![:0]const u8 {
|
||||||
// Get the slices for the string
|
// Get the slices for the string
|
||||||
const slices = self.selectionSlices(sel);
|
const slices = self.selectionSlices(sel);
|
||||||
|
|
||||||
@ -1472,6 +1477,21 @@ pub fn selectionString(self: *Screen, alloc: Allocator, sel: Selection) ![:0]con
|
|||||||
// We use each row header as an opportunity to "count"
|
// We use each row header as an opportunity to "count"
|
||||||
// a new row, and therefore count a possible newline.
|
// a new row, and therefore count a possible newline.
|
||||||
count += 1;
|
count += 1;
|
||||||
|
|
||||||
|
// If we have runtime safety, then we can have invalidly
|
||||||
|
// tagged cells because all cells are headers by default.
|
||||||
|
// This isn't an issue in prod builds because the zero values
|
||||||
|
// we use are correct by default.
|
||||||
|
if (std.debug.runtime_safety) {
|
||||||
|
if (cell.header.id == 0) {
|
||||||
|
std.mem.set(
|
||||||
|
StorageCell,
|
||||||
|
slice[i + 1 .. i + 1 + self.cols],
|
||||||
|
.{ .cell = .{} },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1535,6 +1555,24 @@ pub fn selectionString(self: *Screen, alloc: Allocator, sel: Selection) ![:0]con
|
|||||||
// Remove our trailing newline, its never correct.
|
// Remove our trailing newline, its never correct.
|
||||||
if (buf[buf_i - 1] == '\n') buf_i -= 1;
|
if (buf[buf_i - 1] == '\n') buf_i -= 1;
|
||||||
|
|
||||||
|
// Remove any trailing spaces on lines. We could do optimize this by
|
||||||
|
// doing this in the loop above but this isn't very hot path code and
|
||||||
|
// this is simple.
|
||||||
|
if (trim) {
|
||||||
|
var it = std.mem.tokenize(u8, buf[0..buf_i], "\n");
|
||||||
|
buf_i = 0;
|
||||||
|
while (it.next()) |line| {
|
||||||
|
const trimmed = std.mem.trimRight(u8, line, " \t");
|
||||||
|
std.mem.copy(u8, buf[buf_i..], trimmed);
|
||||||
|
buf_i += trimmed.len;
|
||||||
|
buf[buf_i] = '\n';
|
||||||
|
buf_i += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove our trailing newline again
|
||||||
|
if (buf_i > 0) buf_i -= 1;
|
||||||
|
}
|
||||||
|
|
||||||
// Add null termination
|
// Add null termination
|
||||||
buf[buf_i] = 0;
|
buf[buf_i] = 0;
|
||||||
|
|
||||||
@ -3094,7 +3132,7 @@ test "Screen: clear history" {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
test "Screen: selectionString" {
|
test "Screen: selectionString basic" {
|
||||||
const testing = std.testing;
|
const testing = std.testing;
|
||||||
const alloc = testing.allocator;
|
const alloc = testing.allocator;
|
||||||
|
|
||||||
@ -3107,13 +3145,75 @@ test "Screen: selectionString" {
|
|||||||
var contents = try s.selectionString(alloc, .{
|
var contents = try s.selectionString(alloc, .{
|
||||||
.start = .{ .x = 0, .y = 1 },
|
.start = .{ .x = 0, .y = 1 },
|
||||||
.end = .{ .x = 2, .y = 2 },
|
.end = .{ .x = 2, .y = 2 },
|
||||||
});
|
}, true);
|
||||||
defer alloc.free(contents);
|
defer alloc.free(contents);
|
||||||
const expected = "2EFGH\n3IJ";
|
const expected = "2EFGH\n3IJ";
|
||||||
try testing.expectEqualStrings(expected, contents);
|
try testing.expectEqualStrings(expected, contents);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
test "Screen: selectionString trim space" {
|
||||||
|
const testing = std.testing;
|
||||||
|
const alloc = testing.allocator;
|
||||||
|
|
||||||
|
var s = try init(alloc, 3, 5, 0);
|
||||||
|
defer s.deinit();
|
||||||
|
const str = "1AB \n2EFGH\n3IJKL";
|
||||||
|
try s.testWriteString(str);
|
||||||
|
|
||||||
|
{
|
||||||
|
var contents = try s.selectionString(alloc, .{
|
||||||
|
.start = .{ .x = 0, .y = 0 },
|
||||||
|
.end = .{ .x = 2, .y = 1 },
|
||||||
|
}, true);
|
||||||
|
defer alloc.free(contents);
|
||||||
|
const expected = "1AB\n2EF";
|
||||||
|
try testing.expectEqualStrings(expected, contents);
|
||||||
|
}
|
||||||
|
|
||||||
|
// No trim
|
||||||
|
{
|
||||||
|
var contents = try s.selectionString(alloc, .{
|
||||||
|
.start = .{ .x = 0, .y = 0 },
|
||||||
|
.end = .{ .x = 2, .y = 1 },
|
||||||
|
}, false);
|
||||||
|
defer alloc.free(contents);
|
||||||
|
const expected = "1AB \n2EF";
|
||||||
|
try testing.expectEqualStrings(expected, contents);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
test "Screen: selectionString trim empty line" {
|
||||||
|
const testing = std.testing;
|
||||||
|
const alloc = testing.allocator;
|
||||||
|
|
||||||
|
var s = try init(alloc, 5, 5, 0);
|
||||||
|
defer s.deinit();
|
||||||
|
const str = "1AB \n\n2EFGH\n3IJKL";
|
||||||
|
try s.testWriteString(str);
|
||||||
|
|
||||||
|
{
|
||||||
|
var contents = try s.selectionString(alloc, .{
|
||||||
|
.start = .{ .x = 0, .y = 0 },
|
||||||
|
.end = .{ .x = 2, .y = 2 },
|
||||||
|
}, true);
|
||||||
|
defer alloc.free(contents);
|
||||||
|
const expected = "1AB\n\n2EF";
|
||||||
|
try testing.expectEqualStrings(expected, contents);
|
||||||
|
}
|
||||||
|
|
||||||
|
// No trim
|
||||||
|
{
|
||||||
|
var contents = try s.selectionString(alloc, .{
|
||||||
|
.start = .{ .x = 0, .y = 0 },
|
||||||
|
.end = .{ .x = 2, .y = 2 },
|
||||||
|
}, false);
|
||||||
|
defer alloc.free(contents);
|
||||||
|
const expected = "1AB \n \n2EF";
|
||||||
|
try testing.expectEqualStrings(expected, contents);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
test "Screen: selectionString soft wrap" {
|
test "Screen: selectionString soft wrap" {
|
||||||
const testing = std.testing;
|
const testing = std.testing;
|
||||||
const alloc = testing.allocator;
|
const alloc = testing.allocator;
|
||||||
@ -3127,7 +3227,7 @@ test "Screen: selectionString soft wrap" {
|
|||||||
var contents = try s.selectionString(alloc, .{
|
var contents = try s.selectionString(alloc, .{
|
||||||
.start = .{ .x = 0, .y = 1 },
|
.start = .{ .x = 0, .y = 1 },
|
||||||
.end = .{ .x = 2, .y = 2 },
|
.end = .{ .x = 2, .y = 2 },
|
||||||
});
|
}, true);
|
||||||
defer alloc.free(contents);
|
defer alloc.free(contents);
|
||||||
const expected = "2EFGH3IJ";
|
const expected = "2EFGH3IJ";
|
||||||
try testing.expectEqualStrings(expected, contents);
|
try testing.expectEqualStrings(expected, contents);
|
||||||
@ -3153,7 +3253,7 @@ test "Screen: selectionString wrap around" {
|
|||||||
var contents = try s.selectionString(alloc, .{
|
var contents = try s.selectionString(alloc, .{
|
||||||
.start = .{ .x = 0, .y = 1 },
|
.start = .{ .x = 0, .y = 1 },
|
||||||
.end = .{ .x = 2, .y = 2 },
|
.end = .{ .x = 2, .y = 2 },
|
||||||
});
|
}, true);
|
||||||
defer alloc.free(contents);
|
defer alloc.free(contents);
|
||||||
const expected = "2EFGH\n3IJ";
|
const expected = "2EFGH\n3IJ";
|
||||||
try testing.expectEqualStrings(expected, contents);
|
try testing.expectEqualStrings(expected, contents);
|
||||||
@ -3173,7 +3273,7 @@ test "Screen: selectionString wide char" {
|
|||||||
var contents = try s.selectionString(alloc, .{
|
var contents = try s.selectionString(alloc, .{
|
||||||
.start = .{ .x = 0, .y = 0 },
|
.start = .{ .x = 0, .y = 0 },
|
||||||
.end = .{ .x = 3, .y = 0 },
|
.end = .{ .x = 3, .y = 0 },
|
||||||
});
|
}, true);
|
||||||
defer alloc.free(contents);
|
defer alloc.free(contents);
|
||||||
const expected = str;
|
const expected = str;
|
||||||
try testing.expectEqualStrings(expected, contents);
|
try testing.expectEqualStrings(expected, contents);
|
||||||
@ -3183,7 +3283,7 @@ test "Screen: selectionString wide char" {
|
|||||||
var contents = try s.selectionString(alloc, .{
|
var contents = try s.selectionString(alloc, .{
|
||||||
.start = .{ .x = 0, .y = 0 },
|
.start = .{ .x = 0, .y = 0 },
|
||||||
.end = .{ .x = 2, .y = 0 },
|
.end = .{ .x = 2, .y = 0 },
|
||||||
});
|
}, true);
|
||||||
defer alloc.free(contents);
|
defer alloc.free(contents);
|
||||||
const expected = str;
|
const expected = str;
|
||||||
try testing.expectEqualStrings(expected, contents);
|
try testing.expectEqualStrings(expected, contents);
|
||||||
@ -3193,7 +3293,7 @@ test "Screen: selectionString wide char" {
|
|||||||
var contents = try s.selectionString(alloc, .{
|
var contents = try s.selectionString(alloc, .{
|
||||||
.start = .{ .x = 3, .y = 0 },
|
.start = .{ .x = 3, .y = 0 },
|
||||||
.end = .{ .x = 3, .y = 0 },
|
.end = .{ .x = 3, .y = 0 },
|
||||||
});
|
}, true);
|
||||||
defer alloc.free(contents);
|
defer alloc.free(contents);
|
||||||
const expected = "⚡";
|
const expected = "⚡";
|
||||||
try testing.expectEqualStrings(expected, contents);
|
try testing.expectEqualStrings(expected, contents);
|
||||||
@ -3213,7 +3313,7 @@ test "Screen: selectionString wide char with header" {
|
|||||||
var contents = try s.selectionString(alloc, .{
|
var contents = try s.selectionString(alloc, .{
|
||||||
.start = .{ .x = 0, .y = 0 },
|
.start = .{ .x = 0, .y = 0 },
|
||||||
.end = .{ .x = 4, .y = 0 },
|
.end = .{ .x = 4, .y = 0 },
|
||||||
});
|
}, true);
|
||||||
defer alloc.free(contents);
|
defer alloc.free(contents);
|
||||||
const expected = str;
|
const expected = str;
|
||||||
try testing.expectEqualStrings(expected, contents);
|
try testing.expectEqualStrings(expected, contents);
|
||||||
|
Reference in New Issue
Block a user