mirror of
https://github.com/ghostty-org/ghostty.git
synced 2025-07-17 01:06:08 +03:00
Merge pull request #200 from mitchellh/screen-string
Write scrollback to file, output to tty ("write_scrollback_file" binding)
This commit is contained in:
@ -23,8 +23,8 @@ const Command = @This();
|
|||||||
|
|
||||||
const std = @import("std");
|
const std = @import("std");
|
||||||
const builtin = @import("builtin");
|
const builtin = @import("builtin");
|
||||||
const TempDir = @import("TempDir.zig");
|
|
||||||
const internal_os = @import("os/main.zig");
|
const internal_os = @import("os/main.zig");
|
||||||
|
const TempDir = internal_os.TempDir;
|
||||||
const mem = std.mem;
|
const mem = std.mem;
|
||||||
const os = std.os;
|
const os = std.os;
|
||||||
const debug = std.debug;
|
const debug = std.debug;
|
||||||
|
@ -1113,6 +1113,42 @@ pub fn keyCallback(
|
|||||||
try self.io_thread.wakeup.notify();
|
try self.io_thread.wakeup.notify();
|
||||||
},
|
},
|
||||||
|
|
||||||
|
.write_scrollback_file => {
|
||||||
|
// Create a temporary directory to store our scrollback.
|
||||||
|
var tmp_dir = try internal_os.TempDir.init();
|
||||||
|
errdefer tmp_dir.deinit();
|
||||||
|
|
||||||
|
// Open our scrollback file
|
||||||
|
var file = try tmp_dir.dir.createFile("scrollback", .{});
|
||||||
|
defer file.close();
|
||||||
|
|
||||||
|
// Write the scrollback contents. This requires a lock.
|
||||||
|
{
|
||||||
|
self.renderer_state.mutex.lock();
|
||||||
|
defer self.renderer_state.mutex.unlock();
|
||||||
|
|
||||||
|
const history_max = terminal.Screen.RowIndexTag.history.maxLen(
|
||||||
|
&self.io.terminal.screen,
|
||||||
|
);
|
||||||
|
|
||||||
|
try self.io.terminal.screen.dumpString(file.writer(), .{
|
||||||
|
.start = .{ .history = 0 },
|
||||||
|
.end = .{ .history = history_max -| 1 },
|
||||||
|
.unwrap = true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the final path
|
||||||
|
var path_buf: [std.fs.MAX_PATH_BYTES]u8 = undefined;
|
||||||
|
const path = try tmp_dir.dir.realpath("scrollback", &path_buf);
|
||||||
|
|
||||||
|
_ = self.io_thread.mailbox.push(try termio.Message.writeReq(
|
||||||
|
self.alloc,
|
||||||
|
path,
|
||||||
|
), .{ .forever = {} });
|
||||||
|
try self.io_thread.wakeup.notify();
|
||||||
|
},
|
||||||
|
|
||||||
.toggle_dev_mode => if (DevMode.enabled) {
|
.toggle_dev_mode => if (DevMode.enabled) {
|
||||||
DevMode.instance.visible = !DevMode.instance.visible;
|
DevMode.instance.visible = !DevMode.instance.visible;
|
||||||
try self.queueRender();
|
try self.queueRender();
|
||||||
|
@ -393,6 +393,12 @@ pub const Config = struct {
|
|||||||
.{ .toggle_dev_mode = {} },
|
.{ .toggle_dev_mode = {} },
|
||||||
);
|
);
|
||||||
|
|
||||||
|
try result.keybind.set.put(
|
||||||
|
alloc,
|
||||||
|
.{ .key = .j, .mods = ctrlOrSuper(.{ .shift = true }) },
|
||||||
|
.{ .write_scrollback_file = {} },
|
||||||
|
);
|
||||||
|
|
||||||
// Windowing
|
// Windowing
|
||||||
if (comptime !builtin.target.isDarwin()) {
|
if (comptime !builtin.target.isDarwin()) {
|
||||||
try result.keybind.set.put(
|
try result.keybind.set.put(
|
||||||
|
@ -174,6 +174,10 @@ pub const Action = union(enum) {
|
|||||||
/// is backwards.
|
/// is backwards.
|
||||||
jump_to_prompt: i16,
|
jump_to_prompt: i16,
|
||||||
|
|
||||||
|
/// Write the entire scrollback into a temporary file and write the
|
||||||
|
/// path to the file to the tty.
|
||||||
|
write_scrollback_file: void,
|
||||||
|
|
||||||
/// Dev mode
|
/// Dev mode
|
||||||
toggle_dev_mode: void,
|
toggle_dev_mode: void,
|
||||||
|
|
||||||
|
@ -181,7 +181,6 @@ pub const GlobalState = struct {
|
|||||||
test {
|
test {
|
||||||
_ = @import("Pty.zig");
|
_ = @import("Pty.zig");
|
||||||
_ = @import("Command.zig");
|
_ = @import("Command.zig");
|
||||||
_ = @import("TempDir.zig");
|
|
||||||
_ = @import("font/main.zig");
|
_ = @import("font/main.zig");
|
||||||
_ = @import("renderer.zig");
|
_ = @import("renderer.zig");
|
||||||
_ = @import("termio.zig");
|
_ = @import("termio.zig");
|
||||||
|
@ -6,6 +6,7 @@ const std = @import("std");
|
|||||||
const builtin = @import("builtin");
|
const builtin = @import("builtin");
|
||||||
const testing = std.testing;
|
const testing = std.testing;
|
||||||
const Dir = std.fs.Dir;
|
const Dir = std.fs.Dir;
|
||||||
|
const internal_os = @import("main.zig");
|
||||||
|
|
||||||
const log = std.log.scoped(.tempdir);
|
const log = std.log.scoped(.tempdir);
|
||||||
|
|
||||||
@ -28,8 +29,11 @@ pub fn init() !TempDir {
|
|||||||
var tmp_path_buf: [TMP_PATH_LEN:0]u8 = undefined;
|
var tmp_path_buf: [TMP_PATH_LEN:0]u8 = undefined;
|
||||||
var rand_buf: [RANDOM_BYTES]u8 = undefined;
|
var rand_buf: [RANDOM_BYTES]u8 = undefined;
|
||||||
|
|
||||||
// TODO: use the real temp dir not cwd
|
const dir = dir: {
|
||||||
const dir = std.fs.cwd();
|
const cwd = std.fs.cwd();
|
||||||
|
const tmp_dir = internal_os.tmpDir() orelse break :dir cwd;
|
||||||
|
break :dir try cwd.openDir(tmp_dir, .{});
|
||||||
|
};
|
||||||
|
|
||||||
// We now loop forever until we can find a directory that we can create.
|
// We now loop forever until we can find a directory that we can create.
|
||||||
while (true) {
|
while (true) {
|
@ -50,3 +50,10 @@ pub fn fixMaxFiles() void {
|
|||||||
|
|
||||||
log.debug("file handle limit raised value={}", .{lim.cur});
|
log.debug("file handle limit raised value={}", .{lim.cur});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Return the recommended path for temporary files.
|
||||||
|
pub fn tmpDir() ?[]const u8 {
|
||||||
|
if (std.os.getenv("TMPDIR")) |v| return v;
|
||||||
|
if (std.os.getenv("TMP")) |v| return v;
|
||||||
|
return "/tmp";
|
||||||
|
}
|
||||||
|
@ -6,3 +6,4 @@ pub usingnamespace @import("flatpak.zig");
|
|||||||
pub usingnamespace @import("locale.zig");
|
pub usingnamespace @import("locale.zig");
|
||||||
pub usingnamespace @import("macos_version.zig");
|
pub usingnamespace @import("macos_version.zig");
|
||||||
pub usingnamespace @import("mouse.zig");
|
pub usingnamespace @import("mouse.zig");
|
||||||
|
pub const TempDir = @import("TempDir.zig");
|
||||||
|
@ -2536,37 +2536,113 @@ pub fn testWriteString(self: *Screen, text: []const u8) !void {
|
|||||||
self.cursor.y = y;
|
self.cursor.y = y;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Options for dumping the screen to a string.
|
||||||
|
pub const Dump = struct {
|
||||||
|
/// The start and end rows. These don't have to be in order, the dump
|
||||||
|
/// function will automatically sort them.
|
||||||
|
start: RowIndex,
|
||||||
|
end: RowIndex,
|
||||||
|
|
||||||
|
/// If true, this will unwrap soft-wrapped lines into a single line.
|
||||||
|
unwrap: bool = true,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Dump the screen to a string. The writer given should be buffered;
|
||||||
|
/// this function does not attempt to efficiently write and generally writes
|
||||||
|
/// one byte at a time.
|
||||||
|
///
|
||||||
|
/// TODO: look at selectionString implementation for more efficiency
|
||||||
|
/// TODO: change selectionString to use this too after above todo
|
||||||
|
pub fn dumpString(self: *Screen, writer: anytype, opts: Dump) !void {
|
||||||
|
const start_screen = opts.start.toScreen(self);
|
||||||
|
const end_screen = opts.end.toScreen(self);
|
||||||
|
|
||||||
|
// If we have no rows in our screen, do nothing.
|
||||||
|
const rows_written = self.rowsWritten();
|
||||||
|
if (rows_written == 0) return;
|
||||||
|
|
||||||
|
// Get the actual top and bottom y values. This handles situations
|
||||||
|
// where start/end are backwards.
|
||||||
|
const y_top = @min(start_screen.screen, end_screen.screen);
|
||||||
|
const y_bottom = @min(
|
||||||
|
@max(start_screen.screen, end_screen.screen),
|
||||||
|
rows_written - 1,
|
||||||
|
);
|
||||||
|
|
||||||
|
// This keeps track of the number of blank rows we see. We don't want
|
||||||
|
// to output blank rows unless they're followed by a non-blank row.
|
||||||
|
var blank_rows: usize = 0;
|
||||||
|
|
||||||
|
// Iterate through the rows
|
||||||
|
var y: usize = y_top;
|
||||||
|
while (y <= y_bottom) : (y += 1) {
|
||||||
|
const row = self.getRow(.{ .screen = y });
|
||||||
|
|
||||||
|
// Handle blank rows
|
||||||
|
if (row.isEmpty()) {
|
||||||
|
// Blank rows should never have wrap set. A blank row doesn't
|
||||||
|
// include explicit spaces so there should never be a scenario
|
||||||
|
// it's wrapped.
|
||||||
|
assert(!row.header().flags.wrap);
|
||||||
|
blank_rows += 1;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (blank_rows > 0) {
|
||||||
|
for (0..blank_rows) |_| try writer.writeByte('\n');
|
||||||
|
blank_rows = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!row.header().flags.wrap) {
|
||||||
|
// If we're not wrapped, we always add a newline.
|
||||||
|
blank_rows += 1;
|
||||||
|
} else if (!opts.unwrap) {
|
||||||
|
// If we are wrapped, we only add a new line if we're unwrapping
|
||||||
|
// soft-wrapped lines.
|
||||||
|
blank_rows += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Output each of the cells
|
||||||
|
var cells = row.cellIterator();
|
||||||
|
var spacers: usize = 0;
|
||||||
|
while (cells.next()) |cell| {
|
||||||
|
// Skip spacers
|
||||||
|
if (cell.attrs.wide_spacer_head or cell.attrs.wide_spacer_tail) continue;
|
||||||
|
|
||||||
|
// If we have a zero value, then we accumulate a counter. We
|
||||||
|
// only want to turn zero values into spaces if we have a non-zero
|
||||||
|
// char sometime later.
|
||||||
|
if (cell.char == 0) {
|
||||||
|
spacers += 1;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (spacers > 0) {
|
||||||
|
for (0..spacers) |_| try writer.writeByte(' ');
|
||||||
|
spacers = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
const codepoint: u21 = @intCast(cell.char);
|
||||||
|
try writer.print("{u}", .{codepoint});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Turns the screen into a string. Different regions of the screen can
|
/// Turns the screen into a string. Different regions of the screen can
|
||||||
/// be selected using the "tag", i.e. if you want to output the viewport,
|
/// be selected using the "tag", i.e. if you want to output the viewport,
|
||||||
/// the scrollback, the full screen, etc.
|
/// the scrollback, the full screen, etc.
|
||||||
///
|
///
|
||||||
/// This is only useful for testing.
|
/// This is only useful for testing.
|
||||||
pub fn testString(self: *Screen, alloc: Allocator, tag: RowIndexTag) ![]const u8 {
|
pub fn testString(self: *Screen, alloc: Allocator, tag: RowIndexTag) ![]const u8 {
|
||||||
const buf = try alloc.alloc(u8, self.storage.len() * 4);
|
var builder = std.ArrayList(u8).init(alloc);
|
||||||
|
defer builder.deinit();
|
||||||
|
try self.dumpString(builder.writer(), .{
|
||||||
|
.start = tag.index(0),
|
||||||
|
.end = tag.index(tag.maxLen(self) - 1),
|
||||||
|
|
||||||
var i: usize = 0;
|
// historically our testString wants to view the screen as-is without
|
||||||
var y: usize = 0;
|
// unwrapping soft-wrapped lines so turn this off.
|
||||||
var rows = self.rowIterator(tag);
|
.unwrap = false,
|
||||||
while (rows.next()) |row| {
|
});
|
||||||
defer y += 1;
|
return try builder.toOwnedSlice();
|
||||||
|
|
||||||
if (y > 0) {
|
|
||||||
buf[i] = '\n';
|
|
||||||
i += 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
var cells = row.cellIterator();
|
|
||||||
while (cells.next()) |cell| {
|
|
||||||
// TODO: handle character after null
|
|
||||||
if (cell.char > 0) {
|
|
||||||
i += try std.unicode.utf8Encode(@intCast(cell.char), buf[i..]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Never render the final newline
|
|
||||||
const str = std.mem.trimRight(u8, buf[0..i], "\n");
|
|
||||||
return try alloc.realloc(buf, str.len);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
test "Row: isEmpty with no data" {
|
test "Row: isEmpty with no data" {
|
||||||
@ -3280,7 +3356,7 @@ test "Screen: clone one line viewport" {
|
|||||||
var s2 = try s.clone(alloc, .{ .viewport = 0 }, .{ .viewport = 0 });
|
var s2 = try s.clone(alloc, .{ .viewport = 0 }, .{ .viewport = 0 });
|
||||||
defer s2.deinit();
|
defer s2.deinit();
|
||||||
|
|
||||||
// Test our contents rotated
|
// Test our contents
|
||||||
var contents = try s2.testString(alloc, .viewport);
|
var contents = try s2.testString(alloc, .viewport);
|
||||||
defer alloc.free(contents);
|
defer alloc.free(contents);
|
||||||
try testing.expectEqualStrings("1ABC", contents);
|
try testing.expectEqualStrings("1ABC", contents);
|
||||||
|
@ -398,7 +398,7 @@ fn clearPromptForResize(self: *Terminal) void {
|
|||||||
/// encoded as "\n". This omits any formatting such as fg/bg.
|
/// encoded as "\n". This omits any formatting such as fg/bg.
|
||||||
///
|
///
|
||||||
/// The caller must free the string.
|
/// The caller must free the string.
|
||||||
pub fn plainString(self: *Terminal, alloc: Allocator) ![]const u8 {
|
fn plainString(self: *Terminal, alloc: Allocator) ![]const u8 {
|
||||||
return try self.screen.testString(alloc, .viewport);
|
return try self.screen.testString(alloc, .viewport);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1634,6 +1634,7 @@ test "Terminal: print writes to bottom if scrolled" {
|
|||||||
|
|
||||||
// Basic grid writing
|
// Basic grid writing
|
||||||
for ("hello") |c| try t.print(c);
|
for ("hello") |c| try t.print(c);
|
||||||
|
t.setCursorPos(0, 0);
|
||||||
|
|
||||||
// Make newlines so we create scrollback
|
// Make newlines so we create scrollback
|
||||||
// 3 pushes hello off the screen
|
// 3 pushes hello off the screen
|
||||||
@ -2192,6 +2193,7 @@ test "Terminal: index from the bottom" {
|
|||||||
|
|
||||||
t.setCursorPos(5, 1);
|
t.setCursorPos(5, 1);
|
||||||
try t.print('A');
|
try t.print('A');
|
||||||
|
t.cursorLeft(1); // undo moving right from 'A'
|
||||||
try t.index();
|
try t.index();
|
||||||
|
|
||||||
try t.print('B');
|
try t.print('B');
|
||||||
|
Reference in New Issue
Block a user