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
53 changes: 53 additions & 0 deletions Sources/GhosttyTerminalView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11707,6 +11707,59 @@ extension GhosttyNSView: NSTextInputClient {
}
}

// MARK: - Services

extension GhosttyNSView: NSServicesMenuRequestor {
override func validRequestor(
forSendType sendType: NSPasteboard.PasteboardType?,
returnType: NSPasteboard.PasteboardType?
) -> Any? {
let supportedTypes: [NSPasteboard.PasteboardType] = [
.string,
.init("public.utf8-plain-text")
]
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Duplicate entries in supported pasteboard types array

Low Severity

NSPasteboard.PasteboardType.string has the raw value "public.utf8-plain-text", so .string and .init("public.utf8-plain-text") are the same type. The supportedTypes array contains two identical entries. If the intent was to also cover the legacy Objective-C "NSStringPboardType" pasteboard type, the wrong string was used. The codebase already defines a constant for this at GhosttyPasteboardHelper.utf8PlainTextType.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 3340e71. Configure here.


if (returnType == nil || supportedTypes.contains(returnType!)) &&
(sendType == nil || supportedTypes.contains(sendType!)) {
if let sendType, supportedTypes.contains(sendType) {
guard let surface, ghostty_surface_has_selection(surface) else {
return super.validRequestor(forSendType: sendType, returnType: returnType)
}
}
Comment on lines +11722 to +11728
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Redundant inner type check in validRequestor

The supportedTypes.contains(sendType) condition inside the if let sendType binding is always true at that point — the outer if already guarantees sendType == nil || supportedTypes.contains(sendType!), and since sendType is non-nil here (unwrapped by if let), it is already in supportedTypes.

Suggested change
if (returnType == nil || supportedTypes.contains(returnType!)) &&
(sendType == nil || supportedTypes.contains(sendType!)) {
if let sendType, supportedTypes.contains(sendType) {
guard let surface, ghostty_surface_has_selection(surface) else {
return super.validRequestor(forSendType: sendType, returnType: returnType)
}
}
if (returnType == nil || supportedTypes.contains(returnType!)) &&
(sendType == nil || supportedTypes.contains(sendType!)) {
if let sendType {
guard let surface, ghostty_surface_has_selection(surface) else {
return super.validRequestor(forSendType: sendType, returnType: returnType)
}
}
return self
}


return self
}

return super.validRequestor(forSendType: sendType, returnType: returnType)
}

func writeSelection(
to pboard: NSPasteboard,
types: [NSPasteboard.PasteboardType]
) -> Bool {
guard let snapshot = readSelectionSnapshot() else { return false }

pboard.declareTypes([.string], owner: nil)
pboard.setString(snapshot.string, forType: .string)
return true
}
Comment on lines +11736 to +11745
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 writeSelection ignores the types parameter

The NSServicesMenuRequestor protocol contract requires writing only the types listed in types that the object supports, but the parameter is silently ignored and .string is always written. This is harmless in practice since validRequestor already gates on supported types, but it departs from the protocol contract.

Suggested change
func writeSelection(
to pboard: NSPasteboard,
types: [NSPasteboard.PasteboardType]
) -> Bool {
guard let snapshot = readSelectionSnapshot() else { return false }
pboard.declareTypes([.string], owner: nil)
pboard.setString(snapshot.string, forType: .string)
return true
}
func writeSelection(
to pboard: NSPasteboard,
types: [NSPasteboard.PasteboardType]
) -> Bool {
let supportedTypes: [NSPasteboard.PasteboardType] = [.string, .init("public.utf8-plain-text")]
guard types.isEmpty || types.contains(where: { supportedTypes.contains($0) }) else { return false }
guard let snapshot = readSelectionSnapshot() else { return false }
pboard.declareTypes([.string], owner: nil)
pboard.setString(snapshot.string, forType: .string)
return true
}


func readSelection(from pboard: NSPasteboard) -> Bool {
guard ensureSurfaceReadyForInput() != nil else { return false }
guard let string = GhosttyPasteboardHelper.stringContents(from: pboard) else { return false }
guard !string.isEmpty else { return true }

// Services can return multiline payloads. Route them through the text/paste path
// so Ghostty handles them as pasted text instead of synthesizing Return/Tab keys.
if let terminalSurface {
terminalSurface.sendText(string)
return true
}

return false
}
}

// MARK: - SwiftUI Wrapper

struct GhosttyTerminalView: NSViewRepresentable {
Expand Down
31 changes: 14 additions & 17 deletions Sources/Workspace.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6783,41 +6783,39 @@ final class Workspace: Identifiable, ObservableObject {
}

func applyGhosttyChrome(from config: GhosttyConfig, reason: String = "unspecified") {
let nextHex = Self.bonsplitChromeHex(
backgroundColor: config.backgroundColor,
backgroundOpacity: config.backgroundOpacity
)
let nextTabTitleFontSize = config.surfaceTabBarFontSize
let nextAppearance = Self.bonsplitAppearance(from: config)
let currentAppearance = bonsplitController.configuration.appearance
let nextBackgroundHex = nextAppearance.chromeColors.backgroundHex
let currentBackgroundHex = currentAppearance.chromeColors.backgroundHex
let currentTabTitleFontSize = currentAppearance.tabTitleFontSize
let backgroundChanged = currentBackgroundHex != nextHex
let fontSizeChanged = abs(currentTabTitleFontSize - nextTabTitleFontSize) > 0.0001
let backgroundChanged = currentBackgroundHex != nextBackgroundHex
let fontSizeChanged = abs(currentAppearance.tabTitleFontSize - nextAppearance.tabTitleFontSize) > 0.001
let isNoOp = !backgroundChanged && !fontSizeChanged

if GhosttyApp.shared.backgroundLogEnabled {
GhosttyApp.shared.logBackground(
"theme apply workspace=\(id.uuidString) reason=\(reason) " +
"currentBg=\(currentBackgroundHex ?? "nil") nextBg=\(nextHex) " +
"currentTabFont=\(String(format: "%.3f", currentTabTitleFontSize)) " +
"nextTabFont=\(String(format: "%.3f", nextTabTitleFontSize)) noop=\(isNoOp)"
"currentBg=\(currentBackgroundHex ?? "nil") nextBg=\(nextBackgroundHex ?? "nil") " +
"currentTabFont=\(String(format: "%.2f", currentAppearance.tabTitleFontSize)) " +
"nextTabFont=\(String(format: "%.2f", nextAppearance.tabTitleFontSize)) " +
"noop=\(isNoOp)"
)
}

guard !isNoOp else { return }

if backgroundChanged {
bonsplitController.configuration.appearance.chromeColors.backgroundHex = nextHex
bonsplitController.configuration.appearance.chromeColors.backgroundHex = nextBackgroundHex
}

if fontSizeChanged {
bonsplitController.configuration.appearance.tabTitleFontSize = nextTabTitleFontSize
bonsplitController.configuration.appearance.tabTitleFontSize = nextAppearance.tabTitleFontSize
}

if GhosttyApp.shared.backgroundLogEnabled {
GhosttyApp.shared.logBackground(
"theme applied workspace=\(id.uuidString) reason=\(reason) " +
"resultingBg=\(bonsplitController.configuration.appearance.chromeColors.backgroundHex ?? "nil") " +
"resultingTabFont=\(String(format: "%.3f", bonsplitController.configuration.appearance.tabTitleFontSize))"
"resultingTabFont=\(String(format: "%.2f", bonsplitController.configuration.appearance.tabTitleFontSize))"
)
}
}
Expand Down Expand Up @@ -6870,9 +6868,8 @@ final class Workspace: Identifiable, ObservableObject {
: FileManager.default.homeDirectoryForCurrentUser.path

// Configure bonsplit with keepAllAlive to preserve terminal state
// and keep split entry instantaneous.
// Use the cached Ghostty config so new workspaces inherit tab-strip sizing
// without paying repeated parse costs on the workspace-creation hot path.
// and keep split entry instantaneous. Use the cached Ghostty config so
// new workspaces inherit tab-strip sizing before the first onAppear refresh.
let initialSurfaceTabBarFontSize = GhosttyConfig.load().surfaceTabBarFontSize
let appearance = Self.bonsplitAppearance(
from: GhosttyApp.shared.defaultBackgroundColor,
Expand Down
Loading