macos: readonly badge
parent
dc7bc3014e
commit
ec2638b3c6
|
|
@ -588,6 +588,9 @@ extension Ghostty {
|
||||||
case GHOSTTY_ACTION_RING_BELL:
|
case GHOSTTY_ACTION_RING_BELL:
|
||||||
ringBell(app, target: target)
|
ringBell(app, target: target)
|
||||||
|
|
||||||
|
case GHOSTTY_ACTION_READONLY:
|
||||||
|
setReadonly(app, target: target, v: action.action.readonly)
|
||||||
|
|
||||||
case GHOSTTY_ACTION_CHECK_FOR_UPDATES:
|
case GHOSTTY_ACTION_CHECK_FOR_UPDATES:
|
||||||
checkForUpdates(app)
|
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(
|
private static func moveTab(
|
||||||
_ app: ghostty_app_t,
|
_ app: ghostty_app_t,
|
||||||
target: ghostty_target_s,
|
target: ghostty_target_s,
|
||||||
|
|
|
||||||
|
|
@ -391,6 +391,10 @@ extension Notification.Name {
|
||||||
|
|
||||||
/// Ring the bell
|
/// Ring the bell
|
||||||
static let ghosttyBellDidRing = Notification.Name("com.mitchellh.ghostty.ghosttyBellDidRing")
|
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")
|
static let ghosttyCommandPaletteDidToggle = Notification.Name("com.mitchellh.ghostty.commandPaletteDidToggle")
|
||||||
|
|
||||||
/// Toggle maximize of current window
|
/// Toggle maximize of current window
|
||||||
|
|
|
||||||
|
|
@ -104,6 +104,11 @@ extension Ghostty {
|
||||||
}
|
}
|
||||||
.ghosttySurfaceView(surfaceView)
|
.ghosttySurfaceView(surfaceView)
|
||||||
|
|
||||||
|
// Readonly indicator badge
|
||||||
|
if surfaceView.readonly {
|
||||||
|
ReadonlyBadge()
|
||||||
|
}
|
||||||
|
|
||||||
// Progress report
|
// Progress report
|
||||||
if let progressReport = surfaceView.progressReport, progressReport.state != .remove {
|
if let progressReport = surfaceView.progressReport, progressReport.state != .remove {
|
||||||
VStack(spacing: 0) {
|
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)
|
#if canImport(AppKit)
|
||||||
/// When changing the split state, or going full screen (native or non), the terminal view
|
/// 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
|
/// will lose focus. There has to be some nice SwiftUI-native way to fix this but I can't
|
||||||
|
|
|
||||||
|
|
@ -123,6 +123,9 @@ extension Ghostty {
|
||||||
/// True when the bell is active. This is set inactive on focus or event.
|
/// True when the bell is active. This is set inactive on focus or event.
|
||||||
@Published private(set) var bell: Bool = false
|
@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
|
// An initial size to request for a window. This will only affect
|
||||||
// then the view is moved to a new window.
|
// then the view is moved to a new window.
|
||||||
var initialSize: NSSize? = nil
|
var initialSize: NSSize? = nil
|
||||||
|
|
@ -333,6 +336,11 @@ extension Ghostty {
|
||||||
selector: #selector(ghosttyBellDidRing(_:)),
|
selector: #selector(ghosttyBellDidRing(_:)),
|
||||||
name: .ghosttyBellDidRing,
|
name: .ghosttyBellDidRing,
|
||||||
object: self)
|
object: self)
|
||||||
|
center.addObserver(
|
||||||
|
self,
|
||||||
|
selector: #selector(ghosttyDidChangeReadonly(_:)),
|
||||||
|
name: .ghosttyDidChangeReadonly,
|
||||||
|
object: self)
|
||||||
center.addObserver(
|
center.addObserver(
|
||||||
self,
|
self,
|
||||||
selector: #selector(windowDidChangeScreen),
|
selector: #selector(windowDidChangeScreen),
|
||||||
|
|
@ -703,6 +711,11 @@ extension Ghostty {
|
||||||
bell = true
|
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) {
|
@objc private func windowDidChangeScreen(notification: SwiftUI.Notification) {
|
||||||
guard let window = self.window else { return }
|
guard let window = self.window else { return }
|
||||||
guard let object = notification.object as? NSWindow, window == object else { return }
|
guard let object = notification.object as? NSWindow, window == object else { return }
|
||||||
|
|
|
||||||
|
|
@ -43,6 +43,9 @@ extension Ghostty {
|
||||||
|
|
||||||
// The current search state. When non-nil, the search overlay should be shown.
|
// The current search state. When non-nil, the search overlay should be shown.
|
||||||
@Published var searchState: SearchState? = nil
|
@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
|
// Returns sizing information for the surface. This is the raw C
|
||||||
// structure because I'm lazy.
|
// structure because I'm lazy.
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue