macos: readonly badge

pull/9130/head
Mitchell Hashimoto 2025-12-12 13:41:32 -08:00
parent dc7bc3014e
commit ec2638b3c6
No known key found for this signature in database
GPG Key ID: 523D5DC389D273BC
5 changed files with 95 additions and 0 deletions

View File

@ -588,6 +588,9 @@ extension Ghostty {
case GHOSTTY_ACTION_RING_BELL:
ringBell(app, target: target)
case GHOSTTY_ACTION_READONLY:
setReadonly(app, target: target, v: action.action.readonly)
case GHOSTTY_ACTION_CHECK_FOR_UPDATES:
checkForUpdates(app)
@ -1010,6 +1013,31 @@ extension Ghostty {
}
}
private static func setReadonly(
_ app: ghostty_app_t,
target: ghostty_target_s,
v: ghostty_action_readonly_e) {
switch (target.tag) {
case GHOSTTY_TARGET_APP:
Ghostty.logger.warning("set readonly does nothing with an app target")
return
case GHOSTTY_TARGET_SURFACE:
guard let surface = target.target.surface else { return }
guard let surfaceView = self.surfaceView(from: surface) else { return }
NotificationCenter.default.post(
name: .ghosttyDidChangeReadonly,
object: surfaceView,
userInfo: [
SwiftUI.Notification.Name.ReadonlyKey: v == GHOSTTY_READONLY_ON,
]
)
default:
assertionFailure()
}
}
private static func moveTab(
_ app: ghostty_app_t,
target: ghostty_target_s,

View File

@ -391,6 +391,10 @@ extension Notification.Name {
/// Ring the bell
static let ghosttyBellDidRing = Notification.Name("com.mitchellh.ghostty.ghosttyBellDidRing")
/// Readonly mode changed
static let ghosttyDidChangeReadonly = Notification.Name("com.mitchellh.ghostty.didChangeReadonly")
static let ReadonlyKey = ghosttyDidChangeReadonly.rawValue + ".readonly"
static let ghosttyCommandPaletteDidToggle = Notification.Name("com.mitchellh.ghostty.commandPaletteDidToggle")
/// Toggle maximize of current window

View File

@ -104,6 +104,11 @@ extension Ghostty {
}
.ghosttySurfaceView(surfaceView)
// Readonly indicator badge
if surfaceView.readonly {
ReadonlyBadge()
}
// Progress report
if let progressReport = surfaceView.progressReport, progressReport.state != .remove {
VStack(spacing: 0) {
@ -757,6 +762,48 @@ extension Ghostty {
}
}
// MARK: Readonly Badge
/// A badge overlay that indicates a surface is in readonly mode.
/// Positioned in the top-right corner and styled to be noticeable but unobtrusive.
struct ReadonlyBadge: View {
private let badgeColor = Color(hue: 0.08, saturation: 0.5, brightness: 0.8)
var body: some View {
VStack {
HStack {
Spacer()
HStack(spacing: 5) {
Image(systemName: "eye.fill")
.font(.system(size: 12))
Text("Read-only")
.font(.system(size: 12, weight: .medium))
}
.padding(.horizontal, 8)
.padding(.vertical, 4)
.background(badgeBackground)
.foregroundStyle(badgeColor)
}
.padding(8)
Spacer()
}
.allowsHitTesting(false)
.accessibilityElement(children: .ignore)
.accessibilityLabel("Read-only terminal")
}
private var badgeBackground: some View {
RoundedRectangle(cornerRadius: 6)
.fill(.regularMaterial)
.overlay(
RoundedRectangle(cornerRadius: 6)
.strokeBorder(Color.orange.opacity(0.6), lineWidth: 1.5)
)
}
}
#if canImport(AppKit)
/// When changing the split state, or going full screen (native or non), the terminal view
/// will lose focus. There has to be some nice SwiftUI-native way to fix this but I can't

View File

@ -123,6 +123,9 @@ extension Ghostty {
/// True when the bell is active. This is set inactive on focus or event.
@Published private(set) var bell: Bool = false
/// True when the surface is in readonly mode.
@Published private(set) var readonly: Bool = false
// An initial size to request for a window. This will only affect
// then the view is moved to a new window.
var initialSize: NSSize? = nil
@ -333,6 +336,11 @@ extension Ghostty {
selector: #selector(ghosttyBellDidRing(_:)),
name: .ghosttyBellDidRing,
object: self)
center.addObserver(
self,
selector: #selector(ghosttyDidChangeReadonly(_:)),
name: .ghosttyDidChangeReadonly,
object: self)
center.addObserver(
self,
selector: #selector(windowDidChangeScreen),
@ -703,6 +711,11 @@ extension Ghostty {
bell = true
}
@objc private func ghosttyDidChangeReadonly(_ notification: SwiftUI.Notification) {
guard let value = notification.userInfo?[SwiftUI.Notification.Name.ReadonlyKey] as? Bool else { return }
readonly = value
}
@objc private func windowDidChangeScreen(notification: SwiftUI.Notification) {
guard let window = self.window else { return }
guard let object = notification.object as? NSWindow, window == object else { return }

View File

@ -43,6 +43,9 @@ extension Ghostty {
// The current search state. When non-nil, the search overlay should be shown.
@Published var searchState: SearchState? = nil
/// True when the surface is in readonly mode.
@Published private(set) var readonly: Bool = false
// Returns sizing information for the surface. This is the raw C
// structure because I'm lazy.