mirror of
https://github.com/ghostty-org/ghostty.git
synced 2025-08-02 14:57:31 +03:00
Merge branch 'ghostty-org:main' into hu_HU_localization
This commit is contained in:
@ -355,6 +355,34 @@ typedef struct {
|
|||||||
double tl_px_y;
|
double tl_px_y;
|
||||||
uint32_t offset_start;
|
uint32_t offset_start;
|
||||||
uint32_t offset_len;
|
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 enum {
|
||||||
|
GHOSTTY_POINT_COORD_EXACT,
|
||||||
|
GHOSTTY_POINT_COORD_TOP_LEFT,
|
||||||
|
GHOSTTY_POINT_COORD_BOTTOM_RIGHT,
|
||||||
|
} ghostty_point_coord_e;
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
ghostty_point_tag_e tag;
|
||||||
|
ghostty_point_coord_e coord;
|
||||||
|
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;
|
} ghostty_selection_s;
|
||||||
|
|
||||||
typedef struct {
|
typedef struct {
|
||||||
@ -832,16 +860,16 @@ void ghostty_surface_complete_clipboard_request(ghostty_surface_t,
|
|||||||
void*,
|
void*,
|
||||||
bool);
|
bool);
|
||||||
bool ghostty_surface_has_selection(ghostty_surface_t);
|
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__
|
#ifdef __APPLE__
|
||||||
void ghostty_surface_set_display_id(ghostty_surface_t, uint32_t);
|
void ghostty_surface_set_display_id(ghostty_surface_t, uint32_t);
|
||||||
void* ghostty_surface_quicklook_font(ghostty_surface_t);
|
void* ghostty_surface_quicklook_font(ghostty_surface_t);
|
||||||
uintptr_t ghostty_surface_quicklook_word(ghostty_surface_t,
|
bool ghostty_surface_quicklook_word(ghostty_surface_t, ghostty_text_s*);
|
||||||
char*,
|
|
||||||
uintptr_t,
|
|
||||||
ghostty_selection_s*);
|
|
||||||
bool ghostty_surface_selection_info(ghostty_surface_t, ghostty_selection_s*);
|
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
ghostty_inspector_t ghostty_surface_inspector(ghostty_surface_t);
|
ghostty_inspector_t ghostty_surface_inspector(ghostty_surface_t);
|
||||||
|
@ -18,6 +18,7 @@ class AppDelegate: NSObject,
|
|||||||
)
|
)
|
||||||
|
|
||||||
/// Various menu items so that we can programmatically sync the keyboard shortcut with the Ghostty config
|
/// Various menu items so that we can programmatically sync the keyboard shortcut with the Ghostty config
|
||||||
|
@IBOutlet private var menuAbout: NSMenuItem?
|
||||||
@IBOutlet private var menuServices: NSMenu?
|
@IBOutlet private var menuServices: NSMenu?
|
||||||
@IBOutlet private var menuCheckForUpdates: NSMenuItem?
|
@IBOutlet private var menuCheckForUpdates: NSMenuItem?
|
||||||
@IBOutlet private var menuOpenConfig: NSMenuItem?
|
@IBOutlet private var menuOpenConfig: NSMenuItem?
|
||||||
@ -399,6 +400,11 @@ class AppDelegate: NSObject,
|
|||||||
private func setupMenuImages() {
|
private func setupMenuImages() {
|
||||||
// Note: This COULD Be done all in the xib file, but I find it easier to
|
// Note: This COULD Be done all in the xib file, but I find it easier to
|
||||||
// modify this stuff as code.
|
// modify this stuff as code.
|
||||||
|
self.menuAbout?.setImageIfDesired(systemSymbolName: "info.circle")
|
||||||
|
self.menuCheckForUpdates?.setImageIfDesired(systemSymbolName: "square.and.arrow.down")
|
||||||
|
self.menuOpenConfig?.setImageIfDesired(systemSymbolName: "gear")
|
||||||
|
self.menuReloadConfig?.setImageIfDesired(systemSymbolName: "arrow.trianglehead.2.clockwise.rotate.90")
|
||||||
|
self.menuSecureInput?.setImageIfDesired(systemSymbolName: "lock.display")
|
||||||
self.menuNewWindow?.setImageIfDesired(systemSymbolName: "macwindow.badge.plus")
|
self.menuNewWindow?.setImageIfDesired(systemSymbolName: "macwindow.badge.plus")
|
||||||
self.menuNewTab?.setImageIfDesired(systemSymbolName: "macwindow")
|
self.menuNewTab?.setImageIfDesired(systemSymbolName: "macwindow")
|
||||||
self.menuSplitRight?.setImageIfDesired(systemSymbolName: "rectangle.righthalf.inset.filled")
|
self.menuSplitRight?.setImageIfDesired(systemSymbolName: "rectangle.righthalf.inset.filled")
|
||||||
|
@ -14,6 +14,7 @@
|
|||||||
<customObject id="-3" userLabel="Application" customClass="NSObject"/>
|
<customObject id="-3" userLabel="Application" customClass="NSObject"/>
|
||||||
<customObject id="bbz-4X-AYv" userLabel="AppDelegate" customClass="AppDelegate" customModule="Ghostty" customModuleProvider="target">
|
<customObject id="bbz-4X-AYv" userLabel="AppDelegate" customClass="AppDelegate" customModule="Ghostty" customModuleProvider="target">
|
||||||
<connections>
|
<connections>
|
||||||
|
<outlet property="menuAbout" destination="5kV-Vb-QxS" id="Y5y-UO-NK6"/>
|
||||||
<outlet property="menuBringAllToFront" destination="LE2-aR-0XJ" id="AP9-oK-60V"/>
|
<outlet property="menuBringAllToFront" destination="LE2-aR-0XJ" id="AP9-oK-60V"/>
|
||||||
<outlet property="menuChangeTitle" destination="24I-xg-qIq" id="kg6-kT-jNL"/>
|
<outlet property="menuChangeTitle" destination="24I-xg-qIq" id="kg6-kT-jNL"/>
|
||||||
<outlet property="menuCheckForUpdates" destination="GEA-5y-yzH" id="0nV-Tf-nJQ"/>
|
<outlet property="menuCheckForUpdates" destination="GEA-5y-yzH" id="0nV-Tf-nJQ"/>
|
||||||
|
@ -7,6 +7,7 @@ extension SplitView {
|
|||||||
let visibleSize: CGFloat
|
let visibleSize: CGFloat
|
||||||
let invisibleSize: CGFloat
|
let invisibleSize: CGFloat
|
||||||
let color: Color
|
let color: Color
|
||||||
|
@Binding var split: CGFloat
|
||||||
|
|
||||||
private var visibleWidth: CGFloat? {
|
private var visibleWidth: CGFloat? {
|
||||||
switch (direction) {
|
switch (direction) {
|
||||||
@ -79,6 +80,40 @@ extension SplitView {
|
|||||||
NSCursor.pop()
|
NSCursor.pop()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.accessibilityElement(children: .ignore)
|
||||||
|
.accessibilityLabel(axLabel)
|
||||||
|
.accessibilityValue("\(Int(split * 100))%")
|
||||||
|
.accessibilityHint(axHint)
|
||||||
|
.accessibilityAddTraits(.isButton)
|
||||||
|
.accessibilityAdjustableAction { direction in
|
||||||
|
let adjustment: CGFloat = 0.025
|
||||||
|
switch direction {
|
||||||
|
case .increment:
|
||||||
|
split = min(split + adjustment, 0.9)
|
||||||
|
case .decrement:
|
||||||
|
split = max(split - adjustment, 0.1)
|
||||||
|
@unknown default:
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private var axLabel: String {
|
||||||
|
switch direction {
|
||||||
|
case .horizontal:
|
||||||
|
return "Horizontal split divider"
|
||||||
|
case .vertical:
|
||||||
|
return "Vertical split divider"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private var axHint: String {
|
||||||
|
switch direction {
|
||||||
|
case .horizontal:
|
||||||
|
return "Drag to resize the left and right panes"
|
||||||
|
case .vertical:
|
||||||
|
return "Drag to resize the top and bottom panes"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -42,16 +42,23 @@ struct SplitView<L: View, R: View>: View {
|
|||||||
left
|
left
|
||||||
.frame(width: leftRect.size.width, height: leftRect.size.height)
|
.frame(width: leftRect.size.width, height: leftRect.size.height)
|
||||||
.offset(x: leftRect.origin.x, y: leftRect.origin.y)
|
.offset(x: leftRect.origin.x, y: leftRect.origin.y)
|
||||||
|
.accessibilityElement(children: .contain)
|
||||||
|
.accessibilityLabel(leftPaneLabel)
|
||||||
right
|
right
|
||||||
.frame(width: rightRect.size.width, height: rightRect.size.height)
|
.frame(width: rightRect.size.width, height: rightRect.size.height)
|
||||||
.offset(x: rightRect.origin.x, y: rightRect.origin.y)
|
.offset(x: rightRect.origin.x, y: rightRect.origin.y)
|
||||||
|
.accessibilityElement(children: .contain)
|
||||||
|
.accessibilityLabel(rightPaneLabel)
|
||||||
Divider(direction: direction,
|
Divider(direction: direction,
|
||||||
visibleSize: splitterVisibleSize,
|
visibleSize: splitterVisibleSize,
|
||||||
invisibleSize: splitterInvisibleSize,
|
invisibleSize: splitterInvisibleSize,
|
||||||
color: dividerColor)
|
color: dividerColor,
|
||||||
|
split: $split)
|
||||||
.position(splitterPoint)
|
.position(splitterPoint)
|
||||||
.gesture(dragGesture(geo.size, splitterPoint: splitterPoint))
|
.gesture(dragGesture(geo.size, splitterPoint: splitterPoint))
|
||||||
}
|
}
|
||||||
|
.accessibilityElement(children: .contain)
|
||||||
|
.accessibilityLabel(splitViewLabel)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -137,6 +144,35 @@ struct SplitView<L: View, R: View>: View {
|
|||||||
return CGPoint(x: size.width / 2, y: leftRect.size.height)
|
return CGPoint(x: size.width / 2, y: leftRect.size.height)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MARK: Accessibility
|
||||||
|
|
||||||
|
private var splitViewLabel: String {
|
||||||
|
switch direction {
|
||||||
|
case .horizontal:
|
||||||
|
return "Horizontal split view"
|
||||||
|
case .vertical:
|
||||||
|
return "Vertical split view"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private var leftPaneLabel: String {
|
||||||
|
switch direction {
|
||||||
|
case .horizontal:
|
||||||
|
return "Left pane"
|
||||||
|
case .vertical:
|
||||||
|
return "Top pane"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private var rightPaneLabel: String {
|
||||||
|
switch direction {
|
||||||
|
case .horizontal:
|
||||||
|
return "Right pane"
|
||||||
|
case .vertical:
|
||||||
|
return "Bottom pane"
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
enum SplitViewDirection: Codable {
|
enum SplitViewDirection: Codable {
|
||||||
|
@ -32,6 +32,8 @@ struct TerminalSplitSubtreeView: View {
|
|||||||
Ghostty.InspectableSurface(
|
Ghostty.InspectableSurface(
|
||||||
surfaceView: leafView,
|
surfaceView: leafView,
|
||||||
isSplit: !isRoot)
|
isSplit: !isRoot)
|
||||||
|
.accessibilityElement(children: .contain)
|
||||||
|
.accessibilityLabel("Terminal pane")
|
||||||
|
|
||||||
case .split(let split):
|
case .split(let split):
|
||||||
let splitViewDirection: SplitViewDirection = switch (split.direction) {
|
let splitViewDirection: SplitViewDirection = switch (split.direction) {
|
||||||
|
@ -139,6 +139,10 @@ struct DebugBuildWarningView: View {
|
|||||||
}
|
}
|
||||||
.background(Color(.windowBackgroundColor))
|
.background(Color(.windowBackgroundColor))
|
||||||
.frame(maxWidth: .infinity)
|
.frame(maxWidth: .infinity)
|
||||||
|
.accessibilityElement(children: .combine)
|
||||||
|
.accessibilityLabel("Debug build warning")
|
||||||
|
.accessibilityValue("Debug builds of Ghostty are very slow and you may experience performance problems. Debug builds are only recommended during development.")
|
||||||
|
.accessibilityAddTraits(.isStaticText)
|
||||||
.onTapGesture {
|
.onTapGesture {
|
||||||
isPopover = true
|
isPopover = true
|
||||||
}
|
}
|
||||||
|
@ -96,9 +96,16 @@ class TransparentTitlebarTerminalWindow: TerminalWindow {
|
|||||||
@available(macOS 13.0, *)
|
@available(macOS 13.0, *)
|
||||||
private func syncAppearanceVentura(_ surfaceConfig: Ghostty.SurfaceView.DerivedConfig) {
|
private func syncAppearanceVentura(_ surfaceConfig: Ghostty.SurfaceView.DerivedConfig) {
|
||||||
guard let titlebarContainer else { return }
|
guard let titlebarContainer else { return }
|
||||||
|
|
||||||
|
// Setup the titlebar background color to match ours
|
||||||
titlebarContainer.wantsLayer = true
|
titlebarContainer.wantsLayer = true
|
||||||
titlebarContainer.layer?.backgroundColor = preferredBackgroundColor?.cgColor
|
titlebarContainer.layer?.backgroundColor = preferredBackgroundColor?.cgColor
|
||||||
|
|
||||||
|
// See the docs for the function that sets this to true on why
|
||||||
effectViewIsHidden = false
|
effectViewIsHidden = false
|
||||||
|
|
||||||
|
// Necessary to not draw the border around the title
|
||||||
|
titlebarAppearsTransparent = true
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: View Finders
|
// MARK: View Finders
|
||||||
|
@ -138,6 +138,9 @@ extension Ghostty {
|
|||||||
// by the user, this is set to the prior value (which may be empty, but non-nil).
|
// by the user, this is set to the prior value (which may be empty, but non-nil).
|
||||||
private var titleFromTerminal: String?
|
private var titleFromTerminal: String?
|
||||||
|
|
||||||
|
// The cached contents of the screen.
|
||||||
|
private var cachedScreenContents: CachedValue<String>
|
||||||
|
|
||||||
/// Event monitor (see individual events for why)
|
/// Event monitor (see individual events for why)
|
||||||
private var eventMonitor: Any? = nil
|
private var eventMonitor: Any? = nil
|
||||||
|
|
||||||
@ -159,11 +162,38 @@ extension Ghostty {
|
|||||||
self.derivedConfig = DerivedConfig()
|
self.derivedConfig = DerivedConfig()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// We need to initialize this so it does something but we want to set
|
||||||
|
// it back up later so we can reference `self`. This is a hack we should
|
||||||
|
// fix at some point.
|
||||||
|
self.cachedScreenContents = .init(duration: .milliseconds(500)) { "" }
|
||||||
|
|
||||||
// Initialize with some default frame size. The important thing is that this
|
// Initialize with some default frame size. The important thing is that this
|
||||||
// is non-zero so that our layer bounds are non-zero so that our renderer
|
// is non-zero so that our layer bounds are non-zero so that our renderer
|
||||||
// can do SOMETHING.
|
// can do SOMETHING.
|
||||||
super.init(frame: NSMakeRect(0, 0, 800, 600))
|
super.init(frame: NSMakeRect(0, 0, 800, 600))
|
||||||
|
|
||||||
|
// Our cache of screen data
|
||||||
|
cachedScreenContents = .init(duration: .milliseconds(500)) { [weak self] in
|
||||||
|
guard let self else { return "" }
|
||||||
|
guard let surface = self.surface else { return "" }
|
||||||
|
var text = ghostty_text_s()
|
||||||
|
let sel = ghostty_selection_s(
|
||||||
|
top_left: ghostty_point_s(
|
||||||
|
tag: GHOSTTY_POINT_SCREEN,
|
||||||
|
coord: GHOSTTY_POINT_COORD_TOP_LEFT,
|
||||||
|
x: 0,
|
||||||
|
y: 0),
|
||||||
|
bottom_right: ghostty_point_s(
|
||||||
|
tag: GHOSTTY_POINT_SCREEN,
|
||||||
|
coord: GHOSTTY_POINT_COORD_BOTTOM_RIGHT,
|
||||||
|
x: 0,
|
||||||
|
y: 0),
|
||||||
|
rectangle: false)
|
||||||
|
guard ghostty_surface_read_text(surface, sel, &text) else { return "" }
|
||||||
|
defer { ghostty_surface_free_text(surface, &text) }
|
||||||
|
return String(cString: text.text)
|
||||||
|
}
|
||||||
|
|
||||||
// Set a timer to show the ghost emoji after 500ms if no title is set
|
// Set a timer to show the ghost emoji after 500ms if no title is set
|
||||||
titleFallbackTimer = Timer.scheduledTimer(withTimeInterval: 0.5, repeats: false) { [weak self] _ in
|
titleFallbackTimer = Timer.scheduledTimer(withTimeInterval: 0.5, repeats: false) { [weak self] _ in
|
||||||
if let self = self, self.title.isEmpty {
|
if let self = self, self.title.isEmpty {
|
||||||
@ -1215,11 +1245,10 @@ extension Ghostty {
|
|||||||
guard let surface = self.surface else { return super.quickLook(with: event) }
|
guard let surface = self.surface else { return super.quickLook(with: event) }
|
||||||
|
|
||||||
// Grab the text under the cursor
|
// Grab the text under the cursor
|
||||||
var info: ghostty_selection_s = ghostty_selection_s();
|
var text = ghostty_text_s()
|
||||||
let text = String(unsafeUninitializedCapacity: 1000000) {
|
guard ghostty_surface_quicklook_word(surface, &text) else { return super.quickLook(with: event) }
|
||||||
Int(ghostty_surface_quicklook_word(surface, $0.baseAddress, UInt($0.count), &info))
|
defer { ghostty_surface_free_text(surface, &text) }
|
||||||
}
|
guard text.text_len > 0 else { return super.quickLook(with: event) }
|
||||||
guard !text.isEmpty else { return super.quickLook(with: event) }
|
|
||||||
|
|
||||||
// If we can get a font then we use the font. This should always work
|
// 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
|
// since we always have a primary font. The only scenario this doesn't
|
||||||
@ -1236,8 +1265,8 @@ extension Ghostty {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Ghostty coordinate system is top-left, convert to bottom-left for AppKit
|
// 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 pt = NSMakePoint(text.tl_px_x, frame.size.height - text.tl_px_y)
|
||||||
let str = NSAttributedString.init(string: text, attributes: attributes)
|
let str = NSAttributedString.init(string: String(cString: text.text), attributes: attributes)
|
||||||
self.showDefinition(for: str, at: pt);
|
self.showDefinition(for: str, at: pt);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1522,9 +1551,10 @@ extension Ghostty.SurfaceView: NSTextInputClient {
|
|||||||
// Get our range from the Ghostty API. There is a race condition between getting the
|
// 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
|
// 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.
|
// way I can think of to solve this for AppKit.
|
||||||
var sel: ghostty_selection_s = ghostty_selection_s();
|
var text = ghostty_text_s()
|
||||||
guard ghostty_surface_selection_info(surface, &sel) else { return NSRange() }
|
guard ghostty_surface_read_selection(surface, &text) else { return NSRange() }
|
||||||
return NSRange(location: Int(sel.offset_start), length: Int(sel.offset_len))
|
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) {
|
func setMarkedText(_ string: Any, selectedRange: NSRange, replacementRange: NSRange) {
|
||||||
@ -1562,7 +1592,6 @@ extension Ghostty.SurfaceView: NSTextInputClient {
|
|||||||
func attributedSubstring(forProposedRange range: NSRange, actualRange: NSRangePointer?) -> NSAttributedString? {
|
func attributedSubstring(forProposedRange range: NSRange, actualRange: NSRangePointer?) -> NSAttributedString? {
|
||||||
// Ghostty.logger.warning("pressure substring range=\(range) selectedRange=\(self.selectedRange())")
|
// Ghostty.logger.warning("pressure substring range=\(range) selectedRange=\(self.selectedRange())")
|
||||||
guard let surface = self.surface else { return nil }
|
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
|
// If the range is empty then we don't need to return anything
|
||||||
guard range.length > 0 else { return nil }
|
guard range.length > 0 else { return nil }
|
||||||
@ -1572,11 +1601,10 @@ extension Ghostty.SurfaceView: NSTextInputClient {
|
|||||||
// bogus ranges I truly don't understand so we just always return the
|
// bogus ranges I truly don't understand so we just always return the
|
||||||
// attributed string containing our selection which is... weird but works?
|
// 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
|
// Get our selection text
|
||||||
// arbitrary. If this is a good reason to increase it I'm happy to.
|
var text = ghostty_text_s()
|
||||||
let v = String(unsafeUninitializedCapacity: 1000000) {
|
guard ghostty_surface_read_selection(surface, &text) else { return nil }
|
||||||
Int(ghostty_surface_selection(surface, $0.baseAddress, UInt($0.count)))
|
defer { ghostty_surface_free_text(surface, &text) }
|
||||||
}
|
|
||||||
|
|
||||||
// If we can get a font then we use the font. This should always work
|
// 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
|
// since we always have a primary font. The only scenario this doesn't
|
||||||
@ -1592,7 +1620,7 @@ extension Ghostty.SurfaceView: NSTextInputClient {
|
|||||||
font.release()
|
font.release()
|
||||||
}
|
}
|
||||||
|
|
||||||
return .init(string: v, attributes: attributes)
|
return .init(string: String(cString: text.text), attributes: attributes)
|
||||||
}
|
}
|
||||||
|
|
||||||
func characterIndex(for point: NSPoint) -> Int {
|
func characterIndex(for point: NSPoint) -> Int {
|
||||||
@ -1614,12 +1642,15 @@ extension Ghostty.SurfaceView: NSTextInputClient {
|
|||||||
// point right now. I'm sure I'm missing something fundamental...
|
// point right now. I'm sure I'm missing something fundamental...
|
||||||
if range.length > 0 && range != self.selectedRange() {
|
if range.length > 0 && range != self.selectedRange() {
|
||||||
// QuickLook
|
// QuickLook
|
||||||
var sel: ghostty_selection_s = ghostty_selection_s();
|
var text = ghostty_text_s()
|
||||||
if ghostty_surface_selection_info(surface, &sel) {
|
if ghostty_surface_read_selection(surface, &text) {
|
||||||
// The -2/+2 here is subjective. QuickLook seems to offset the rectangle
|
// 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.
|
// a bit and I think these small adjustments make it look more natural.
|
||||||
x = sel.tl_px_x - 2;
|
x = text.tl_px_x - 2;
|
||||||
y = sel.tl_px_y + 2;
|
y = text.tl_px_y + 2;
|
||||||
|
|
||||||
|
// Free our text
|
||||||
|
ghostty_surface_free_text(surface, &text)
|
||||||
} else {
|
} else {
|
||||||
ghostty_surface_ime_point(surface, &x, &y)
|
ghostty_surface_ime_point(surface, &x, &y)
|
||||||
}
|
}
|
||||||
@ -1745,14 +1776,13 @@ extension Ghostty.SurfaceView: NSServicesMenuRequestor {
|
|||||||
) -> Bool {
|
) -> Bool {
|
||||||
guard let surface = self.surface else { return false }
|
guard let surface = self.surface else { return false }
|
||||||
|
|
||||||
// We currently cap the maximum copy size to 1MB. iTerm2 I believe
|
// Read the selection
|
||||||
// caps theirs at 0.1MB (configurable) so this is probably reasonable.
|
var text = ghostty_text_s()
|
||||||
let v = String(unsafeUninitializedCapacity: 1000000) {
|
guard ghostty_surface_read_selection(surface, &text) else { return false }
|
||||||
Int(ghostty_surface_selection(surface, $0.baseAddress, UInt($0.count)))
|
defer { ghostty_surface_free_text(surface, &text) }
|
||||||
}
|
|
||||||
|
|
||||||
pboard.declareTypes([.string], owner: nil)
|
pboard.declareTypes([.string], owner: nil)
|
||||||
pboard.setString(v, forType: .string)
|
pboard.setString(String(cString: text.text), forType: .string)
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1844,3 +1874,148 @@ extension Ghostty.SurfaceView {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MARK: Accessibility
|
||||||
|
|
||||||
|
extension Ghostty.SurfaceView {
|
||||||
|
/// Indicates that this view should be exposed to accessibility tools like VoiceOver.
|
||||||
|
/// By returning true, we make the terminal surface accessible to screen readers
|
||||||
|
/// and other assistive technologies.
|
||||||
|
override func isAccessibilityElement() -> Bool {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Defines the accessibility role for this view, which helps assistive technologies
|
||||||
|
/// understand what kind of content this view contains and how users can interact with it.
|
||||||
|
override func accessibilityRole() -> NSAccessibility.Role? {
|
||||||
|
/// We use .textArea because the terminal surface is essentially an editable text area
|
||||||
|
/// where users can input commands and view output.
|
||||||
|
return .textArea
|
||||||
|
}
|
||||||
|
|
||||||
|
override func accessibilityHelp() -> String? {
|
||||||
|
return "Terminal content area"
|
||||||
|
}
|
||||||
|
|
||||||
|
override func accessibilityValue() -> Any? {
|
||||||
|
return cachedScreenContents.get()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 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
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the number of characters in the terminal content.
|
||||||
|
/// This helps assistive technologies understand the size of the content.
|
||||||
|
override func accessibilityNumberOfCharacters() -> Int {
|
||||||
|
let content = cachedScreenContents.get()
|
||||||
|
return content.count
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the visible character range for the terminal.
|
||||||
|
/// For terminals, we typically show all content as visible.
|
||||||
|
override func accessibilityVisibleCharacterRange() -> NSRange {
|
||||||
|
let content = cachedScreenContents.get()
|
||||||
|
return NSRange(location: 0, length: content.count)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the line number for a given character index.
|
||||||
|
/// This helps assistive technologies navigate by line.
|
||||||
|
override func accessibilityLine(for index: Int) -> Int {
|
||||||
|
let content = cachedScreenContents.get()
|
||||||
|
let substring = String(content.prefix(index))
|
||||||
|
return substring.components(separatedBy: .newlines).count - 1
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns a substring for the given range.
|
||||||
|
/// This allows assistive technologies to read specific portions of the content.
|
||||||
|
override func accessibilityString(for range: NSRange) -> String? {
|
||||||
|
let content = cachedScreenContents.get()
|
||||||
|
guard let swiftRange = Range(range, in: content) else { return nil }
|
||||||
|
return String(content[swiftRange])
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns an attributed string for the given range.
|
||||||
|
///
|
||||||
|
/// Note: right now this only applies font information. One day it'd be nice to extend
|
||||||
|
/// this to copy styling information as well but we need to augment Ghostty core to
|
||||||
|
/// expose that.
|
||||||
|
///
|
||||||
|
/// This provides styling information to assistive technologies.
|
||||||
|
override func accessibilityAttributedString(for range: NSRange) -> NSAttributedString? {
|
||||||
|
guard let surface = self.surface else { return nil }
|
||||||
|
guard let plainString = accessibilityString(for: range) else { return nil }
|
||||||
|
|
||||||
|
var attributes: [NSAttributedString.Key: Any] = [:]
|
||||||
|
|
||||||
|
// Try to get the font from the surface
|
||||||
|
if let fontRaw = ghostty_surface_quicklook_font(surface) {
|
||||||
|
let font = Unmanaged<CTFont>.fromOpaque(fontRaw)
|
||||||
|
attributes[.font] = font.takeUnretainedValue()
|
||||||
|
font.release()
|
||||||
|
}
|
||||||
|
|
||||||
|
return NSAttributedString(string: plainString, attributes: attributes)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Caches a value for some period of time, evicting it automatically when that time expires.
|
||||||
|
/// We use this to cache our surface content. This probably should be extracted some day
|
||||||
|
/// to a more generic helper.
|
||||||
|
fileprivate class CachedValue<T> {
|
||||||
|
private var value: T?
|
||||||
|
private let fetch: () -> T
|
||||||
|
private let duration: Duration
|
||||||
|
private var expiryTask: Task<Void, Never>?
|
||||||
|
|
||||||
|
init(duration: Duration, fetch: @escaping () -> T) {
|
||||||
|
self.duration = duration
|
||||||
|
self.fetch = fetch
|
||||||
|
}
|
||||||
|
|
||||||
|
deinit {
|
||||||
|
expiryTask?.cancel()
|
||||||
|
}
|
||||||
|
|
||||||
|
func get() -> T {
|
||||||
|
if let value {
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
|
||||||
|
// We don't have a value (or it expired). Fetch and store.
|
||||||
|
let result = fetch()
|
||||||
|
let now = ContinuousClock.now
|
||||||
|
let expires = now + duration
|
||||||
|
self.value = result
|
||||||
|
|
||||||
|
// Schedule a task to clear the value
|
||||||
|
expiryTask = Task { [weak self] in
|
||||||
|
do {
|
||||||
|
try await Task.sleep(until: expires)
|
||||||
|
self?.value = nil
|
||||||
|
self?.expiryTask = nil
|
||||||
|
} catch {
|
||||||
|
// Task was cancelled, do nothing
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
}
|
||||||
|
127
src/Surface.zig
127
src/Surface.zig
@ -1292,6 +1292,133 @@ fn recomputeInitialSize(
|
|||||||
) catch return error.AppActionFailed;
|
) 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.
|
/// Returns true if the terminal has a selection.
|
||||||
pub fn hasSelection(self: *const Surface) bool {
|
pub fn hasSelection(self: *const Surface) bool {
|
||||||
self.renderer_state.mutex.lock();
|
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 {
|
const SurfaceSize = extern struct {
|
||||||
columns: u16,
|
columns: u16,
|
||||||
rows: u16,
|
rows: u16,
|
||||||
@ -1154,6 +1147,104 @@ pub const CAPI = struct {
|
|||||||
cell_height_px: u32,
|
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,
|
||||||
|
coord_tag: CoordTag,
|
||||||
|
x: u32,
|
||||||
|
y: u32,
|
||||||
|
|
||||||
|
const Tag = enum(c_int) {
|
||||||
|
active = 0,
|
||||||
|
viewport = 1,
|
||||||
|
screen = 2,
|
||||||
|
history = 3,
|
||||||
|
};
|
||||||
|
|
||||||
|
const CoordTag = enum(c_int) {
|
||||||
|
exact = 0,
|
||||||
|
top_left = 1,
|
||||||
|
bottom_right = 2,
|
||||||
|
};
|
||||||
|
|
||||||
|
fn pin(
|
||||||
|
self: Point,
|
||||||
|
screen: *const terminal.Screen,
|
||||||
|
) ?terminal.Pin {
|
||||||
|
// The core point tag.
|
||||||
|
const tag: terminal.point.Tag = switch (self.tag) {
|
||||||
|
inline else => |tag| @field(
|
||||||
|
terminal.point.Tag,
|
||||||
|
@tagName(tag),
|
||||||
|
),
|
||||||
|
};
|
||||||
|
|
||||||
|
// 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 switch (self.coord_tag) {
|
||||||
|
// Exact coordinates require a specific pin.
|
||||||
|
.exact => exact: {
|
||||||
|
const pt_x = std.math.cast(
|
||||||
|
terminal.size.CellCountInt,
|
||||||
|
clamped_x,
|
||||||
|
) orelse std.math.maxInt(terminal.size.CellCountInt);
|
||||||
|
|
||||||
|
const pt: terminal.Point = switch (tag) {
|
||||||
|
inline else => |v| @unionInit(
|
||||||
|
terminal.Point,
|
||||||
|
@tagName(v),
|
||||||
|
.{ .x = pt_x, .y = clamped_y },
|
||||||
|
),
|
||||||
|
};
|
||||||
|
|
||||||
|
break :exact screen.pages.pin(pt) orelse null;
|
||||||
|
},
|
||||||
|
|
||||||
|
.top_left => screen.pages.getTopLeft(tag),
|
||||||
|
|
||||||
|
.bottom_right => screen.pages.getBottomRight(tag),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 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 = self.tl.pin(screen) orelse return null,
|
||||||
|
.end = self.br.pin(screen) orelse return null,
|
||||||
|
} },
|
||||||
|
.rectangle = self.rectangle,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// Reference the conditional exports based on target platform
|
// Reference the conditional exports based on target platform
|
||||||
// so they're included in the C API.
|
// so they're included in the C API.
|
||||||
comptime {
|
comptime {
|
||||||
@ -1369,23 +1460,80 @@ pub const CAPI = struct {
|
|||||||
return surface.core_surface.hasSelection();
|
return surface.core_surface.hasSelection();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Copies the surface selection text into the provided buffer and
|
/// Same as ghostty_surface_read_text but reads from the user selection,
|
||||||
/// returns the copied size. If the buffer is too small, there is no
|
/// if any.
|
||||||
/// selection, or there is an error, then 0 is returned.
|
export fn ghostty_surface_read_selection(
|
||||||
export fn ghostty_surface_selection(surface: *Surface, buf: [*]u8, cap: usize) usize {
|
surface: *Surface,
|
||||||
const selection_ = surface.core_surface.selectionString(global.alloc) catch |err| {
|
result: *Text,
|
||||||
log.warn("error getting selection err={}", .{err});
|
) bool {
|
||||||
return 0;
|
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.
|
const vp: CoreSurface.Text.Viewport = text.viewport orelse .{
|
||||||
if (selection.len > cap) return 0;
|
.tl_px_x = -1,
|
||||||
|
.tl_px_y = -1,
|
||||||
|
.offset_start = 0,
|
||||||
|
.offset_len = 0,
|
||||||
|
};
|
||||||
|
|
||||||
// Copy into the buffer and return the length
|
result.* = .{
|
||||||
@memcpy(buf[0..selection.len], selection);
|
.tl_px_x = vp.tl_px_x,
|
||||||
return selection.len;
|
.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
|
/// Tell the surface that it needs to schedule a render
|
||||||
@ -1888,21 +2036,12 @@ pub const CAPI = struct {
|
|||||||
/// This does not modify the selection active on the surface (if any).
|
/// This does not modify the selection active on the surface (if any).
|
||||||
export fn ghostty_surface_quicklook_word(
|
export fn ghostty_surface_quicklook_word(
|
||||||
ptr: *Surface,
|
ptr: *Surface,
|
||||||
buf: [*]u8,
|
result: *Text,
|
||||||
cap: usize,
|
) bool {
|
||||||
info: *Selection,
|
|
||||||
) usize {
|
|
||||||
const surface = &ptr.core_surface;
|
const surface = &ptr.core_surface;
|
||||||
surface.renderer_state.mutex.lock();
|
surface.renderer_state.mutex.lock();
|
||||||
defer surface.renderer_state.mutex.unlock();
|
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
|
// Get our word selection
|
||||||
const sel = sel: {
|
const sel = sel: {
|
||||||
const screen = &surface.renderer_state.terminal.screen;
|
const screen = &surface.renderer_state.terminal.screen;
|
||||||
@ -1915,45 +2054,13 @@ pub const CAPI = struct {
|
|||||||
},
|
},
|
||||||
}) orelse {
|
}) orelse {
|
||||||
if (comptime std.debug.runtime_safety) unreachable;
|
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
|
// Read the selection
|
||||||
surface.io.terminal.screen.selection = sel;
|
return readTextLocked(ptr, sel, result);
|
||||||
|
|
||||||
// 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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export fn ghostty_inspector_metal_init(ptr: *Inspector, device: objc.c.id) bool {
|
export fn ghostty_inspector_metal_init(ptr: *Inspector, device: objc.c.id) bool {
|
||||||
|
@ -15,10 +15,8 @@
|
|||||||
# You should have received a copy of the GNU General Public License
|
# You should have received a copy of the GNU General Public License
|
||||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
# We need to be in interactive mode and we need to have the Ghostty
|
# We need to be in interactive mode to proceed.
|
||||||
# resources dir set which also tells us we're running in Ghostty.
|
|
||||||
if [[ "$-" != *i* ]] ; then builtin return; fi
|
if [[ "$-" != *i* ]] ; then builtin return; fi
|
||||||
if [ -z "$GHOSTTY_RESOURCES_DIR" ]; then builtin return; fi
|
|
||||||
|
|
||||||
# When automatic shell integration is active, we were started in POSIX
|
# When automatic shell integration is active, we were started in POSIX
|
||||||
# mode and need to manually recreate the bash startup sequence.
|
# mode and need to manually recreate the bash startup sequence.
|
||||||
@ -98,7 +96,7 @@ if [[ "$GHOSTTY_SHELL_FEATURES" == *"sudo"* && -n "$TERMINFO" ]]; then
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
# Import bash-preexec, safe to do multiple times
|
# Import bash-preexec, safe to do multiple times
|
||||||
builtin source "$GHOSTTY_RESOURCES_DIR/shell-integration/bash/bash-preexec.sh"
|
builtin source "$(dirname -- "${BASH_SOURCE[0]}")/bash-preexec.sh"
|
||||||
|
|
||||||
# This is set to 1 when we're executing a command so that we don't
|
# This is set to 1 when we're executing a command so that we don't
|
||||||
# send prompt marks multiple times.
|
# send prompt marks multiple times.
|
||||||
|
@ -35,6 +35,7 @@ pub const Page = page.Page;
|
|||||||
pub const PageList = @import("PageList.zig");
|
pub const PageList = @import("PageList.zig");
|
||||||
pub const Parser = @import("Parser.zig");
|
pub const Parser = @import("Parser.zig");
|
||||||
pub const Pin = PageList.Pin;
|
pub const Pin = PageList.Pin;
|
||||||
|
pub const Point = point.Point;
|
||||||
pub const Screen = @import("Screen.zig");
|
pub const Screen = @import("Screen.zig");
|
||||||
pub const ScreenType = Terminal.ScreenType;
|
pub const ScreenType = Terminal.ScreenType;
|
||||||
pub const Selection = @import("Selection.zig");
|
pub const Selection = @import("Selection.zig");
|
||||||
|
Reference in New Issue
Block a user