mirror of
https://github.com/ghostty-org/ghostty.git
synced 2025-07-03 12:48:44 +03:00
apprt/embedded: improve text reading APIs (selection, random points)
This commit is contained in:
@ -355,6 +355,27 @@ typedef struct {
|
||||
double tl_px_y;
|
||||
uint32_t offset_start;
|
||||
uint32_t offset_len;
|
||||
const char* text;
|
||||
uintptr_t text_len;
|
||||
} ghostty_text_s;
|
||||
|
||||
typedef enum {
|
||||
GHOSTTY_POINT_ACTIVE,
|
||||
GHOSTTY_POINT_VIEWPORT,
|
||||
GHOSTTY_POINT_SCREEN,
|
||||
GHOSTTY_POINT_SURFACE,
|
||||
} ghostty_point_tag_e;
|
||||
|
||||
typedef struct {
|
||||
ghostty_point_tag_e tag;
|
||||
uint32_t x;
|
||||
uint32_t y;
|
||||
} ghostty_point_s;
|
||||
|
||||
typedef struct {
|
||||
ghostty_point_s top_left;
|
||||
ghostty_point_s bottom_right;
|
||||
bool rectangle;
|
||||
} ghostty_selection_s;
|
||||
|
||||
typedef struct {
|
||||
@ -832,16 +853,16 @@ void ghostty_surface_complete_clipboard_request(ghostty_surface_t,
|
||||
void*,
|
||||
bool);
|
||||
bool ghostty_surface_has_selection(ghostty_surface_t);
|
||||
uintptr_t ghostty_surface_selection(ghostty_surface_t, char*, uintptr_t);
|
||||
bool ghostty_surface_read_selection(ghostty_surface_t, ghostty_text_s*);
|
||||
bool ghostty_surface_read_text(ghostty_surface_t,
|
||||
ghostty_selection_s,
|
||||
ghostty_text_s*);
|
||||
void ghostty_surface_free_text(ghostty_surface_t, ghostty_text_s*);
|
||||
|
||||
#ifdef __APPLE__
|
||||
void ghostty_surface_set_display_id(ghostty_surface_t, uint32_t);
|
||||
void* ghostty_surface_quicklook_font(ghostty_surface_t);
|
||||
uintptr_t ghostty_surface_quicklook_word(ghostty_surface_t,
|
||||
char*,
|
||||
uintptr_t,
|
||||
ghostty_selection_s*);
|
||||
bool ghostty_surface_selection_info(ghostty_surface_t, ghostty_selection_s*);
|
||||
bool ghostty_surface_quicklook_word(ghostty_surface_t, ghostty_text_s*);
|
||||
#endif
|
||||
|
||||
ghostty_inspector_t ghostty_surface_inspector(ghostty_surface_t);
|
||||
|
@ -1215,11 +1215,10 @@ extension Ghostty {
|
||||
guard let surface = self.surface else { return super.quickLook(with: event) }
|
||||
|
||||
// Grab the text under the cursor
|
||||
var info: ghostty_selection_s = ghostty_selection_s();
|
||||
let text = String(unsafeUninitializedCapacity: 1000000) {
|
||||
Int(ghostty_surface_quicklook_word(surface, $0.baseAddress, UInt($0.count), &info))
|
||||
}
|
||||
guard !text.isEmpty else { return super.quickLook(with: event) }
|
||||
var text = ghostty_text_s()
|
||||
guard ghostty_surface_quicklook_word(surface, &text) else { return super.quickLook(with: event) }
|
||||
defer { ghostty_surface_free_text(surface, &text) }
|
||||
guard text.text_len > 0 else { return super.quickLook(with: event) }
|
||||
|
||||
// If we can get a font then we use the font. This should always work
|
||||
// since we always have a primary font. The only scenario this doesn't
|
||||
@ -1236,8 +1235,8 @@ extension Ghostty {
|
||||
}
|
||||
|
||||
// Ghostty coordinate system is top-left, convert to bottom-left for AppKit
|
||||
let pt = NSMakePoint(info.tl_px_x, frame.size.height - info.tl_px_y)
|
||||
let str = NSAttributedString.init(string: text, attributes: attributes)
|
||||
let pt = NSMakePoint(text.tl_px_x, frame.size.height - text.tl_px_y)
|
||||
let str = NSAttributedString.init(string: String(cString: text.text), attributes: attributes)
|
||||
self.showDefinition(for: str, at: pt);
|
||||
}
|
||||
|
||||
@ -1522,9 +1521,10 @@ extension Ghostty.SurfaceView: NSTextInputClient {
|
||||
// Get our range from the Ghostty API. There is a race condition between getting the
|
||||
// range and actually using it since our selection may change but there isn't a good
|
||||
// way I can think of to solve this for AppKit.
|
||||
var sel: ghostty_selection_s = ghostty_selection_s();
|
||||
guard ghostty_surface_selection_info(surface, &sel) else { return NSRange() }
|
||||
return NSRange(location: Int(sel.offset_start), length: Int(sel.offset_len))
|
||||
var text = ghostty_text_s()
|
||||
guard ghostty_surface_read_selection(surface, &text) else { return NSRange() }
|
||||
defer { ghostty_surface_free_text(surface, &text) }
|
||||
return NSRange(location: Int(text.offset_start), length: Int(text.offset_len))
|
||||
}
|
||||
|
||||
func setMarkedText(_ string: Any, selectedRange: NSRange, replacementRange: NSRange) {
|
||||
@ -1562,7 +1562,6 @@ extension Ghostty.SurfaceView: NSTextInputClient {
|
||||
func attributedSubstring(forProposedRange range: NSRange, actualRange: NSRangePointer?) -> NSAttributedString? {
|
||||
// Ghostty.logger.warning("pressure substring range=\(range) selectedRange=\(self.selectedRange())")
|
||||
guard let surface = self.surface else { return nil }
|
||||
guard ghostty_surface_has_selection(surface) else { return nil }
|
||||
|
||||
// If the range is empty then we don't need to return anything
|
||||
guard range.length > 0 else { return nil }
|
||||
@ -1572,11 +1571,10 @@ extension Ghostty.SurfaceView: NSTextInputClient {
|
||||
// bogus ranges I truly don't understand so we just always return the
|
||||
// attributed string containing our selection which is... weird but works?
|
||||
|
||||
// Get our selection. We cap it at 1MB for the purpose of this. This is
|
||||
// arbitrary. If this is a good reason to increase it I'm happy to.
|
||||
let v = String(unsafeUninitializedCapacity: 1000000) {
|
||||
Int(ghostty_surface_selection(surface, $0.baseAddress, UInt($0.count)))
|
||||
}
|
||||
// Get our selection text
|
||||
var text = ghostty_text_s()
|
||||
guard ghostty_surface_read_selection(surface, &text) else { return nil }
|
||||
defer { ghostty_surface_free_text(surface, &text) }
|
||||
|
||||
// If we can get a font then we use the font. This should always work
|
||||
// since we always have a primary font. The only scenario this doesn't
|
||||
@ -1592,7 +1590,7 @@ extension Ghostty.SurfaceView: NSTextInputClient {
|
||||
font.release()
|
||||
}
|
||||
|
||||
return .init(string: v, attributes: attributes)
|
||||
return .init(string: String(cString: text.text), attributes: attributes)
|
||||
}
|
||||
|
||||
func characterIndex(for point: NSPoint) -> Int {
|
||||
@ -1614,12 +1612,15 @@ extension Ghostty.SurfaceView: NSTextInputClient {
|
||||
// point right now. I'm sure I'm missing something fundamental...
|
||||
if range.length > 0 && range != self.selectedRange() {
|
||||
// QuickLook
|
||||
var sel: ghostty_selection_s = ghostty_selection_s();
|
||||
if ghostty_surface_selection_info(surface, &sel) {
|
||||
var text = ghostty_text_s()
|
||||
if ghostty_surface_read_selection(surface, &text) {
|
||||
// The -2/+2 here is subjective. QuickLook seems to offset the rectangle
|
||||
// a bit and I think these small adjustments make it look more natural.
|
||||
x = sel.tl_px_x - 2;
|
||||
y = sel.tl_px_y + 2;
|
||||
x = text.tl_px_x - 2;
|
||||
y = text.tl_px_y + 2;
|
||||
|
||||
// Free our text
|
||||
ghostty_surface_free_text(surface, &text)
|
||||
} else {
|
||||
ghostty_surface_ime_point(surface, &x, &y)
|
||||
}
|
||||
@ -1745,14 +1746,13 @@ extension Ghostty.SurfaceView: NSServicesMenuRequestor {
|
||||
) -> Bool {
|
||||
guard let surface = self.surface else { return false }
|
||||
|
||||
// We currently cap the maximum copy size to 1MB. iTerm2 I believe
|
||||
// caps theirs at 0.1MB (configurable) so this is probably reasonable.
|
||||
let v = String(unsafeUninitializedCapacity: 1000000) {
|
||||
Int(ghostty_surface_selection(surface, $0.baseAddress, UInt($0.count)))
|
||||
}
|
||||
// Read the selection
|
||||
var text = ghostty_text_s()
|
||||
guard ghostty_surface_read_selection(surface, &text) else { return false }
|
||||
defer { ghostty_surface_free_text(surface, &text) }
|
||||
|
||||
pboard.declareTypes([.string], owner: nil)
|
||||
pboard.setString(v, forType: .string)
|
||||
pboard.setString(String(cString: text.text), forType: .string)
|
||||
return true
|
||||
}
|
||||
|
||||
@ -1866,4 +1866,25 @@ extension Ghostty.SurfaceView {
|
||||
override func accessibilityHelp() -> String? {
|
||||
return "Terminal content area"
|
||||
}
|
||||
|
||||
/// Returns the range of text that is currently selected in the terminal.
|
||||
/// This allows VoiceOver and other assistive technologies to understand
|
||||
/// what text the user has selected.
|
||||
override func accessibilitySelectedTextRange() -> NSRange {
|
||||
return selectedRange()
|
||||
}
|
||||
|
||||
/// Returns the currently selected text as a string.
|
||||
/// This allows assistive technologies to read the selected content.
|
||||
override func accessibilitySelectedText() -> String? {
|
||||
guard let surface = self.surface else { return nil }
|
||||
|
||||
// Attempt to read the selection
|
||||
var text = ghostty_text_s()
|
||||
guard ghostty_surface_read_selection(surface, &text) else { return nil }
|
||||
defer { ghostty_surface_free_text(surface, &text) }
|
||||
|
||||
let str = String(cString: text.text)
|
||||
return str.isEmpty ? nil : str
|
||||
}
|
||||
}
|
||||
|
127
src/Surface.zig
127
src/Surface.zig
@ -1292,6 +1292,133 @@ fn recomputeInitialSize(
|
||||
) catch return error.AppActionFailed;
|
||||
}
|
||||
|
||||
/// Represents text read from the terminal and some metadata about it
|
||||
/// that is often useful to apprts.
|
||||
pub const Text = struct {
|
||||
/// The text that was read from the terminal.
|
||||
text: [:0]const u8,
|
||||
|
||||
/// The viewport information about this text, if it is visible in
|
||||
/// the viewport.
|
||||
///
|
||||
/// NOTE(mitchellh): This will only be non-null currently if the entirety
|
||||
/// of the selection is contained within the viewport. We don't have a
|
||||
/// use case currently for partial bounds but we should support this
|
||||
/// eventually.
|
||||
viewport: ?Viewport = null,
|
||||
|
||||
pub const Viewport = struct {
|
||||
/// The top-left corner of the selection in pixels within the viewport.
|
||||
tl_px_x: f64,
|
||||
tl_px_y: f64,
|
||||
|
||||
/// The linear offset of the start of the selection and the length.
|
||||
/// This is "linear" in the sense that it is the offset in the
|
||||
/// flattened viewport as a single array of text.
|
||||
offset_start: u32,
|
||||
offset_len: u32,
|
||||
};
|
||||
|
||||
pub fn deinit(self: *Text, alloc: Allocator) void {
|
||||
alloc.free(self.text);
|
||||
}
|
||||
};
|
||||
|
||||
/// Grab the value of text at the given selection point. Note that the
|
||||
/// selection structure is used as a way to determine the area of the
|
||||
/// screen to read from, it doesn't have to match the user's current
|
||||
/// selection state.
|
||||
///
|
||||
/// The returned value contains allocated data and must be deinitialized.
|
||||
pub fn dumpText(
|
||||
self: *Surface,
|
||||
alloc: Allocator,
|
||||
sel: terminal.Selection,
|
||||
) !Text {
|
||||
self.renderer_state.mutex.lock();
|
||||
defer self.renderer_state.mutex.unlock();
|
||||
return try self.dumpTextLocked(alloc, sel);
|
||||
}
|
||||
|
||||
/// Same as `dumpText` but assumes the renderer state mutex is already
|
||||
/// held.
|
||||
pub fn dumpTextLocked(
|
||||
self: *Surface,
|
||||
alloc: Allocator,
|
||||
sel: terminal.Selection,
|
||||
) !Text {
|
||||
// Read out the text
|
||||
const text = try self.io.terminal.screen.selectionString(alloc, .{
|
||||
.sel = sel,
|
||||
.trim = false,
|
||||
});
|
||||
errdefer alloc.free(text);
|
||||
|
||||
// Calculate our viewport info if we can.
|
||||
const vp: ?Text.Viewport = viewport: {
|
||||
// If our tl or br is not in the viewport then we don't
|
||||
// have a viewport. One day we should extend this to support
|
||||
// partial selections that are in the viewport.
|
||||
const tl_pt = self.io.terminal.screen.pages.pointFromPin(
|
||||
.viewport,
|
||||
sel.topLeft(&self.io.terminal.screen),
|
||||
) orelse break :viewport null;
|
||||
const br_pt = self.io.terminal.screen.pages.pointFromPin(
|
||||
.viewport,
|
||||
sel.bottomRight(&self.io.terminal.screen),
|
||||
) orelse break :viewport null;
|
||||
const tl_coord = tl_pt.coord();
|
||||
const br_coord = br_pt.coord();
|
||||
|
||||
// Our sizes are all scaled so we need to send the unscaled values back.
|
||||
const content_scale = self.rt_surface.getContentScale() catch .{ .x = 1, .y = 1 };
|
||||
const x: f64 = x: {
|
||||
// Simple x * cell width gives the left
|
||||
var x: f64 = @floatFromInt(tl_coord.x * self.size.cell.width);
|
||||
|
||||
// Add padding
|
||||
x += @floatFromInt(self.size.padding.left);
|
||||
|
||||
// Scale
|
||||
x /= content_scale.x;
|
||||
|
||||
break :x x;
|
||||
};
|
||||
const y: f64 = y: {
|
||||
// Simple y * cell height gives the top
|
||||
var y: f64 = @floatFromInt(tl_coord.y * self.size.cell.height);
|
||||
|
||||
// We want the text baseline
|
||||
y += @floatFromInt(self.size.cell.height);
|
||||
y -= @floatFromInt(self.font_metrics.cell_baseline);
|
||||
|
||||
// Add padding
|
||||
y += @floatFromInt(self.size.padding.top);
|
||||
|
||||
// Scale
|
||||
y /= content_scale.y;
|
||||
|
||||
break :y y;
|
||||
};
|
||||
|
||||
// Utilize viewport sizing to convert to offsets
|
||||
const start = tl_coord.y * self.io.terminal.screen.pages.cols + tl_coord.x;
|
||||
const end = br_coord.y * self.io.terminal.screen.pages.cols + br_coord.x;
|
||||
|
||||
break :viewport .{
|
||||
.tl_px_x = x,
|
||||
.tl_px_y = y,
|
||||
.offset_start = start,
|
||||
.offset_len = end - start,
|
||||
};
|
||||
};
|
||||
|
||||
return .{
|
||||
.text = text,
|
||||
.viewport = vp,
|
||||
};
|
||||
}
|
||||
|
||||
/// Returns true if the terminal has a selection.
|
||||
pub fn hasSelection(self: *const Surface) bool {
|
||||
self.renderer_state.mutex.lock();
|
||||
|
@ -1138,13 +1138,6 @@ pub const CAPI = struct {
|
||||
}
|
||||
};
|
||||
|
||||
const Selection = extern struct {
|
||||
tl_x_px: f64,
|
||||
tl_y_px: f64,
|
||||
offset_start: u32,
|
||||
offset_len: u32,
|
||||
};
|
||||
|
||||
const SurfaceSize = extern struct {
|
||||
columns: u16,
|
||||
rows: u16,
|
||||
@ -1154,6 +1147,83 @@ pub const CAPI = struct {
|
||||
cell_height_px: u32,
|
||||
};
|
||||
|
||||
// ghostty_text_s
|
||||
const Text = extern struct {
|
||||
tl_px_x: f64,
|
||||
tl_px_y: f64,
|
||||
offset_start: u32,
|
||||
offset_len: u32,
|
||||
text: ?[*:0]const u8,
|
||||
text_len: usize,
|
||||
|
||||
pub fn deinit(self: *Text) void {
|
||||
if (self.text) |ptr| {
|
||||
global.alloc.free(ptr[0..self.text_len :0]);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// ghostty_point_s
|
||||
const Point = extern struct {
|
||||
tag: Tag,
|
||||
x: u32,
|
||||
y: u32,
|
||||
|
||||
const Tag = enum(c_int) {
|
||||
active = 0,
|
||||
viewport = 1,
|
||||
screen = 2,
|
||||
history = 3,
|
||||
};
|
||||
|
||||
fn core(self: Point) terminal.Point {
|
||||
// This comes from the C API so we can't trust the input.
|
||||
const pt_x = std.math.cast(
|
||||
terminal.size.CellCountInt,
|
||||
self.x,
|
||||
) orelse std.math.maxInt(terminal.size.CellCountInt);
|
||||
|
||||
return switch (self.tag) {
|
||||
inline else => |tag| @unionInit(
|
||||
terminal.Point,
|
||||
@tagName(tag),
|
||||
.{ .x = pt_x, .y = self.y },
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
fn clamp(self: Point, screen: *const terminal.Screen) Point {
|
||||
// Clamp our point to the screen bounds.
|
||||
const clamped_x = @min(self.x, screen.pages.cols -| 1);
|
||||
const clamped_y = @min(self.y, screen.pages.rows -| 1);
|
||||
return .{ .tag = self.tag, .x = clamped_x, .y = clamped_y };
|
||||
}
|
||||
};
|
||||
|
||||
// ghostty_selection_s
|
||||
const Selection = extern struct {
|
||||
tl: Point,
|
||||
br: Point,
|
||||
rectangle: bool,
|
||||
|
||||
fn core(
|
||||
self: Selection,
|
||||
screen: *const terminal.Screen,
|
||||
) ?terminal.Selection {
|
||||
return .{
|
||||
.bounds = .{ .untracked = .{
|
||||
.start = screen.pages.pin(
|
||||
self.tl.clamp(screen).core(),
|
||||
) orelse return null,
|
||||
.end = screen.pages.pin(
|
||||
self.br.clamp(screen).core(),
|
||||
) orelse return null,
|
||||
} },
|
||||
.rectangle = self.rectangle,
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
// Reference the conditional exports based on target platform
|
||||
// so they're included in the C API.
|
||||
comptime {
|
||||
@ -1369,23 +1439,80 @@ pub const CAPI = struct {
|
||||
return surface.core_surface.hasSelection();
|
||||
}
|
||||
|
||||
/// Copies the surface selection text into the provided buffer and
|
||||
/// returns the copied size. If the buffer is too small, there is no
|
||||
/// selection, or there is an error, then 0 is returned.
|
||||
export fn ghostty_surface_selection(surface: *Surface, buf: [*]u8, cap: usize) usize {
|
||||
const selection_ = surface.core_surface.selectionString(global.alloc) catch |err| {
|
||||
log.warn("error getting selection err={}", .{err});
|
||||
return 0;
|
||||
/// Same as ghostty_surface_read_text but reads from the user selection,
|
||||
/// if any.
|
||||
export fn ghostty_surface_read_selection(
|
||||
surface: *Surface,
|
||||
result: *Text,
|
||||
) bool {
|
||||
const core_surface = &surface.core_surface;
|
||||
core_surface.renderer_state.mutex.lock();
|
||||
defer core_surface.renderer_state.mutex.unlock();
|
||||
|
||||
// If we don't have a selection, do nothing.
|
||||
const core_sel = core_surface.io.terminal.screen.selection orelse return false;
|
||||
|
||||
// Read the text from the selection.
|
||||
return readTextLocked(surface, core_sel, result);
|
||||
}
|
||||
|
||||
/// Read some arbitrary text from the surface.
|
||||
///
|
||||
/// This is an expensive operation so it shouldn't be called too
|
||||
/// often. We recommend that callers cache the result and throttle
|
||||
/// calls to this function.
|
||||
export fn ghostty_surface_read_text(
|
||||
surface: *Surface,
|
||||
sel: Selection,
|
||||
result: *Text,
|
||||
) bool {
|
||||
surface.core_surface.renderer_state.mutex.lock();
|
||||
defer surface.core_surface.renderer_state.mutex.unlock();
|
||||
|
||||
const core_sel = sel.core(
|
||||
&surface.core_surface.renderer_state.terminal.screen,
|
||||
) orelse return false;
|
||||
|
||||
return readTextLocked(surface, core_sel, result);
|
||||
}
|
||||
|
||||
fn readTextLocked(
|
||||
surface: *Surface,
|
||||
core_sel: terminal.Selection,
|
||||
result: *Text,
|
||||
) bool {
|
||||
const core_surface = &surface.core_surface;
|
||||
|
||||
// Get our text directly from the core surface.
|
||||
const text = core_surface.dumpTextLocked(
|
||||
global.alloc,
|
||||
core_sel,
|
||||
) catch |err| {
|
||||
log.warn("error reading text err={}", .{err});
|
||||
return false;
|
||||
};
|
||||
const selection = selection_ orelse return 0;
|
||||
defer global.alloc.free(selection);
|
||||
|
||||
// If the buffer is too small, return no selection.
|
||||
if (selection.len > cap) return 0;
|
||||
const vp: CoreSurface.Text.Viewport = text.viewport orelse .{
|
||||
.tl_px_x = -1,
|
||||
.tl_px_y = -1,
|
||||
.offset_start = 0,
|
||||
.offset_len = 0,
|
||||
};
|
||||
|
||||
// Copy into the buffer and return the length
|
||||
@memcpy(buf[0..selection.len], selection);
|
||||
return selection.len;
|
||||
result.* = .{
|
||||
.tl_px_x = vp.tl_px_x,
|
||||
.tl_px_y = vp.tl_px_y,
|
||||
.offset_start = vp.offset_start,
|
||||
.offset_len = vp.offset_len,
|
||||
.text = text.text.ptr,
|
||||
.text_len = text.text.len,
|
||||
};
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
export fn ghostty_surface_free_text(ptr: *Text) void {
|
||||
ptr.deinit();
|
||||
}
|
||||
|
||||
/// Tell the surface that it needs to schedule a render
|
||||
@ -1888,21 +2015,12 @@ pub const CAPI = struct {
|
||||
/// This does not modify the selection active on the surface (if any).
|
||||
export fn ghostty_surface_quicklook_word(
|
||||
ptr: *Surface,
|
||||
buf: [*]u8,
|
||||
cap: usize,
|
||||
info: *Selection,
|
||||
) usize {
|
||||
result: *Text,
|
||||
) bool {
|
||||
const surface = &ptr.core_surface;
|
||||
surface.renderer_state.mutex.lock();
|
||||
defer surface.renderer_state.mutex.unlock();
|
||||
|
||||
// To make everything in this function easier, we modify the
|
||||
// selection to be the word under the cursor and call normal APIs.
|
||||
// We restore the old selection so it isn't ever changed. Since we hold
|
||||
// the renderer mutex it'll never show up in a frame.
|
||||
const prev = surface.io.terminal.screen.selection;
|
||||
defer surface.io.terminal.screen.selection = prev;
|
||||
|
||||
// Get our word selection
|
||||
const sel = sel: {
|
||||
const screen = &surface.renderer_state.terminal.screen;
|
||||
@ -1915,45 +2033,13 @@ pub const CAPI = struct {
|
||||
},
|
||||
}) orelse {
|
||||
if (comptime std.debug.runtime_safety) unreachable;
|
||||
return 0;
|
||||
return false;
|
||||
};
|
||||
break :sel surface.io.terminal.screen.selectWord(pin) orelse return 0;
|
||||
break :sel surface.io.terminal.screen.selectWord(pin) orelse return false;
|
||||
};
|
||||
|
||||
// Set the selection
|
||||
surface.io.terminal.screen.selection = sel;
|
||||
|
||||
// No we call normal functions. These require that the lock
|
||||
// is unlocked. This may cause a frame flicker with the fake
|
||||
// selection but I think the lack of new complexity is worth it
|
||||
// for now.
|
||||
{
|
||||
surface.renderer_state.mutex.unlock();
|
||||
defer surface.renderer_state.mutex.lock();
|
||||
const len = ghostty_surface_selection(ptr, buf, cap);
|
||||
if (!ghostty_surface_selection_info(ptr, info)) return 0;
|
||||
return len;
|
||||
}
|
||||
}
|
||||
|
||||
/// This returns the selection metadata for the current selection.
|
||||
/// This will return false if there is no selection or the
|
||||
/// selection is not fully contained in the viewport (since the
|
||||
/// metadata is all about that).
|
||||
export fn ghostty_surface_selection_info(
|
||||
ptr: *Surface,
|
||||
info: *Selection,
|
||||
) bool {
|
||||
const sel = ptr.core_surface.selectionInfo() orelse
|
||||
return false;
|
||||
|
||||
info.* = .{
|
||||
.tl_x_px = sel.tl_x_px,
|
||||
.tl_y_px = sel.tl_y_px,
|
||||
.offset_start = sel.offset_start,
|
||||
.offset_len = sel.offset_len,
|
||||
};
|
||||
return true;
|
||||
// Read the selection
|
||||
return readTextLocked(ptr, sel, result);
|
||||
}
|
||||
|
||||
export fn ghostty_inspector_metal_init(ptr: *Inspector, device: objc.c.id) bool {
|
||||
|
@ -35,6 +35,7 @@ pub const Page = page.Page;
|
||||
pub const PageList = @import("PageList.zig");
|
||||
pub const Parser = @import("Parser.zig");
|
||||
pub const Pin = PageList.Pin;
|
||||
pub const Point = point.Point;
|
||||
pub const Screen = @import("Screen.zig");
|
||||
pub const ScreenType = Terminal.ScreenType;
|
||||
pub const Selection = @import("Selection.zig");
|
||||
|
Reference in New Issue
Block a user