diff --git a/src/Command.zig b/src/Command.zig index 2f8b871e8d..7bf64566e4 100644 --- a/src/Command.zig +++ b/src/Command.zig @@ -23,8 +23,8 @@ const Command = @This(); const std = @import("std"); const builtin = @import("builtin"); -const TempDir = @import("TempDir.zig"); const internal_os = @import("os/main.zig"); +const TempDir = internal_os.TempDir; const mem = std.mem; const os = std.os; const debug = std.debug; diff --git a/src/Surface.zig b/src/Surface.zig index 3f0e0b25b7..d5a994556b 100644 --- a/src/Surface.zig +++ b/src/Surface.zig @@ -1113,6 +1113,42 @@ pub fn keyCallback( 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) { DevMode.instance.visible = !DevMode.instance.visible; try self.queueRender(); diff --git a/src/config.zig b/src/config.zig index feeea8cd28..0a7f9f5338 100644 --- a/src/config.zig +++ b/src/config.zig @@ -393,6 +393,12 @@ pub const Config = struct { .{ .toggle_dev_mode = {} }, ); + try result.keybind.set.put( + alloc, + .{ .key = .j, .mods = ctrlOrSuper(.{ .shift = true }) }, + .{ .write_scrollback_file = {} }, + ); + // Windowing if (comptime !builtin.target.isDarwin()) { try result.keybind.set.put( diff --git a/src/input/Binding.zig b/src/input/Binding.zig index 010992960b..f57ecaae0f 100644 --- a/src/input/Binding.zig +++ b/src/input/Binding.zig @@ -174,6 +174,10 @@ pub const Action = union(enum) { /// is backwards. 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 toggle_dev_mode: void, diff --git a/src/main.zig b/src/main.zig index f9160a2824..d93683d062 100644 --- a/src/main.zig +++ b/src/main.zig @@ -181,7 +181,6 @@ pub const GlobalState = struct { test { _ = @import("Pty.zig"); _ = @import("Command.zig"); - _ = @import("TempDir.zig"); _ = @import("font/main.zig"); _ = @import("renderer.zig"); _ = @import("termio.zig"); diff --git a/src/TempDir.zig b/src/os/TempDir.zig similarity index 92% rename from src/TempDir.zig rename to src/os/TempDir.zig index ceb586aa09..0422a2cef5 100644 --- a/src/TempDir.zig +++ b/src/os/TempDir.zig @@ -6,6 +6,7 @@ const std = @import("std"); const builtin = @import("builtin"); const testing = std.testing; const Dir = std.fs.Dir; +const internal_os = @import("main.zig"); 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 rand_buf: [RANDOM_BYTES]u8 = undefined; - // TODO: use the real temp dir not cwd - const dir = std.fs.cwd(); + const dir = dir: { + 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. while (true) { diff --git a/src/os/file.zig b/src/os/file.zig index ae4f5907d4..7e54909b36 100644 --- a/src/os/file.zig +++ b/src/os/file.zig @@ -50,3 +50,10 @@ pub fn fixMaxFiles() void { 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"; +} diff --git a/src/os/main.zig b/src/os/main.zig index 070e7be1dd..7479c30897 100644 --- a/src/os/main.zig +++ b/src/os/main.zig @@ -6,3 +6,4 @@ pub usingnamespace @import("flatpak.zig"); pub usingnamespace @import("locale.zig"); pub usingnamespace @import("macos_version.zig"); pub usingnamespace @import("mouse.zig"); +pub const TempDir = @import("TempDir.zig"); diff --git a/src/terminal/Screen.zig b/src/terminal/Screen.zig index 2177a47fd6..0345f345f3 100644 --- a/src/terminal/Screen.zig +++ b/src/terminal/Screen.zig @@ -2536,37 +2536,113 @@ pub fn testWriteString(self: *Screen, text: []const u8) !void { self.cursor.y = y; } -/// 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, -/// the scrollback, the full screen, etc. +/// 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. /// -/// This is only useful for testing. -pub fn testString(self: *Screen, alloc: Allocator, tag: RowIndexTag) ![]const u8 { - const buf = try alloc.alloc(u8, self.storage.len() * 4); +/// 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); - var i: usize = 0; - var y: usize = 0; - var rows = self.rowIterator(tag); - while (rows.next()) |row| { - defer y += 1; + // 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 (y > 0) { - buf[i] = '\n'; - i += 1; + 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| { - // TODO: handle character after null - if (cell.char > 0) { - i += try std.unicode.utf8Encode(@intCast(cell.char), buf[i..]); + // 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}); } } +} - // Never render the final newline - const str = std.mem.trimRight(u8, buf[0..i], "\n"); - return try alloc.realloc(buf, str.len); +/// 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, +/// the scrollback, the full screen, etc. +/// +/// This is only useful for testing. +pub fn testString(self: *Screen, alloc: Allocator, tag: RowIndexTag) ![]const u8 { + 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), + + // historically our testString wants to view the screen as-is without + // unwrapping soft-wrapped lines so turn this off. + .unwrap = false, + }); + return try builder.toOwnedSlice(); } 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 }); defer s2.deinit(); - // Test our contents rotated + // Test our contents var contents = try s2.testString(alloc, .viewport); defer alloc.free(contents); try testing.expectEqualStrings("1ABC", contents); diff --git a/src/terminal/Terminal.zig b/src/terminal/Terminal.zig index 9315aab6b7..0c1e121ce0 100644 --- a/src/terminal/Terminal.zig +++ b/src/terminal/Terminal.zig @@ -398,7 +398,7 @@ fn clearPromptForResize(self: *Terminal) void { /// encoded as "\n". This omits any formatting such as fg/bg. /// /// 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); } @@ -1634,6 +1634,7 @@ test "Terminal: print writes to bottom if scrolled" { // Basic grid writing for ("hello") |c| try t.print(c); + t.setCursorPos(0, 0); // Make newlines so we create scrollback // 3 pushes hello off the screen @@ -1704,7 +1705,7 @@ test "Terminal: print charset outside of ASCII" { { var str = try t.plainString(testing.allocator); defer testing.allocator.free(str); - try testing.expectEqualStrings("◆ ", str); + try testing.expectEqualStrings("◆ ", str); } } @@ -2192,6 +2193,7 @@ test "Terminal: index from the bottom" { t.setCursorPos(5, 1); try t.print('A'); + t.cursorLeft(1); // undo moving right from 'A' try t.index(); try t.print('B');