mirror of
https://github.com/ghostty-org/ghostty.git
synced 2025-08-02 14:57:31 +03:00
Copy Selection to Clipboard
This commit is contained in:
@ -448,28 +448,58 @@ fn keyCallback(
|
|||||||
|
|
||||||
_ = scancode;
|
_ = scancode;
|
||||||
|
|
||||||
// Paste
|
if (action == .press and mods.super) {
|
||||||
if (action == .press and mods.super and key == .v) {
|
switch (key) {
|
||||||
const win = window.getUserPointer(Window) orelse return;
|
// Copy
|
||||||
|
.c => {
|
||||||
|
const win = window.getUserPointer(Window) orelse return;
|
||||||
|
|
||||||
// Ignore this character for writing
|
// Ignore this character for writing
|
||||||
win.ignore_char = true;
|
win.ignore_char = true;
|
||||||
|
|
||||||
const data = glfw.getClipboardString() catch |err| {
|
// If we have a selection, copy it.
|
||||||
log.warn("error reading clipboard: {}", .{err});
|
if (win.terminal.selection) |sel| {
|
||||||
return;
|
var buf = win.terminal.screen.selectionString(win.alloc, sel) catch |err| {
|
||||||
};
|
log.err("error reading selection string err={}", .{err});
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
defer win.alloc.free(buf);
|
||||||
|
|
||||||
if (data.len > 0) {
|
glfw.setClipboardString(buf) catch |err| {
|
||||||
if (win.bracketed_paste) win.queueWrite("\x1B[200~") catch |err|
|
log.err("error setting clipboard string err={}", .{err});
|
||||||
log.err("error queueing write in keyCallback err={}", .{err});
|
return;
|
||||||
win.queueWrite(data) catch |err|
|
};
|
||||||
log.warn("error pasting clipboard: {}", .{err});
|
}
|
||||||
if (win.bracketed_paste) win.queueWrite("\x1B[201~") catch |err|
|
|
||||||
log.err("error queueing write in keyCallback err={}", .{err});
|
return;
|
||||||
|
},
|
||||||
|
|
||||||
|
// Paste
|
||||||
|
.v => {
|
||||||
|
const win = window.getUserPointer(Window) orelse return;
|
||||||
|
|
||||||
|
// Ignore this character for writing
|
||||||
|
win.ignore_char = true;
|
||||||
|
|
||||||
|
const data = glfw.getClipboardString() catch |err| {
|
||||||
|
log.warn("error reading clipboard: {}", .{err});
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
if (data.len > 0) {
|
||||||
|
if (win.bracketed_paste) win.queueWrite("\x1B[200~") catch |err|
|
||||||
|
log.err("error queueing write in keyCallback err={}", .{err});
|
||||||
|
win.queueWrite(data) catch |err|
|
||||||
|
log.warn("error pasting clipboard: {}", .{err});
|
||||||
|
if (win.bracketed_paste) win.queueWrite("\x1B[201~") catch |err|
|
||||||
|
log.err("error queueing write in keyCallback err={}", .{err});
|
||||||
|
}
|
||||||
|
|
||||||
|
return;
|
||||||
|
},
|
||||||
|
|
||||||
|
else => {},
|
||||||
}
|
}
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
//log.info("KEY {} {} {} {}", .{ key, scancode, mods, action });
|
//log.info("KEY {} {} {} {}", .{ key, scancode, mods, action });
|
||||||
|
@ -30,6 +30,7 @@ const Allocator = std.mem.Allocator;
|
|||||||
const color = @import("color.zig");
|
const color = @import("color.zig");
|
||||||
const point = @import("point.zig");
|
const point = @import("point.zig");
|
||||||
const Point = point.Point;
|
const Point = point.Point;
|
||||||
|
const Selection = @import("Selection.zig");
|
||||||
|
|
||||||
const log = std.log.scoped(.screen);
|
const log = std.log.scoped(.screen);
|
||||||
|
|
||||||
@ -172,7 +173,7 @@ pub fn getVisible(self: Screen) []Cell {
|
|||||||
/// Get a single row in the active area by index (0-indexed).
|
/// Get a single row in the active area by index (0-indexed).
|
||||||
pub fn getRow(self: Screen, idx: usize) Row {
|
pub fn getRow(self: Screen, idx: usize) Row {
|
||||||
// Get the index of the first byte of the the row at index.
|
// Get the index of the first byte of the the row at index.
|
||||||
const real_idx = self.rowIndex(idx);
|
const real_idx = self.viewportRowIndex(idx);
|
||||||
|
|
||||||
// The storage is sliced to return exactly the number of columns.
|
// The storage is sliced to return exactly the number of columns.
|
||||||
return self.storage[real_idx .. real_idx + self.cols];
|
return self.storage[real_idx .. real_idx + self.cols];
|
||||||
@ -182,19 +183,31 @@ pub fn getRow(self: Screen, idx: usize) Row {
|
|||||||
pub fn getCell(self: Screen, row: usize, col: usize) *Cell {
|
pub fn getCell(self: Screen, row: usize, col: usize) *Cell {
|
||||||
assert(row < self.rows);
|
assert(row < self.rows);
|
||||||
assert(col < self.cols);
|
assert(col < self.cols);
|
||||||
const row_idx = self.rowIndex(row);
|
const row_idx = self.viewportRowIndex(row);
|
||||||
return &self.storage[row_idx + col];
|
return &self.storage[row_idx + col];
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns the index for the given row (0-indexed) into the underlying
|
/// Returns the index for the given row (0-indexed) into the underlying
|
||||||
/// storage array.
|
/// storage array. The row is 0-indexed from the top of the viewport.
|
||||||
fn rowIndex(self: Screen, idx: usize) usize {
|
fn viewportRowIndex(self: Screen, idx: usize) usize {
|
||||||
assert(idx < self.rows);
|
assert(idx < self.rows);
|
||||||
const val = (self.top + self.visible_offset + idx) * self.cols;
|
return self.rowIndex(self.visible_offset + idx);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the index for the given row (0-indexed) into the underlying
|
||||||
|
/// storage array. The row is 0-indexed from the top of the screen.
|
||||||
|
fn rowIndex(self: Screen, y: usize) usize {
|
||||||
|
assert(y < self.totalRows());
|
||||||
|
const val = (self.top + y) * self.cols;
|
||||||
if (val < self.storage.len) return val;
|
if (val < self.storage.len) return val;
|
||||||
return val - self.storage.len;
|
return val - self.storage.len;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Returns the total number of rows in the screen.
|
||||||
|
inline fn totalRows(self: Screen) usize {
|
||||||
|
return self.storage.len / self.cols;
|
||||||
|
}
|
||||||
|
|
||||||
/// Scroll behaviors for the scroll function.
|
/// Scroll behaviors for the scroll function.
|
||||||
pub const Scroll = union(enum) {
|
pub const Scroll = union(enum) {
|
||||||
/// Scroll to the top of the scroll buffer. The first line of the
|
/// Scroll to the top of the scroll buffer. The first line of the
|
||||||
@ -380,13 +393,124 @@ pub fn resize(self: *Screen, alloc: Allocator, rows: usize, cols: usize) !void {
|
|||||||
|
|
||||||
// If we grew rows, then set the remaining data to zero.
|
// If we grew rows, then set the remaining data to zero.
|
||||||
if (rows > old.rows) {
|
if (rows > old.rows) {
|
||||||
std.mem.set(Cell, self.storage[self.rowIndex(old.rows)..], .{ .char = 0 });
|
std.mem.set(Cell, self.storage[self.viewportRowIndex(old.rows)..], .{ .char = 0 });
|
||||||
}
|
}
|
||||||
|
|
||||||
// Free the old data
|
// Free the old data
|
||||||
alloc.free(old.storage);
|
alloc.free(old.storage);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Returns the raw text associated with a selection. This will unwrap
|
||||||
|
/// soft-wrapped edges. The returned slice is owned by the caller.
|
||||||
|
pub fn selectionString(self: Screen, alloc: Allocator, sel: Selection) ![:0]const u8 {
|
||||||
|
// Get the slices for the string
|
||||||
|
const slices = self.selectionSlices(sel);
|
||||||
|
|
||||||
|
// We can now know how much space we'll need to store the string. We loop
|
||||||
|
// over and UTF8-encode and calculate the exact size required. We will be
|
||||||
|
// off here by at most "newlines" values in the worst case that every
|
||||||
|
// single line is soft-wrapped.
|
||||||
|
const newlines = @divFloor(slices.top.len + slices.bot.len, self.cols) + 1;
|
||||||
|
const chars = chars: {
|
||||||
|
var count: usize = 0;
|
||||||
|
const arr = [_][]Cell{ slices.top, slices.bot };
|
||||||
|
for (arr) |slice| {
|
||||||
|
for (slice) |cell| {
|
||||||
|
var buf: [4]u8 = undefined;
|
||||||
|
const char = if (cell.char > 0) cell.char else ' ';
|
||||||
|
count += try std.unicode.utf8Encode(@intCast(u21, char), &buf);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
break :chars count;
|
||||||
|
};
|
||||||
|
const buf = try alloc.alloc(u8, chars + newlines + 1);
|
||||||
|
errdefer alloc.free(buf);
|
||||||
|
|
||||||
|
var i: usize = 0;
|
||||||
|
for (slices.top) |cell, idx| {
|
||||||
|
// If our index cleanly divides into the col count then we're
|
||||||
|
// at a newline and we add it.
|
||||||
|
if (idx > 0 and
|
||||||
|
@mod(idx + slices.top_offset, self.cols) == 0 and
|
||||||
|
slices.top[idx - 1].attrs.wrap == 0)
|
||||||
|
{
|
||||||
|
buf[i] = '\n';
|
||||||
|
i += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
const char = if (cell.char > 0) cell.char else ' ';
|
||||||
|
i += try std.unicode.utf8Encode(@intCast(u21, char), buf[i..]);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (slices.bot) |cell, idx| {
|
||||||
|
// We don't use "top_offset" here because the bot by definition
|
||||||
|
// is never offset, it always starts at index 0 so we can just check
|
||||||
|
// the index directly.
|
||||||
|
if (@mod(idx, self.cols) == 0) {
|
||||||
|
// Determine if we soft-wrapped. For the bottom slice this is
|
||||||
|
// a bit unique because if we're at idx 0, we actually need to
|
||||||
|
// check the end of the top.
|
||||||
|
const wrapped = if (idx > 0)
|
||||||
|
slices.bot[idx - 1].attrs.wrap == 1
|
||||||
|
else
|
||||||
|
slices.top[slices.top.len - 1].attrs.wrap == 1;
|
||||||
|
|
||||||
|
if (!wrapped) {
|
||||||
|
buf[i] = '\n';
|
||||||
|
i += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const char = if (cell.char > 0) cell.char else ' ';
|
||||||
|
i += try std.unicode.utf8Encode(@intCast(u21, char), buf[i..]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add null termination
|
||||||
|
buf[i] = 0;
|
||||||
|
return buf[0..i :0];
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 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 {
|
||||||
|
// 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,
|
||||||
|
top: []Cell,
|
||||||
|
bot: []Cell,
|
||||||
|
} {
|
||||||
|
// TODO: test
|
||||||
|
assert(sel.start.y < self.totalRows());
|
||||||
|
assert(sel.end.y < self.totalRows());
|
||||||
|
assert(sel.start.x < self.cols);
|
||||||
|
assert(sel.end.x < self.cols);
|
||||||
|
|
||||||
|
// Get the true "top" and "bottom"
|
||||||
|
const sel_top = sel.topLeft();
|
||||||
|
const sel_bot = sel.bottomRight();
|
||||||
|
const top = self.rowIndex(sel_top.y);
|
||||||
|
const bot = self.rowIndex(sel_bot.y);
|
||||||
|
|
||||||
|
// The bottom and top are available in one contiguous slice.
|
||||||
|
if (bot >= top) {
|
||||||
|
return .{
|
||||||
|
.top_offset = sel_top.x,
|
||||||
|
.top = self.storage[top + sel_top.x .. bot + sel_bot.x + 1],
|
||||||
|
.bot = self.storage[0..0], // just so its a valid slice, but zero length
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// The bottom and top are split into two slices, so we slice to the
|
||||||
|
// bottom of the storage, then from the top.
|
||||||
|
return .{
|
||||||
|
.top_offset = sel_top.x,
|
||||||
|
.top = self.storage[top + sel_top.x .. self.storage.len],
|
||||||
|
.bot = self.storage[0 .. bot + sel_bot.x + 1],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
/// Turns the screen into a string.
|
/// Turns the screen into a string.
|
||||||
pub fn testString(self: Screen, alloc: Allocator) ![]const u8 {
|
pub fn testString(self: Screen, alloc: Allocator) ![]const u8 {
|
||||||
const buf = try alloc.alloc(u8, self.storage.len + self.rows);
|
const buf = try alloc.alloc(u8, self.storage.len + self.rows);
|
||||||
@ -416,12 +540,14 @@ pub fn testString(self: Screen, alloc: Allocator) ![]const u8 {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Writes a basic string into the screen for testing. Newlines (\n) separate
|
/// Writes a basic string into the screen for testing. Newlines (\n) separate
|
||||||
/// each row.
|
/// each row. If a line is longer than the available columns, soft-wrapping
|
||||||
|
/// will occur.
|
||||||
fn testWriteString(self: *Screen, text: []const u8) void {
|
fn testWriteString(self: *Screen, text: []const u8) void {
|
||||||
var y: usize = 0;
|
var y: usize = 0;
|
||||||
var x: usize = 0;
|
var x: usize = 0;
|
||||||
var row = self.getRow(y);
|
var row = self.getRow(y);
|
||||||
for (text) |c| {
|
for (text) |c| {
|
||||||
|
// Explicit newline forces a new row
|
||||||
if (c == '\n') {
|
if (c == '\n') {
|
||||||
y += 1;
|
y += 1;
|
||||||
x = 0;
|
x = 0;
|
||||||
@ -429,7 +555,14 @@ fn testWriteString(self: *Screen, text: []const u8) void {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
assert(x < self.cols);
|
// If we're writing past the end, we need to soft wrap.
|
||||||
|
if (x == self.cols) {
|
||||||
|
row[x - 1].attrs.wrap = 1;
|
||||||
|
y += 1;
|
||||||
|
x = 0;
|
||||||
|
row = self.getRow(y);
|
||||||
|
}
|
||||||
|
|
||||||
row[x].char = @intCast(u32, c);
|
row[x].char = @intCast(u32, c);
|
||||||
x += 1;
|
x += 1;
|
||||||
}
|
}
|
||||||
@ -478,9 +611,9 @@ test "Screen: scrolling" {
|
|||||||
try testing.expect(s.viewportIsBottom());
|
try testing.expect(s.viewportIsBottom());
|
||||||
|
|
||||||
// Test our row index
|
// Test our row index
|
||||||
try testing.expectEqual(@as(usize, 5), s.rowIndex(0));
|
try testing.expectEqual(@as(usize, 5), s.viewportRowIndex(0));
|
||||||
try testing.expectEqual(@as(usize, 10), s.rowIndex(1));
|
try testing.expectEqual(@as(usize, 10), s.viewportRowIndex(1));
|
||||||
try testing.expectEqual(@as(usize, 0), s.rowIndex(2));
|
try testing.expectEqual(@as(usize, 0), s.viewportRowIndex(2));
|
||||||
|
|
||||||
{
|
{
|
||||||
// Test our contents rotated
|
// Test our contents rotated
|
||||||
@ -516,9 +649,9 @@ test "Screen: scrolling" {
|
|||||||
// try testing.expect(s.viewportIsBottom());
|
// try testing.expect(s.viewportIsBottom());
|
||||||
//
|
//
|
||||||
// // Test our row index
|
// // Test our row index
|
||||||
// try testing.expectEqual(@as(usize, 5), s.rowIndex(0));
|
// try testing.expectEqual(@as(usize, 5), s.viewportRowIndex(0));
|
||||||
// try testing.expectEqual(@as(usize, 10), s.rowIndex(1));
|
// try testing.expectEqual(@as(usize, 10), s.viewportRowIndex(1));
|
||||||
// try testing.expectEqual(@as(usize, 15), s.rowIndex(2));
|
// try testing.expectEqual(@as(usize, 15), s.viewportRowIndex(2));
|
||||||
// }
|
// }
|
||||||
|
|
||||||
test "Screen: scroll down from 0" {
|
test "Screen: scroll down from 0" {
|
||||||
@ -549,9 +682,9 @@ test "Screen: scrollback" {
|
|||||||
s.scroll(.{ .delta = 1 });
|
s.scroll(.{ .delta = 1 });
|
||||||
|
|
||||||
// Test our row index
|
// Test our row index
|
||||||
try testing.expectEqual(@as(usize, 5), s.rowIndex(0));
|
try testing.expectEqual(@as(usize, 5), s.viewportRowIndex(0));
|
||||||
try testing.expectEqual(@as(usize, 10), s.rowIndex(1));
|
try testing.expectEqual(@as(usize, 10), s.viewportRowIndex(1));
|
||||||
try testing.expectEqual(@as(usize, 15), s.rowIndex(2));
|
try testing.expectEqual(@as(usize, 15), s.viewportRowIndex(2));
|
||||||
|
|
||||||
{
|
{
|
||||||
// Test our contents rotated
|
// Test our contents rotated
|
||||||
@ -726,3 +859,70 @@ test "Screen: resize less cols" {
|
|||||||
try testing.expectEqualStrings(expected, contents);
|
try testing.expectEqualStrings(expected, contents);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
test "Screen: selectionString" {
|
||||||
|
const testing = std.testing;
|
||||||
|
const alloc = testing.allocator;
|
||||||
|
|
||||||
|
var s = try init(alloc, 3, 5, 0);
|
||||||
|
defer s.deinit(alloc);
|
||||||
|
const str = "1ABCD\n2EFGH\n3IJKL";
|
||||||
|
s.testWriteString(str);
|
||||||
|
|
||||||
|
{
|
||||||
|
var contents = try s.selectionString(alloc, .{
|
||||||
|
.start = .{ .x = 0, .y = 1 },
|
||||||
|
.end = .{ .x = 2, .y = 2 },
|
||||||
|
});
|
||||||
|
defer alloc.free(contents);
|
||||||
|
const expected = "2EFGH\n3IJ";
|
||||||
|
try testing.expectEqualStrings(expected, contents);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
test "Screen: selectionString soft wrap" {
|
||||||
|
const testing = std.testing;
|
||||||
|
const alloc = testing.allocator;
|
||||||
|
|
||||||
|
var s = try init(alloc, 3, 5, 0);
|
||||||
|
defer s.deinit(alloc);
|
||||||
|
const str = "1ABCD2EFGH3IJKL";
|
||||||
|
s.testWriteString(str);
|
||||||
|
|
||||||
|
{
|
||||||
|
var contents = try s.selectionString(alloc, .{
|
||||||
|
.start = .{ .x = 0, .y = 1 },
|
||||||
|
.end = .{ .x = 2, .y = 2 },
|
||||||
|
});
|
||||||
|
defer alloc.free(contents);
|
||||||
|
const expected = "2EFGH3IJ";
|
||||||
|
try testing.expectEqualStrings(expected, contents);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
test "Screen: selectionString wrap around" {
|
||||||
|
const testing = std.testing;
|
||||||
|
const alloc = testing.allocator;
|
||||||
|
|
||||||
|
var s = try init(alloc, 3, 5, 0);
|
||||||
|
defer s.deinit(alloc);
|
||||||
|
s.testWriteString("1ABCD\n2EFGH\n3IJKL");
|
||||||
|
try testing.expect(s.viewportIsBottom());
|
||||||
|
|
||||||
|
// Scroll down, should still be bottom, but should wrap because
|
||||||
|
// we're out of space.
|
||||||
|
s.scroll(.{ .delta = 1 });
|
||||||
|
try testing.expect(s.viewportIsBottom());
|
||||||
|
try testing.expectEqual(@as(usize, 0), s.viewportRowIndex(2));
|
||||||
|
s.testWriteString("1ABCD\n2EFGH\n3IJKL");
|
||||||
|
|
||||||
|
{
|
||||||
|
var contents = try s.selectionString(alloc, .{
|
||||||
|
.start = .{ .x = 0, .y = 1 },
|
||||||
|
.end = .{ .x = 2, .y = 2 },
|
||||||
|
});
|
||||||
|
defer alloc.free(contents);
|
||||||
|
const expected = "2EFGH\n3IJ";
|
||||||
|
try testing.expectEqualStrings(expected, contents);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Reference in New Issue
Block a user