diff options
| author | Ogulcan Celik <ogulcancelik@gmail.com> | 2026-04-06 22:05:00 +0300 |
|---|---|---|
| committer | Ogulcan Celik <ogulcancelik@gmail.com> | 2026-04-06 22:56:01 +0300 |
| commit | 6ccf156a3dd23f419c03892ddc1136e72957f66a (patch) | |
| tree | 1d5465e61d03344a63683ad3b9bbdec9a4476a5c | |
| parent | 62b751b2b551d866e2aebfdecf89f7a003d64ebc (diff) | |
refactor: use libghostty-native selection text reading
| -rw-r--r-- | src/ghostty/mod.rs | 90 | ||||
| -rw-r--r-- | src/pane.rs | 45 | ||||
| -rw-r--r-- | src/selection.rs | 4 | ||||
| -rw-r--r-- | src/workspace.rs | 27 | ||||
| -rw-r--r-- | vendor/libghostty-vt/include/ghostty/vt/terminal.h | 48 | ||||
| -rw-r--r-- | vendor/libghostty-vt/src/lib_vt.zig | 1 | ||||
| -rw-r--r-- | vendor/libghostty-vt/src/terminal/c/main.zig | 1 | ||||
| -rw-r--r-- | vendor/libghostty-vt/src/terminal/c/terminal.zig | 70 | ||||
| -rw-r--r-- | vendor/libghostty-vt/src/terminal/c/types.zig | 1 |
9 files changed, 228 insertions, 59 deletions
diff --git a/src/ghostty/mod.rs b/src/ghostty/mod.rs index a4846db4..e9ca1f86 100644 --- a/src/ghostty/mod.rs +++ b/src/ghostty/mod.rs @@ -223,6 +223,23 @@ struct WritePtyCallbackState { callback: Box<dyn FnMut(&[u8]) + Send>, } +#[repr(C)] +struct GhosttyTerminalSelection { + start: ffi::GhosttyPoint, + end: ffi::GhosttyPoint, + rectangle: bool, +} + +unsafe extern "C" { + fn ghostty_terminal_read_text( + terminal: ffi::GhosttyTerminal_ptr, + selection: GhosttyTerminalSelection, + allocator: *const ffi::GhosttyAllocator, + out_ptr: *mut *mut u8, + out_len: *mut usize, + ) -> ffi::GhosttyResult; +} + unsafe extern "C" fn write_pty_trampoline( _terminal: ffi::GhosttyTerminal_ptr, userdata: *mut c_void, @@ -438,6 +455,46 @@ impl Terminal { Ok(buffer) } + pub fn read_text_viewport( + &self, + start: (u16, u32), + end: (u16, u32), + rectangle: bool, + ) -> Result<String, Error> { + let selection = GhosttyTerminalSelection { + start: ghostty_viewport_point(start.0, start.1), + end: ghostty_viewport_point(end.0, end.1), + rectangle, + }; + let mut out_ptr = ptr::null_mut(); + let mut out_len = 0usize; + unsafe { + ghostty_terminal_read_text( + self.raw, + selection, + ptr::null(), + &mut out_ptr, + &mut out_len, + ) + .into_result()?; + } + + let text = if out_len == 0 { + String::new() + } else { + let bytes = unsafe { slice::from_raw_parts(out_ptr.cast_const(), out_len) }; + String::from_utf8_lossy(bytes).into_owned() + }; + + if !out_ptr.is_null() { + unsafe { + ffi::ghostty_free(ptr::null(), out_ptr, out_len); + } + } + + Ok(text) + } + pub fn scroll_viewport_bottom(&mut self) { let viewport = ffi::GhosttyTerminalScrollViewport { tag: ffi::GhosttyTerminalScrollViewportTag_GHOSTTY_SCROLL_VIEWPORT_BOTTOM, @@ -504,6 +561,15 @@ impl Drop for Terminal { } } +fn ghostty_viewport_point(x: u16, y: u32) -> ffi::GhosttyPoint { + ffi::GhosttyPoint { + tag: ffi::GhosttyPointTag_GHOSTTY_POINT_TAG_VIEWPORT, + value: ffi::GhosttyPointValue { + coordinate: ffi::GhosttyPointCoordinate { x, y }, + }, + } +} + pub struct RenderState { raw: ffi::GhosttyRenderState_ptr, } @@ -1104,6 +1170,30 @@ mod tests { } #[test] + fn terminal_read_text_viewport_unwraps_soft_wrapped_selection() { + let mut terminal = Terminal::new(5, 3, 0).unwrap(); + terminal.write("1ABCD2EFGH3IJKL".as_bytes()); + + let text = terminal.read_text_viewport((0, 1), (2, 2), false).unwrap(); + assert_eq!(text, "2EFGH3IJ"); + } + + #[test] + fn terminal_read_text_viewport_handles_wide_chars() { + let mut terminal = Terminal::new(5, 3, 0).unwrap(); + terminal.write("1A⚡".as_bytes()); + + let full = terminal.read_text_viewport((0, 0), (3, 0), false).unwrap(); + assert_eq!(full, "1A⚡"); + + let through_wide_head = terminal.read_text_viewport((0, 0), (2, 0), false).unwrap(); + assert_eq!(through_wide_head, "1A⚡"); + + let wide_only = terminal.read_text_viewport((3, 0), (3, 0), false).unwrap(); + assert_eq!(wide_only, "⚡"); + } + + #[test] fn terminal_and_render_state_smoke_test() { let mut terminal = Terminal::new(8, 3, 100).unwrap(); assert_eq!(terminal.cols().unwrap(), 8); diff --git a/src/pane.rs b/src/pane.rs index 314554e1..a170d295 100644 --- a/src/pane.rs +++ b/src/pane.rs @@ -1059,47 +1059,12 @@ fn ghostty_extract_selection( core: &mut GhosttyPaneCore, selection: &crate::selection::Selection, ) -> Result<String, crate::ghostty::Error> { - let GhosttyPaneCore { - terminal, - render_state, - } = core; - render_state.update(terminal)?; - let mut row_iterator = crate::ghostty::RowIterator::new()?; - let mut row_cells = crate::ghostty::RowCells::new()?; - let mut rows = render_state.populate_row_iterator(&mut row_iterator)?; - let mut lines = Vec::new(); - let mut row_index = 0u16; - let ((start_row, _), (end_row, _)) = selection.ordered_cells(); - while rows.next() { - if row_index > end_row { - break; - } - if row_index >= start_row { - let mut cells = rows.populate_cells(&mut row_cells)?; - let (start_col, end_col) = selection_cols_for_row(selection, row_index); - let mut line = String::new(); - for x in start_col..=end_col { - cells.select(x)?; - line.push_str(&ghostty_cell_symbol(&cells)?); - } - lines.push(line.trim_end().to_string()); - } - row_index += 1; - } - Ok(lines.join("\n")) -} - -fn selection_cols_for_row(selection: &crate::selection::Selection, row: u16) -> (u16, u16) { let ((start_row, start_col), (end_row, end_col)) = selection.ordered_cells(); - if start_row == end_row { - (start_col, end_col) - } else if row == start_row { - (start_col, selection.pane_width().saturating_sub(1)) - } else if row == end_row { - (0, end_col) - } else { - (0, selection.pane_width().saturating_sub(1)) - } + core.terminal.read_text_viewport( + (start_col, u32::from(start_row)), + (end_col, u32::from(end_row)), + false, + ) } fn ghostty_screen_row( diff --git a/src/selection.rs b/src/selection.rs index 5531a189..bb5a76e8 100644 --- a/src/selection.rs +++ b/src/selection.rs @@ -104,10 +104,6 @@ impl Selection { self.ordered() } - pub(crate) fn pane_width(&self) -> u16 { - self.pane_inner.width - } - /// Check whether a pane-relative cell (row, col) is inside the selection. pub fn contains(&self, row: u16, col: u16) -> bool { if !self.is_visible() { diff --git a/src/workspace.rs b/src/workspace.rs index 25de6568..01a96b3f 100644 --- a/src/workspace.rs +++ b/src/workspace.rs @@ -670,13 +670,26 @@ pub fn derive_label_from_cwd(cwd: &Path) -> String { } pub fn git_branch(cwd: &Path) -> Option<String> { - let repo_root = git_repo_root(cwd)?; - let head_path = repo_root.join(".git").join("HEAD"); - let content = std::fs::read_to_string(head_path).ok()?; - let trimmed = content.trim(); - trimmed - .strip_prefix("ref: refs/heads/") - .map(|s| s.to_string()) + git_repo_root(cwd)?; + + let output = std::process::Command::new("git") + .arg("-C") + .arg(cwd) + .args(["rev-parse", "--abbrev-ref", "HEAD"]) + .output() + .ok()?; + + if !output.status.success() { + return None; + } + + let stdout = String::from_utf8(output.stdout).ok()?; + let branch = stdout.trim(); + if branch.is_empty() || branch == "HEAD" { + None + } else { + Some(branch.to_string()) + } } fn git_repo_root(start: &Path) -> Option<PathBuf> { diff --git a/vendor/libghostty-vt/include/ghostty/vt/terminal.h b/vendor/libghostty-vt/include/ghostty/vt/terminal.h index b43e37cf..b466bd84 100644 --- a/vendor/libghostty-vt/include/ghostty/vt/terminal.h +++ b/vendor/libghostty-vt/include/ghostty/vt/terminal.h @@ -254,6 +254,26 @@ typedef struct { } GhosttyTerminalScrollbar; /** + * Selection bounds for terminal text extraction. + * + * The start and end points may be in any supported point coordinate system + * (active, viewport, screen, or history). Text is always returned in reading + * order. Set `rectangle` to true for rectangular selection semantics. + * + * @ingroup terminal + */ +typedef struct { + /** Selection start point. */ + GhosttyPoint start; + + /** Selection end point. */ + GhosttyPoint end; + + /** Whether to treat the selection as a rectangle. */ + bool rectangle; +} GhosttyTerminalSelection; + +/** * Callback function type for bell. * * Called when the terminal receives a BEL character (0x07). @@ -974,6 +994,34 @@ GHOSTTY_API GhosttyResult ghostty_terminal_grid_ref(GhosttyTerminal terminal, GhosttyPoint point, GhosttyGridRef *out_ref); +/** + * Read terminal text for an arbitrary selection. + * + * Formats the selected region as plain text using Ghostty's native selection + * semantics. Soft-wrapped lines are unwrapped and trailing whitespace is + * trimmed on non-blank lines. + * + * The returned buffer is allocated using the provided allocator, or the + * default allocator if NULL is passed. Free it with ghostty_free(), passing + * the same allocator pointer and returned length. + * + * @param terminal The terminal handle (NULL returns GHOSTTY_INVALID_VALUE) + * @param selection The selection bounds to read + * @param allocator Pointer to allocator, or NULL to use the default allocator + * @param[out] out_ptr On success, set to the allocated output buffer + * @param[out] out_len On success, set to the output length in bytes + * @return GHOSTTY_SUCCESS on success, GHOSTTY_INVALID_VALUE if the terminal + * is NULL or the selection points are out of bounds, + * GHOSTTY_OUT_OF_MEMORY on allocation failure + * + * @ingroup terminal + */ +GHOSTTY_API GhosttyResult ghostty_terminal_read_text(GhosttyTerminal terminal, + GhosttyTerminalSelection selection, + const GhosttyAllocator* allocator, + uint8_t** out_ptr, + size_t* out_len); + /** @} */ #ifdef __cplusplus diff --git a/vendor/libghostty-vt/src/lib_vt.zig b/vendor/libghostty-vt/src/lib_vt.zig index 9f5ee6c4..9d2442a8 100644 --- a/vendor/libghostty-vt/src/lib_vt.zig +++ b/vendor/libghostty-vt/src/lib_vt.zig @@ -214,6 +214,7 @@ comptime { @export(&c.terminal_mode_set, .{ .name = "ghostty_terminal_mode_set" }); @export(&c.terminal_get, .{ .name = "ghostty_terminal_get" }); @export(&c.terminal_grid_ref, .{ .name = "ghostty_terminal_grid_ref" }); + @export(&c.terminal_read_text, .{ .name = "ghostty_terminal_read_text" }); @export(&c.grid_ref_cell, .{ .name = "ghostty_grid_ref_cell" }); @export(&c.grid_ref_row, .{ .name = "ghostty_grid_ref_row" }); @export(&c.grid_ref_graphemes, .{ .name = "ghostty_grid_ref_graphemes" }); diff --git a/vendor/libghostty-vt/src/terminal/c/main.zig b/vendor/libghostty-vt/src/terminal/c/main.zig index 17056779..34d1a8f4 100644 --- a/vendor/libghostty-vt/src/terminal/c/main.zig +++ b/vendor/libghostty-vt/src/terminal/c/main.zig @@ -142,6 +142,7 @@ pub const terminal_mode_get = terminal.mode_get; pub const terminal_mode_set = terminal.mode_set; pub const terminal_get = terminal.get; pub const terminal_grid_ref = terminal.grid_ref; +pub const terminal_read_text = terminal.read_text; pub const type_json = types.get_json; diff --git a/vendor/libghostty-vt/src/terminal/c/terminal.zig b/vendor/libghostty-vt/src/terminal/c/terminal.zig index ec749ffa..fa421f21 100644 --- a/vendor/libghostty-vt/src/terminal/c/terminal.zig +++ b/vendor/libghostty-vt/src/terminal/c/terminal.zig @@ -13,6 +13,8 @@ const size = @import("../size.zig"); const device_attributes = @import("../device_attributes.zig"); const device_status = @import("../device_status.zig"); const size_report = @import("../size_report.zig"); +const formatterpkg = @import("../formatter.zig"); +const selectionpkg = @import("../Selection.zig"); const cell_c = @import("cell.zig"); const row_c = @import("row.zig"); const grid_ref_c = @import("grid_ref.zig"); @@ -398,6 +400,21 @@ pub const DeviceAttributes = Effects.CDeviceAttributes; /// C: GhosttyTerminalScrollViewport pub const ScrollViewport = ZigTerminal.ScrollViewport.C; +/// C: GhosttyTerminalSelection +pub const Selection = extern struct { + start: point.Point.C, + end: point.Point.C, + rectangle: bool, + + fn toZig(self: Selection, terminal: *const ZigTerminal) ?selectionpkg { + return selectionpkg.init( + pinFromPoint(terminal, self.start) orelse return null, + pinFromPoint(terminal, self.end) orelse return null, + self.rectangle, + ); + } +}; + pub fn scroll_viewport( terminal_: Terminal, behavior: ScrollViewport, @@ -608,24 +625,61 @@ fn getTyped( return .success; } +fn pointToZig(pt: point.Point.C) point.Point { + return switch (pt.tag) { + .active => .{ .active = pt.value.active }, + .viewport => .{ .viewport = pt.value.viewport }, + .screen => .{ .screen = pt.value.screen }, + .history => .{ .history = pt.value.history }, + }; +} + +fn pinFromPoint(terminal: *const ZigTerminal, pt: point.Point.C) ?PageList.Pin { + return terminal.screens.active.pages.pin(pointToZig(pt)); +} + pub fn grid_ref( terminal_: Terminal, pt: point.Point.C, out_ref: ?*grid_ref_c.CGridRef, ) callconv(lib.calling_conv) Result { const t: *ZigTerminal = (terminal_ orelse return .invalid_value).terminal; - const zig_pt: point.Point = switch (pt.tag) { - .active => .{ .active = pt.value.active }, - .viewport => .{ .viewport = pt.value.viewport }, - .screen => .{ .screen = pt.value.screen }, - .history => .{ .history = pt.value.history }, - }; - const p = t.screens.active.pages.pin(zig_pt) orelse - return .invalid_value; + const p = pinFromPoint(t, pt) orelse return .invalid_value; if (out_ref) |out| out.* = grid_ref_c.CGridRef.fromPin(p); return .success; } +pub fn read_text( + terminal_: Terminal, + selection: Selection, + alloc_: ?*const CAllocator, + out_ptr: *?[*]u8, + out_len: *usize, +) callconv(lib.calling_conv) Result { + out_ptr.* = null; + out_len.* = 0; + + const t: *ZigTerminal = (terminal_ orelse return .invalid_value).terminal; + const alloc = lib.alloc.default(alloc_); + const sel = selection.toZig(t) orelse return .invalid_value; + + var formatter: formatterpkg.ScreenFormatter = .init(t.screens.active, .{ + .emit = .plain, + .unwrap = true, + .trim = true, + }); + formatter.content = .{ .selection = sel }; + + var aw: std.Io.Writer.Allocating = .init(alloc); + defer aw.deinit(); + formatter.format(&aw.writer) catch return .out_of_memory; + + const buf = aw.toOwnedSlice() catch return .out_of_memory; + out_ptr.* = buf.ptr; + out_len.* = buf.len; + return .success; +} + pub fn free(terminal_: Terminal) callconv(lib.calling_conv) void { const wrapper = terminal_ orelse return; const t = wrapper.terminal; diff --git a/vendor/libghostty-vt/src/terminal/c/types.zig b/vendor/libghostty-vt/src/terminal/c/types.zig index b808bf38..51184b47 100644 --- a/vendor/libghostty-vt/src/terminal/c/types.zig +++ b/vendor/libghostty-vt/src/terminal/c/types.zig @@ -41,6 +41,7 @@ pub const structs: std.StaticStringMap(StructInfo) = .initComptime(.{ .{ "GhosttyTerminalOptions", StructInfo.init(terminal.Options) }, .{ "GhosttyTerminalScrollbar", StructInfo.init(terminal.TerminalScrollbar) }, .{ "GhosttyTerminalScrollViewport", StructInfo.init(terminal.ScrollViewport) }, + .{ "GhosttyTerminalSelection", StructInfo.init(terminal.Selection) }, }); /// The comptime-generated JSON string of all structs. |
