Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 15 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -185,6 +185,16 @@ Available border styles:

The focused pane uses `focused_color` (default: blue) to make it easy to identify the active terminal.

### Capture Pane

Capture the full scrollback history of the active pane and open it in a new tab in your editor:

The `capture_pane` action (default keybind: `<leader>x`) will:
1. Capture the entire scrollback history of the active pane
2. Write it to a temporary file in `/tmp` (named `pane_YYYYMMDD_HHMMSS.txt`)
3. Create a new tab and open the file in your configured `$EDITOR` (defaults to `vim`)
4. Close the tab automatically when you quit the editor

### Default Keybinds

The default leader key is `Super+k` (Cmd+k on macOS). After pressing the leader:
Expand All @@ -200,9 +210,13 @@ The default leader key is `Super+k` (Cmd+k on macOS). After pressing the leader:
| `c` | Close tab |
| `n/p` | Next/previous tab |
| `d` | Detach session |
| `x` | Capture pane |
| `q` | Quit |

Press `Super+p` to open the command palette.
Additional keybinds:
- `Super+p` - Open command palette

Press `Super+p` to open the command palette for other available commands.

### Custom Keybinds

Expand Down
2 changes: 2 additions & 0 deletions src/action.zig
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ pub const Action = union(enum) {

// UI
command_palette,
capture_pane,

// Lua function reference (registry ref)
lua_function: i32,
Expand Down Expand Up @@ -126,6 +127,7 @@ pub const Action = union(enum) {
.rename_session => "Rename Session",
.quit => "Quit",
.command_palette => "Command Palette",
.capture_pane => "Capture Pane",
.lua_function => null,
};
}
Expand Down
71 changes: 69 additions & 2 deletions src/client.zig
Original file line number Diff line number Diff line change
Expand Up @@ -186,6 +186,7 @@ pub const ClientState = struct {
detach,
get_server_info,
copy_selection,
capture_pane: struct { pty_id: u32 },
};

pub fn init(allocator: std.mem.Allocator) ClientState {
Expand Down Expand Up @@ -218,6 +219,7 @@ pub const ServerAction = union(enum) {
color_query: ColorQueryTarget,
server_info: struct { pty_validity: i64 },
copy_to_clipboard: []const u8,
capture_pane_complete: struct { pty_id: u32, content: []const u8 },

pub const ColorQueryTarget = struct {
pty_id: u32,
Expand Down Expand Up @@ -296,6 +298,7 @@ pub const ClientLogic = struct {
.detach => .detached,
.get_server_info => handleServerInfoResult(state, result),
.copy_selection => handleCopySelectionResult(result),
.capture_pane => |capture_info| handleCapturePaneResult(result, capture_info.pty_id),
};
}
return handleUnsolicitedResult(state, result);
Expand All @@ -314,6 +317,15 @@ pub const ClientLogic = struct {
return .none;
}

fn handleCapturePaneResult(result: msgpack.Value, pty_id: u32) ServerAction {
if (result == .string) {
log.info("handleCapturePaneResult: received {} bytes for pty {}", .{ result.string.len, pty_id });
return .{ .capture_pane_complete = .{ .pty_id = pty_id, .content = result.string } };
}
log.warn("handleCapturePaneResult: unexpected result type: {s}", .{@tagName(result)});
return .none;
}

fn handleServerInfoResult(state: *ClientState, result: msgpack.Value) ServerAction {
if (result != .map) return .none;

Expand Down Expand Up @@ -2136,6 +2148,12 @@ pub const App = struct {
try self.requestCopySelection(id);
}
}.appCopySelection,
.capture_pane_fn = struct {
fn appCapturePane(ctx: *anyopaque, id: u32) anyerror!void {
const self: *App = @ptrCast(@alignCast(ctx));
try self.requestCapturePane(id);
}
}.appCapturePane,
.cell_size_fn = struct {
fn appGetCellSize(ctx: *anyopaque) lua_event.CellSize {
const self: *App = @ptrCast(@alignCast(ctx));
Expand Down Expand Up @@ -2308,6 +2326,11 @@ pub const App = struct {
.copy_to_clipboard => |text| {
app.copyToClipboard(text);
},
.capture_pane_complete => |info| {
app.ui.update(.{ .capture_pane_complete = .{ .pty_id = info.pty_id, .content = info.content } }) catch |err| {
log.err("Failed to update UI with capture_pane_complete: {}", .{err});
};
},
.none => {},
}

Expand Down Expand Up @@ -2384,16 +2407,38 @@ pub const App = struct {
try env_array.append(self.allocator, .{ .string = env_str });
}

const num_params: usize = if (opts.cwd != null) 5 else 4;
// Build command array if specified
var cmd_array = std.ArrayList(msgpack.Value).empty;
defer cmd_array.deinit(self.allocator);
if (opts.command) |command| {
log.info("spawnPty: building command with {} args", .{command.len});
for (command) |arg| {
log.info("spawnPty: arg = '{s}'", .{arg});
try cmd_array.append(self.allocator, .{ .string = arg });
}
} else {
log.info("spawnPty: no command in opts", .{});
}

var num_params: usize = 4; // rows, cols, attach, env
if (opts.cwd != null) num_params += 1;
if (opts.command != null) num_params += 1;

var map_items = try self.allocator.alloc(msgpack.Value.KeyValue, num_params);
defer self.allocator.free(map_items);

map_items[0] = .{ .key = .{ .string = "rows" }, .value = .{ .unsigned = opts.rows } };
map_items[1] = .{ .key = .{ .string = "cols" }, .value = .{ .unsigned = opts.cols } };
map_items[2] = .{ .key = .{ .string = "attach" }, .value = .{ .boolean = opts.attach } };
map_items[3] = .{ .key = .{ .string = "env" }, .value = .{ .array = env_array.items } };

var idx: usize = 4;
if (opts.cwd) |cwd| {
map_items[4] = .{ .key = .{ .string = "cwd" }, .value = .{ .string = cwd } };
map_items[idx] = .{ .key = .{ .string = "cwd" }, .value = .{ .string = cwd } };
idx += 1;
}
if (opts.command != null) {
map_items[idx] = .{ .key = .{ .string = "command" }, .value = .{ .array = cmd_array.items } };
}

const params = msgpack.Value{ .map = map_items };
Expand Down Expand Up @@ -2424,6 +2469,22 @@ pub const App = struct {
try self.sendDirect(msg);
}

pub fn requestCapturePane(self: *App, pty_id: u32) !void {
const msgid = self.state.next_msgid;
self.state.next_msgid += 1;

try self.state.pending_requests.put(msgid, .{ .capture_pane = .{ .pty_id = pty_id } });

var params = try self.allocator.alloc(msgpack.Value, 1);
defer self.allocator.free(params);
params[0] = .{ .unsigned = pty_id };

const msg = try msgpack.encode(self.allocator, .{ 0, msgid, "capture_pane", msgpack.Value{ .array = params } });
defer self.allocator.free(msg);

try self.sendDirect(msg);
}

fn copyToClipboard(self: *App, text: []const u8) void {
if (text.len == 0) {
log.warn("copyToClipboard: empty text, nothing to copy", .{});
Expand Down Expand Up @@ -2969,6 +3030,12 @@ pub const App = struct {
try app.requestCopySelection(pty_id);
}
}.copySelection,
.capture_pane_fn = struct {
fn capturePane(app_ctx: *anyopaque, pty_id: u32) anyerror!void {
const app: *App = @ptrCast(@alignCast(app_ctx));
try app.requestCapturePane(pty_id);
}
}.capturePane,
.cell_size_fn = struct {
fn getCellSize(app_ctx: *anyopaque) lua_event.CellSize {
const app: *App = @ptrCast(@alignCast(app_ctx));
Expand Down
Loading
Loading