diff --git a/Sources/GhosttyTerminalView.swift b/Sources/GhosttyTerminalView.swift index 4e2139e4..a2091c66 100644 --- a/Sources/GhosttyTerminalView.swift +++ b/Sources/GhosttyTerminalView.swift @@ -378,11 +378,27 @@ enum TerminalKeyboardCopyModeResolution: Equatable { } private let terminalKeyboardCopyModeMaxCount = 9_999 +private let terminalKeyboardCopyModeIndicatorText = "vim" private func terminalKeyboardCopyModeClampCount(_ value: Int) -> Int { min(max(value, 1), terminalKeyboardCopyModeMaxCount) } +private func terminalKeyTableIndicatorText(_ name: String) -> String { + let trimmed = name.trimmingCharacters(in: .whitespacesAndNewlines) + switch trimmed.lowercased() { + case "", "set": + return "key table" + case "vi", "vim": + return terminalKeyboardCopyModeIndicatorText + default: + let normalized = trimmed + .replacingOccurrences(of: "_", with: " ") + .replacingOccurrences(of: "-", with: " ") + return normalized.isEmpty ? "key table" : normalized + } +} + func terminalKeyboardCopyModeInitialViewportRow( rows: Int, imePointY: Double, @@ -2090,6 +2106,7 @@ final class TerminalSurface: Identifiable, ObservableObject { } @Published private(set) var keyboardCopyModeActive: Bool = false private var searchNeedleCancellable: AnyCancellable? + var currentKeyStateIndicatorText: String? { surfaceView.currentKeyStateIndicatorText } init( tabId: UUID, @@ -2812,7 +2829,7 @@ final class TerminalSurface: Identifiable, ObservableObject { if keyboardCopyModeActive != active { keyboardCopyModeActive = active } - hostedView.setKeyboardCopyModeIndicator(visible: active) + hostedView.syncKeyStateIndicator(text: surfaceView.currentKeyStateIndicatorText) } func hasSelection() -> Bool { @@ -2934,6 +2951,17 @@ class GhosttyNSView: NSView, NSUserInterfaceValidations { /// movements should extend the selection (visual) or scroll the viewport. private var keyboardCopyModeVisualActive = false fileprivate var isKeyboardCopyModeActive: Bool { keyboardCopyModeActive } + fileprivate var currentKeyStateIndicatorText: String? { + if let name = keyTables.last { + return terminalKeyTableIndicatorText(name) + } + + if keyboardCopyModeActive { + return terminalKeyboardCopyModeIndicatorText + } + + return nil + } #if DEBUG private static let keyLatencyProbeEnabled: Bool = { if ProcessInfo.processInfo.environment["CMUX_KEY_LATENCY_PROBE"] == "1" { @@ -4415,6 +4443,8 @@ class GhosttyNSView: NSView, NSUserInterfaceValidations { default: break } + + terminalSurface?.hostedView.syncKeyStateIndicator(text: currentKeyStateIndicatorText) } // MARK: - Mouse Handling @@ -4917,6 +4947,7 @@ final class GhosttySurfaceScrollView: NSView { private let flashOverlayView: GhosttyFlashOverlayView private let flashLayer: CAShapeLayer private let keyboardCopyModeBadgeView: GhosttyPassthroughVisualEffectView + private let keyboardCopyModeBadgeIconView: NSImageView private let keyboardCopyModeBadgeLabel: NSTextField private var searchOverlayHostingView: NSHostingView? private var lastSearchOverlayStateID: ObjectIdentifier? @@ -5085,7 +5116,8 @@ final class GhosttySurfaceScrollView: NSView { flashOverlayView = GhosttyFlashOverlayView(frame: .zero) flashLayer = CAShapeLayer() keyboardCopyModeBadgeView = GhosttyPassthroughVisualEffectView(frame: .zero) - keyboardCopyModeBadgeLabel = NSTextField(labelWithString: "VI MODE") + keyboardCopyModeBadgeIconView = NSImageView(frame: .zero) + keyboardCopyModeBadgeLabel = NSTextField(labelWithString: terminalKeyboardCopyModeIndicatorText) scrollView.hasVerticalScroller = true scrollView.hasHorizontalScroller = false scrollView.autohidesScrollers = false @@ -5163,20 +5195,41 @@ final class GhosttySurfaceScrollView: NSView { keyboardCopyModeBadgeView.material = .hudWindow keyboardCopyModeBadgeView.blendingMode = .withinWindow keyboardCopyModeBadgeView.state = .active - keyboardCopyModeBadgeView.layer?.cornerRadius = 7 + keyboardCopyModeBadgeView.layer?.cornerRadius = 18 keyboardCopyModeBadgeView.layer?.masksToBounds = true keyboardCopyModeBadgeView.layer?.borderWidth = 1 - keyboardCopyModeBadgeView.layer?.borderColor = cmuxAccentNSColor().withAlphaComponent(0.45).cgColor + keyboardCopyModeBadgeView.layer?.borderColor = NSColor.white.withAlphaComponent(0.12).cgColor + keyboardCopyModeBadgeView.layer?.shadowColor = NSColor.black.cgColor + keyboardCopyModeBadgeView.layer?.shadowOpacity = 0.22 + keyboardCopyModeBadgeView.layer?.shadowRadius = 10 + keyboardCopyModeBadgeView.layer?.shadowOffset = CGSize(width: 0, height: 2) keyboardCopyModeBadgeView.alphaValue = 0.97 + keyboardCopyModeBadgeIconView.translatesAutoresizingMaskIntoConstraints = false + keyboardCopyModeBadgeIconView.symbolConfiguration = NSImage.SymbolConfiguration( + pointSize: 13, + weight: .regular, + scale: .medium + ) + keyboardCopyModeBadgeIconView.image = NSImage( + systemSymbolName: "keyboard.badge.ellipsis", + accessibilityDescription: "Key table" + ) + keyboardCopyModeBadgeIconView.contentTintColor = NSColor.secondaryLabelColor keyboardCopyModeBadgeLabel.translatesAutoresizingMaskIntoConstraints = false keyboardCopyModeBadgeLabel.textColor = NSColor.labelColor + keyboardCopyModeBadgeLabel.font = NSFont.systemFont(ofSize: 13, weight: .semibold) keyboardCopyModeBadgeLabel.lineBreakMode = .byClipping + keyboardCopyModeBadgeView.addSubview(keyboardCopyModeBadgeIconView) keyboardCopyModeBadgeView.addSubview(keyboardCopyModeBadgeLabel) NSLayoutConstraint.activate([ - keyboardCopyModeBadgeLabel.leadingAnchor.constraint(equalTo: keyboardCopyModeBadgeView.leadingAnchor, constant: 8), - keyboardCopyModeBadgeLabel.trailingAnchor.constraint(equalTo: keyboardCopyModeBadgeView.trailingAnchor, constant: -8), - keyboardCopyModeBadgeLabel.topAnchor.constraint(equalTo: keyboardCopyModeBadgeView.topAnchor, constant: 4), - keyboardCopyModeBadgeLabel.bottomAnchor.constraint(equalTo: keyboardCopyModeBadgeView.bottomAnchor, constant: -4), + keyboardCopyModeBadgeIconView.leadingAnchor.constraint(equalTo: keyboardCopyModeBadgeView.leadingAnchor, constant: 12), + keyboardCopyModeBadgeIconView.centerYAnchor.constraint(equalTo: keyboardCopyModeBadgeView.centerYAnchor), + keyboardCopyModeBadgeIconView.widthAnchor.constraint(equalToConstant: 18), + keyboardCopyModeBadgeIconView.heightAnchor.constraint(equalToConstant: 18), + keyboardCopyModeBadgeLabel.leadingAnchor.constraint(equalTo: keyboardCopyModeBadgeIconView.trailingAnchor, constant: 7), + keyboardCopyModeBadgeLabel.trailingAnchor.constraint(equalTo: keyboardCopyModeBadgeView.trailingAnchor, constant: -14), + keyboardCopyModeBadgeLabel.topAnchor.constraint(equalTo: keyboardCopyModeBadgeView.topAnchor, constant: 8), + keyboardCopyModeBadgeLabel.bottomAnchor.constraint(equalTo: keyboardCopyModeBadgeView.bottomAnchor, constant: -8), ]) keyboardCopyModeBadgeView.isHidden = true addSubview(keyboardCopyModeBadgeView) @@ -5603,22 +5656,26 @@ final class GhosttySurfaceScrollView: NSView { lastSearchOverlayStateID = searchStateID } - func setKeyboardCopyModeIndicator(visible: Bool) { + func syncKeyStateIndicator(text: String?) { if !Thread.isMainThread { DispatchQueue.main.async { [weak self] in - self?.setKeyboardCopyModeIndicator(visible: visible) + self?.syncKeyStateIndicator(text: text) } return } - keyboardCopyModeBadgeView.isHidden = !visible - if visible { + if let text, !text.isEmpty { + keyboardCopyModeBadgeLabel.stringValue = text + keyboardCopyModeBadgeView.isHidden = false if let overlay = searchOverlayHostingView { addSubview(keyboardCopyModeBadgeView, positioned: .above, relativeTo: overlay) } else { addSubview(keyboardCopyModeBadgeView, positioned: .above, relativeTo: nil) } + return } + + keyboardCopyModeBadgeView.isHidden = true } private func dropZoneOverlayFrame(for zone: DropZone, in size: CGSize) -> CGRect { @@ -6947,7 +7004,7 @@ struct GhosttyTerminalView: NSViewRepresentable { ) hostedView.setNotificationRing(visible: showsUnreadNotificationRing) hostedView.setSearchOverlay(searchState: searchState) - hostedView.setKeyboardCopyModeIndicator(visible: terminalSurface.keyboardCopyModeActive) + hostedView.syncKeyStateIndicator(text: terminalSurface.currentKeyStateIndicatorText) hostedView.setFocusHandler { onFocus?(terminalSurface.id) } hostedView.setTriggerFlashHandler(onTriggerFlash) let portalExpectedSurfaceId = terminalSurface.id