mirror of
https://github.com/ghostty-org/ghostty.git
synced 2025-04-20 00:18:53 +03:00
1419 lines
54 KiB
Swift
1419 lines
54 KiB
Swift
import SwiftUI
|
|
import UserNotifications
|
|
import GhosttyKit
|
|
|
|
protocol GhosttyAppDelegate: AnyObject {
|
|
#if os(macOS)
|
|
/// Called when a callback needs access to a specific surface. This should return nil
|
|
/// when the surface is no longer valid.
|
|
func findSurface(forUUID uuid: UUID) -> Ghostty.SurfaceView?
|
|
#endif
|
|
}
|
|
|
|
extension Ghostty {
|
|
// IMPORTANT: THIS IS NOT DONE.
|
|
// This is a refactor/redo of Ghostty.AppState so that it supports both macOS and iOS
|
|
class App: ObservableObject {
|
|
enum Readiness: String {
|
|
case loading, error, ready
|
|
}
|
|
|
|
/// Optional delegate
|
|
weak var delegate: GhosttyAppDelegate?
|
|
|
|
/// The readiness value of the state.
|
|
@Published var readiness: Readiness = .loading
|
|
|
|
/// The global app configuration. This defines the app level configuration plus any behavior
|
|
/// for new windows, tabs, etc. Note that when creating a new window, it may inherit some
|
|
/// configuration (i.e. font size) from the previously focused window. This would override this.
|
|
@Published private(set) var config: Config
|
|
|
|
/// The ghostty app instance. We only have one of these for the entire app, although I guess
|
|
/// in theory you can have multiple... I don't know why you would...
|
|
@Published var app: ghostty_app_t? = nil {
|
|
didSet {
|
|
guard let old = oldValue else { return }
|
|
ghostty_app_free(old)
|
|
}
|
|
}
|
|
|
|
/// True if we need to confirm before quitting.
|
|
var needsConfirmQuit: Bool {
|
|
guard let app = app else { return false }
|
|
return ghostty_app_needs_confirm_quit(app)
|
|
}
|
|
|
|
init() {
|
|
// Initialize ghostty global state. This happens once per process.
|
|
if ghostty_init() != GHOSTTY_SUCCESS {
|
|
logger.critical("ghostty_init failed, weird things may happen")
|
|
readiness = .error
|
|
}
|
|
|
|
// Initialize the global configuration.
|
|
self.config = Config()
|
|
if self.config.config == nil {
|
|
readiness = .error
|
|
return
|
|
}
|
|
|
|
// Create our "runtime" config. The "runtime" is the configuration that ghostty
|
|
// uses to interface with the application runtime environment.
|
|
var runtime_cfg = ghostty_runtime_config_s(
|
|
userdata: Unmanaged.passUnretained(self).toOpaque(),
|
|
supports_selection_clipboard: true,
|
|
wakeup_cb: { userdata in App.wakeup(userdata) },
|
|
action_cb: { app, target, action in App.action(app!, target: target, action: action) },
|
|
read_clipboard_cb: { userdata, loc, state in App.readClipboard(userdata, location: loc, state: state) },
|
|
confirm_read_clipboard_cb: { userdata, str, state, request in App.confirmReadClipboard(userdata, string: str, state: state, request: request ) },
|
|
write_clipboard_cb: { userdata, str, loc, confirm in App.writeClipboard(userdata, string: str, location: loc, confirm: confirm) },
|
|
close_surface_cb: { userdata, processAlive in App.closeSurface(userdata, processAlive: processAlive) }
|
|
)
|
|
|
|
// Create the ghostty app.
|
|
guard let app = ghostty_app_new(&runtime_cfg, config.config) else {
|
|
logger.critical("ghostty_app_new failed")
|
|
readiness = .error
|
|
return
|
|
}
|
|
self.app = app
|
|
|
|
#if os(macOS)
|
|
// Set our initial focus state
|
|
ghostty_app_set_focus(app, NSApp.isActive)
|
|
|
|
let center = NotificationCenter.default
|
|
center.addObserver(
|
|
self,
|
|
selector: #selector(keyboardSelectionDidChange(notification:)),
|
|
name: NSTextInputContext.keyboardSelectionDidChangeNotification,
|
|
object: nil)
|
|
center.addObserver(
|
|
self,
|
|
selector: #selector(applicationDidBecomeActive(notification:)),
|
|
name: NSApplication.didBecomeActiveNotification,
|
|
object: nil)
|
|
center.addObserver(
|
|
self,
|
|
selector: #selector(applicationDidResignActive(notification:)),
|
|
name: NSApplication.didResignActiveNotification,
|
|
object: nil)
|
|
#endif
|
|
|
|
self.readiness = .ready
|
|
}
|
|
|
|
deinit {
|
|
// This will force the didSet callbacks to run which free.
|
|
self.app = nil
|
|
|
|
#if os(macOS)
|
|
NotificationCenter.default.removeObserver(self)
|
|
#endif
|
|
}
|
|
|
|
// MARK: App Operations
|
|
|
|
func appTick() {
|
|
guard let app = self.app else { return }
|
|
ghostty_app_tick(app)
|
|
}
|
|
|
|
func openConfig() {
|
|
guard let app = self.app else { return }
|
|
ghostty_app_open_config(app)
|
|
}
|
|
|
|
/// Reload the configuration.
|
|
func reloadConfig(soft: Bool = false) {
|
|
guard let app = self.app else { return }
|
|
|
|
// Soft updates just call with our existing config
|
|
if (soft) {
|
|
ghostty_app_update_config(app, config.config!)
|
|
return
|
|
}
|
|
|
|
// Hard or full updates have to reload the full configuration
|
|
let newConfig = Config()
|
|
guard newConfig.loaded else {
|
|
Ghostty.logger.warning("failed to reload configuration")
|
|
return
|
|
}
|
|
|
|
ghostty_app_update_config(app, newConfig.config!)
|
|
|
|
// We can only set our config after updating it so that we don't free
|
|
// memory that may still be in use
|
|
self.config = newConfig
|
|
}
|
|
|
|
func reloadConfig(surface: ghostty_surface_t, soft: Bool = false) {
|
|
// Soft updates just call with our existing config
|
|
if (soft) {
|
|
ghostty_surface_update_config(surface, config.config!)
|
|
return
|
|
}
|
|
|
|
// Hard or full updates have to reload the full configuration.
|
|
// NOTE: We never set this on self.config because this is a surface-only
|
|
// config. We free it after the call.
|
|
let newConfig = Config()
|
|
guard newConfig.loaded else {
|
|
Ghostty.logger.warning("failed to reload configuration")
|
|
return
|
|
}
|
|
|
|
ghostty_surface_update_config(surface, newConfig.config!)
|
|
}
|
|
|
|
/// Request that the given surface is closed. This will trigger the full normal surface close event
|
|
/// cycle which will call our close surface callback.
|
|
func requestClose(surface: ghostty_surface_t) {
|
|
ghostty_surface_request_close(surface)
|
|
}
|
|
|
|
func newTab(surface: ghostty_surface_t) {
|
|
let action = "new_tab"
|
|
if (!ghostty_surface_binding_action(surface, action, UInt(action.count))) {
|
|
logger.warning("action failed action=\(action)")
|
|
}
|
|
}
|
|
|
|
func newWindow(surface: ghostty_surface_t) {
|
|
let action = "new_window"
|
|
if (!ghostty_surface_binding_action(surface, action, UInt(action.count))) {
|
|
logger.warning("action failed action=\(action)")
|
|
}
|
|
}
|
|
|
|
func split(surface: ghostty_surface_t, direction: ghostty_action_split_direction_e) {
|
|
ghostty_surface_split(surface, direction)
|
|
}
|
|
|
|
func splitMoveFocus(surface: ghostty_surface_t, direction: SplitFocusDirection) {
|
|
ghostty_surface_split_focus(surface, direction.toNative())
|
|
}
|
|
|
|
func splitResize(surface: ghostty_surface_t, direction: SplitResizeDirection, amount: UInt16) {
|
|
ghostty_surface_split_resize(surface, direction.toNative(), amount)
|
|
}
|
|
|
|
func splitEqualize(surface: ghostty_surface_t) {
|
|
ghostty_surface_split_equalize(surface)
|
|
}
|
|
|
|
func splitToggleZoom(surface: ghostty_surface_t) {
|
|
let action = "toggle_split_zoom"
|
|
if (!ghostty_surface_binding_action(surface, action, UInt(action.count))) {
|
|
logger.warning("action failed action=\(action)")
|
|
}
|
|
}
|
|
|
|
func toggleFullscreen(surface: ghostty_surface_t) {
|
|
let action = "toggle_fullscreen"
|
|
if (!ghostty_surface_binding_action(surface, action, UInt(action.count))) {
|
|
logger.warning("action failed action=\(action)")
|
|
}
|
|
}
|
|
|
|
enum FontSizeModification {
|
|
case increase(Int)
|
|
case decrease(Int)
|
|
case reset
|
|
}
|
|
|
|
func changeFontSize(surface: ghostty_surface_t, _ change: FontSizeModification) {
|
|
let action: String
|
|
switch change {
|
|
case .increase(let amount):
|
|
action = "increase_font_size:\(amount)"
|
|
case .decrease(let amount):
|
|
action = "decrease_font_size:\(amount)"
|
|
case .reset:
|
|
action = "reset_font_size"
|
|
}
|
|
if (!ghostty_surface_binding_action(surface, action, UInt(action.count))) {
|
|
logger.warning("action failed action=\(action)")
|
|
}
|
|
}
|
|
|
|
func toggleTerminalInspector(surface: ghostty_surface_t) {
|
|
let action = "inspector:toggle"
|
|
if (!ghostty_surface_binding_action(surface, action, UInt(action.count))) {
|
|
logger.warning("action failed action=\(action)")
|
|
}
|
|
}
|
|
|
|
func resetTerminal(surface: ghostty_surface_t) {
|
|
let action = "reset"
|
|
if (!ghostty_surface_binding_action(surface, action, UInt(action.count))) {
|
|
logger.warning("action failed action=\(action)")
|
|
}
|
|
}
|
|
|
|
#if os(iOS)
|
|
// MARK: Ghostty Callbacks (iOS)
|
|
|
|
static func wakeup(_ userdata: UnsafeMutableRawPointer?) {}
|
|
static func action(_ app: ghostty_app_t, target: ghostty_target_s, action: ghostty_action_s) -> Bool { return false }
|
|
static func readClipboard(
|
|
_ userdata: UnsafeMutableRawPointer?,
|
|
location: ghostty_clipboard_e,
|
|
state: UnsafeMutableRawPointer?
|
|
) {}
|
|
|
|
static func confirmReadClipboard(
|
|
_ userdata: UnsafeMutableRawPointer?,
|
|
string: UnsafePointer<CChar>?,
|
|
state: UnsafeMutableRawPointer?,
|
|
request: ghostty_clipboard_request_e
|
|
) {}
|
|
|
|
static func writeClipboard(
|
|
_ userdata: UnsafeMutableRawPointer?,
|
|
string: UnsafePointer<CChar>?,
|
|
location: ghostty_clipboard_e,
|
|
confirm: Bool
|
|
) {}
|
|
|
|
static func closeSurface(_ userdata: UnsafeMutableRawPointer?, processAlive: Bool) {}
|
|
#endif
|
|
|
|
#if os(macOS)
|
|
|
|
// MARK: Notifications
|
|
|
|
// Called when the selected keyboard changes. We have to notify Ghostty so that
|
|
// it can reload the keyboard mapping for input.
|
|
@objc private func keyboardSelectionDidChange(notification: NSNotification) {
|
|
guard let app = self.app else { return }
|
|
ghostty_app_keyboard_changed(app)
|
|
}
|
|
|
|
// Called when the app becomes active.
|
|
@objc private func applicationDidBecomeActive(notification: NSNotification) {
|
|
guard let app = self.app else { return }
|
|
ghostty_app_set_focus(app, true)
|
|
}
|
|
|
|
// Called when the app becomes inactive.
|
|
@objc private func applicationDidResignActive(notification: NSNotification) {
|
|
guard let app = self.app else { return }
|
|
ghostty_app_set_focus(app, false)
|
|
}
|
|
|
|
|
|
// MARK: Ghostty Callbacks (macOS)
|
|
|
|
static func closeSurface(_ userdata: UnsafeMutableRawPointer?, processAlive: Bool) {
|
|
let surface = self.surfaceUserdata(from: userdata)
|
|
NotificationCenter.default.post(name: Notification.ghosttyCloseSurface, object: surface, userInfo: [
|
|
"process_alive": processAlive,
|
|
])
|
|
}
|
|
|
|
static func readClipboard(_ userdata: UnsafeMutableRawPointer?, location: ghostty_clipboard_e, state: UnsafeMutableRawPointer?) {
|
|
// If we don't even have a surface, something went terrible wrong so we have
|
|
// to leak "state".
|
|
let surfaceView = self.surfaceUserdata(from: userdata)
|
|
guard let surface = surfaceView.surface else { return }
|
|
|
|
// Get our pasteboard
|
|
guard let pasteboard = NSPasteboard.ghostty(location) else {
|
|
return completeClipboardRequest(surface, data: "", state: state)
|
|
}
|
|
|
|
// Get our string
|
|
let str = pasteboard.getOpinionatedStringContents() ?? ""
|
|
completeClipboardRequest(surface, data: str, state: state)
|
|
}
|
|
|
|
static func confirmReadClipboard(
|
|
_ userdata: UnsafeMutableRawPointer?,
|
|
string: UnsafePointer<CChar>?,
|
|
state: UnsafeMutableRawPointer?,
|
|
request: ghostty_clipboard_request_e
|
|
) {
|
|
let surface = self.surfaceUserdata(from: userdata)
|
|
guard let valueStr = String(cString: string!, encoding: .utf8) else { return }
|
|
guard let request = Ghostty.ClipboardRequest.from(request: request) else { return }
|
|
NotificationCenter.default.post(
|
|
name: Notification.confirmClipboard,
|
|
object: surface,
|
|
userInfo: [
|
|
Notification.ConfirmClipboardStrKey: valueStr,
|
|
Notification.ConfirmClipboardStateKey: state as Any,
|
|
Notification.ConfirmClipboardRequestKey: request,
|
|
]
|
|
)
|
|
}
|
|
|
|
static func completeClipboardRequest(
|
|
_ surface: ghostty_surface_t,
|
|
data: String,
|
|
state: UnsafeMutableRawPointer?,
|
|
confirmed: Bool = false
|
|
) {
|
|
data.withCString { ptr in
|
|
ghostty_surface_complete_clipboard_request(surface, ptr, state, confirmed)
|
|
}
|
|
}
|
|
|
|
static func writeClipboard(_ userdata: UnsafeMutableRawPointer?, string: UnsafePointer<CChar>?, location: ghostty_clipboard_e, confirm: Bool) {
|
|
let surface = self.surfaceUserdata(from: userdata)
|
|
|
|
|
|
guard let pasteboard = NSPasteboard.ghostty(location) else { return }
|
|
guard let valueStr = String(cString: string!, encoding: .utf8) else { return }
|
|
if !confirm {
|
|
pasteboard.declareTypes([.string], owner: nil)
|
|
pasteboard.setString(valueStr, forType: .string)
|
|
return
|
|
}
|
|
|
|
NotificationCenter.default.post(
|
|
name: Notification.confirmClipboard,
|
|
object: surface,
|
|
userInfo: [
|
|
Notification.ConfirmClipboardStrKey: valueStr,
|
|
Notification.ConfirmClipboardRequestKey: Ghostty.ClipboardRequest.osc_52_write(pasteboard),
|
|
]
|
|
)
|
|
}
|
|
|
|
static func wakeup(_ userdata: UnsafeMutableRawPointer?) {
|
|
let state = Unmanaged<App>.fromOpaque(userdata!).takeUnretainedValue()
|
|
|
|
// Wakeup can be called from any thread so we schedule the app tick
|
|
// from the main thread. There is probably some improvements we can make
|
|
// to coalesce multiple ticks but I don't think it matters from a performance
|
|
// standpoint since we don't do this much.
|
|
DispatchQueue.main.async { state.appTick() }
|
|
}
|
|
|
|
/// Determine if a given notification should be presented to the user when Ghostty is running in the foreground.
|
|
func shouldPresentNotification(notification: UNNotification) -> Bool {
|
|
let userInfo = notification.request.content.userInfo
|
|
guard let uuidString = userInfo["surface"] as? String,
|
|
let uuid = UUID(uuidString: uuidString),
|
|
let surface = delegate?.findSurface(forUUID: uuid),
|
|
let window = surface.window else { return false }
|
|
return !window.isKeyWindow || !surface.focused
|
|
}
|
|
|
|
/// Returns the GhosttyState from the given userdata value.
|
|
static private func appState(fromView view: SurfaceView) -> App? {
|
|
guard let surface = view.surface else { return nil }
|
|
guard let app = ghostty_surface_app(surface) else { return nil }
|
|
guard let app_ud = ghostty_app_userdata(app) else { return nil }
|
|
return Unmanaged<App>.fromOpaque(app_ud).takeUnretainedValue()
|
|
}
|
|
|
|
/// Returns the surface view from the userdata.
|
|
static private func surfaceUserdata(from userdata: UnsafeMutableRawPointer?) -> SurfaceView {
|
|
return Unmanaged<SurfaceView>.fromOpaque(userdata!).takeUnretainedValue()
|
|
}
|
|
|
|
static private func surfaceView(from surface: ghostty_surface_t) -> SurfaceView? {
|
|
guard let surface_ud = ghostty_surface_userdata(surface) else { return nil }
|
|
return Unmanaged<SurfaceView>.fromOpaque(surface_ud).takeUnretainedValue()
|
|
}
|
|
|
|
// MARK: Actions (macOS)
|
|
|
|
static func action(_ app: ghostty_app_t, target: ghostty_target_s, action: ghostty_action_s) -> Bool {
|
|
// Make sure it a target we understand so all our action handlers can assert
|
|
switch (target.tag) {
|
|
case GHOSTTY_TARGET_APP, GHOSTTY_TARGET_SURFACE:
|
|
break
|
|
|
|
default:
|
|
Ghostty.logger.warning("unknown action target=\(target.tag.rawValue)")
|
|
return false
|
|
}
|
|
|
|
// Action dispatch
|
|
switch (action.tag) {
|
|
case GHOSTTY_ACTION_QUIT:
|
|
quit(app)
|
|
|
|
case GHOSTTY_ACTION_NEW_WINDOW:
|
|
newWindow(app, target: target)
|
|
|
|
case GHOSTTY_ACTION_NEW_TAB:
|
|
newTab(app, target: target)
|
|
|
|
case GHOSTTY_ACTION_NEW_SPLIT:
|
|
newSplit(app, target: target, direction: action.action.new_split)
|
|
|
|
case GHOSTTY_ACTION_CLOSE_TAB:
|
|
closeTab(app, target: target)
|
|
|
|
case GHOSTTY_ACTION_CLOSE_WINDOW:
|
|
closeWindow(app, target: target)
|
|
|
|
case GHOSTTY_ACTION_TOGGLE_FULLSCREEN:
|
|
toggleFullscreen(app, target: target, mode: action.action.toggle_fullscreen)
|
|
|
|
case GHOSTTY_ACTION_MOVE_TAB:
|
|
return moveTab(app, target: target, move: action.action.move_tab)
|
|
|
|
case GHOSTTY_ACTION_GOTO_TAB:
|
|
return gotoTab(app, target: target, tab: action.action.goto_tab)
|
|
|
|
case GHOSTTY_ACTION_GOTO_SPLIT:
|
|
return gotoSplit(app, target: target, direction: action.action.goto_split)
|
|
|
|
case GHOSTTY_ACTION_RESIZE_SPLIT:
|
|
resizeSplit(app, target: target, resize: action.action.resize_split)
|
|
|
|
case GHOSTTY_ACTION_EQUALIZE_SPLITS:
|
|
equalizeSplits(app, target: target)
|
|
|
|
case GHOSTTY_ACTION_TOGGLE_SPLIT_ZOOM:
|
|
toggleSplitZoom(app, target: target)
|
|
|
|
case GHOSTTY_ACTION_INSPECTOR:
|
|
controlInspector(app, target: target, mode: action.action.inspector)
|
|
|
|
case GHOSTTY_ACTION_RENDER_INSPECTOR:
|
|
renderInspector(app, target: target)
|
|
|
|
case GHOSTTY_ACTION_DESKTOP_NOTIFICATION:
|
|
showDesktopNotification(app, target: target, n: action.action.desktop_notification)
|
|
|
|
case GHOSTTY_ACTION_SET_TITLE:
|
|
setTitle(app, target: target, v: action.action.set_title)
|
|
|
|
case GHOSTTY_ACTION_PROMPT_TITLE:
|
|
return promptTitle(app, target: target)
|
|
|
|
case GHOSTTY_ACTION_PWD:
|
|
pwdChanged(app, target: target, v: action.action.pwd)
|
|
|
|
case GHOSTTY_ACTION_OPEN_CONFIG:
|
|
ghostty_config_open()
|
|
|
|
case GHOSTTY_ACTION_SECURE_INPUT:
|
|
toggleSecureInput(app, target: target, mode: action.action.secure_input)
|
|
|
|
case GHOSTTY_ACTION_MOUSE_SHAPE:
|
|
setMouseShape(app, target: target, shape: action.action.mouse_shape)
|
|
|
|
case GHOSTTY_ACTION_MOUSE_VISIBILITY:
|
|
setMouseVisibility(app, target: target, v: action.action.mouse_visibility)
|
|
|
|
case GHOSTTY_ACTION_MOUSE_OVER_LINK:
|
|
setMouseOverLink(app, target: target, v: action.action.mouse_over_link)
|
|
|
|
case GHOSTTY_ACTION_INITIAL_SIZE:
|
|
setInitialSize(app, target: target, v: action.action.initial_size)
|
|
|
|
case GHOSTTY_ACTION_RESET_WINDOW_SIZE:
|
|
resetWindowSize(app, target: target)
|
|
|
|
case GHOSTTY_ACTION_CELL_SIZE:
|
|
setCellSize(app, target: target, v: action.action.cell_size)
|
|
|
|
case GHOSTTY_ACTION_RENDERER_HEALTH:
|
|
rendererHealth(app, target: target, v: action.action.renderer_health)
|
|
|
|
case GHOSTTY_ACTION_TOGGLE_QUICK_TERMINAL:
|
|
toggleQuickTerminal(app, target: target)
|
|
|
|
case GHOSTTY_ACTION_TOGGLE_VISIBILITY:
|
|
toggleVisibility(app, target: target)
|
|
|
|
case GHOSTTY_ACTION_KEY_SEQUENCE:
|
|
keySequence(app, target: target, v: action.action.key_sequence)
|
|
|
|
case GHOSTTY_ACTION_CONFIG_CHANGE:
|
|
configChange(app, target: target, v: action.action.config_change)
|
|
|
|
case GHOSTTY_ACTION_RELOAD_CONFIG:
|
|
configReload(app, target: target, v: action.action.reload_config)
|
|
|
|
case GHOSTTY_ACTION_COLOR_CHANGE:
|
|
colorChange(app, target: target, change: action.action.color_change)
|
|
|
|
case GHOSTTY_ACTION_CLOSE_ALL_WINDOWS:
|
|
fallthrough
|
|
case GHOSTTY_ACTION_TOGGLE_TAB_OVERVIEW:
|
|
fallthrough
|
|
case GHOSTTY_ACTION_TOGGLE_WINDOW_DECORATIONS:
|
|
fallthrough
|
|
case GHOSTTY_ACTION_PRESENT_TERMINAL:
|
|
fallthrough
|
|
case GHOSTTY_ACTION_SIZE_LIMIT:
|
|
fallthrough
|
|
case GHOSTTY_ACTION_QUIT_TIMER:
|
|
Ghostty.logger.info("known but unimplemented action action=\(action.tag.rawValue)")
|
|
return false
|
|
default:
|
|
Ghostty.logger.warning("unknown action action=\(action.tag.rawValue)")
|
|
return false
|
|
}
|
|
|
|
// If we reached here then we assume performed since all unknown actions
|
|
// are captured in the switch and return false.
|
|
return true
|
|
}
|
|
|
|
private static func quit(_ app: ghostty_app_t) {
|
|
// On iOS, applications do not terminate programmatically like they do
|
|
// on macOS. On iOS, applications are only terminated when a user physically
|
|
// closes the application (i.e. going to the home screen). If we request
|
|
// exit on iOS we ignore it.
|
|
#if os(iOS)
|
|
logger.info("quit request received, ignoring on iOS")
|
|
#endif
|
|
|
|
#if os(macOS)
|
|
// We want to quit, start that process
|
|
NSApplication.shared.terminate(nil)
|
|
#endif
|
|
}
|
|
|
|
private static func newWindow(_ app: ghostty_app_t, target: ghostty_target_s) {
|
|
switch (target.tag) {
|
|
case GHOSTTY_TARGET_APP:
|
|
NotificationCenter.default.post(
|
|
name: Notification.ghosttyNewWindow,
|
|
object: nil,
|
|
userInfo: [:]
|
|
)
|
|
|
|
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: Notification.ghosttyNewWindow,
|
|
object: surfaceView,
|
|
userInfo: [
|
|
Notification.NewSurfaceConfigKey: SurfaceConfiguration(from: ghostty_surface_inherited_config(surface)),
|
|
]
|
|
)
|
|
|
|
|
|
default:
|
|
assertionFailure()
|
|
}
|
|
}
|
|
|
|
private static func newTab(_ app: ghostty_app_t, target: ghostty_target_s) {
|
|
switch (target.tag) {
|
|
case GHOSTTY_TARGET_APP:
|
|
NotificationCenter.default.post(
|
|
name: Notification.ghosttyNewTab,
|
|
object: nil,
|
|
userInfo: [:]
|
|
)
|
|
|
|
case GHOSTTY_TARGET_SURFACE:
|
|
guard let surface = target.target.surface else { return }
|
|
guard let surfaceView = self.surfaceView(from: surface) else { return }
|
|
guard let appState = self.appState(fromView: surfaceView) else { return }
|
|
guard appState.config.windowDecorations else {
|
|
let alert = NSAlert()
|
|
alert.messageText = "Tabs are disabled"
|
|
alert.informativeText = "Enable window decorations to use tabs"
|
|
alert.addButton(withTitle: "OK")
|
|
alert.alertStyle = .warning
|
|
_ = alert.runModal()
|
|
return
|
|
}
|
|
|
|
NotificationCenter.default.post(
|
|
name: Notification.ghosttyNewTab,
|
|
object: surfaceView,
|
|
userInfo: [
|
|
Notification.NewSurfaceConfigKey: SurfaceConfiguration(from: ghostty_surface_inherited_config(surface)),
|
|
]
|
|
)
|
|
|
|
|
|
default:
|
|
assertionFailure()
|
|
}
|
|
}
|
|
|
|
private static func newSplit(
|
|
_ app: ghostty_app_t,
|
|
target: ghostty_target_s,
|
|
direction: ghostty_action_split_direction_e) {
|
|
switch (target.tag) {
|
|
case GHOSTTY_TARGET_APP:
|
|
// New split does nothing with an app target
|
|
Ghostty.logger.warning("new split 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: Notification.ghosttyNewSplit,
|
|
object: surfaceView,
|
|
userInfo: [
|
|
"direction": direction,
|
|
Notification.NewSurfaceConfigKey: SurfaceConfiguration(from: ghostty_surface_inherited_config(surface)),
|
|
]
|
|
)
|
|
|
|
|
|
default:
|
|
assertionFailure()
|
|
}
|
|
}
|
|
|
|
private static func closeTab(_ app: ghostty_app_t, target: ghostty_target_s) {
|
|
switch (target.tag) {
|
|
case GHOSTTY_TARGET_APP:
|
|
Ghostty.logger.warning("close tab 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: .ghosttyCloseTab,
|
|
object: surfaceView
|
|
)
|
|
|
|
|
|
default:
|
|
assertionFailure()
|
|
}
|
|
}
|
|
|
|
private static func closeWindow(_ app: ghostty_app_t, target: ghostty_target_s) {
|
|
switch (target.tag) {
|
|
case GHOSTTY_TARGET_APP:
|
|
Ghostty.logger.warning("close window 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: .ghosttyCloseWindow,
|
|
object: surfaceView
|
|
)
|
|
|
|
default:
|
|
assertionFailure()
|
|
}
|
|
}
|
|
|
|
private static func toggleFullscreen(
|
|
_ app: ghostty_app_t,
|
|
target: ghostty_target_s,
|
|
mode raw: ghostty_action_fullscreen_e) {
|
|
switch (target.tag) {
|
|
case GHOSTTY_TARGET_APP:
|
|
Ghostty.logger.warning("toggle fullscreen 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 }
|
|
guard let mode = FullscreenMode.from(ghostty: raw) else {
|
|
Ghostty.logger.warning("unknow fullscreen mode raw=\(raw.rawValue)")
|
|
return
|
|
}
|
|
NotificationCenter.default.post(
|
|
name: Notification.ghosttyToggleFullscreen,
|
|
object: surfaceView,
|
|
userInfo: [
|
|
Notification.FullscreenModeKey: mode,
|
|
]
|
|
)
|
|
|
|
|
|
default:
|
|
assertionFailure()
|
|
}
|
|
}
|
|
|
|
private static func toggleVisibility(
|
|
_ app: ghostty_app_t,
|
|
target: ghostty_target_s
|
|
) {
|
|
guard let appDelegate = NSApplication.shared.delegate as? AppDelegate else { return }
|
|
appDelegate.toggleVisibility(self)
|
|
}
|
|
|
|
private static func moveTab(
|
|
_ app: ghostty_app_t,
|
|
target: ghostty_target_s,
|
|
move: ghostty_action_move_tab_s) -> Bool {
|
|
switch (target.tag) {
|
|
case GHOSTTY_TARGET_APP:
|
|
Ghostty.logger.warning("move tab does nothing with an app target")
|
|
return false
|
|
|
|
case GHOSTTY_TARGET_SURFACE:
|
|
guard let surface = target.target.surface else { return false }
|
|
guard let surfaceView = self.surfaceView(from: surface) else { return false }
|
|
|
|
// See gotoTab for notes on this check.
|
|
guard (surfaceView.window?.tabGroup?.windows.count ?? 0) > 1 else { return false }
|
|
|
|
NotificationCenter.default.post(
|
|
name: .ghosttyMoveTab,
|
|
object: surfaceView,
|
|
userInfo: [
|
|
SwiftUI.Notification.Name.GhosttyMoveTabKey: Action.MoveTab(c: move),
|
|
]
|
|
)
|
|
|
|
default:
|
|
assertionFailure()
|
|
}
|
|
|
|
return true
|
|
}
|
|
|
|
private static func gotoTab(
|
|
_ app: ghostty_app_t,
|
|
target: ghostty_target_s,
|
|
tab: ghostty_action_goto_tab_e) -> Bool {
|
|
switch (target.tag) {
|
|
case GHOSTTY_TARGET_APP:
|
|
Ghostty.logger.warning("goto tab does nothing with an app target")
|
|
return false
|
|
|
|
case GHOSTTY_TARGET_SURFACE:
|
|
guard let surface = target.target.surface else { return false }
|
|
guard let surfaceView = self.surfaceView(from: surface) else { return false }
|
|
|
|
// Similar to goto_split (see comment there) about our performability,
|
|
// we should make this more accurate later.
|
|
guard (surfaceView.window?.tabGroup?.windows.count ?? 0) > 1 else { return false }
|
|
|
|
NotificationCenter.default.post(
|
|
name: Notification.ghosttyGotoTab,
|
|
object: surfaceView,
|
|
userInfo: [
|
|
Notification.GotoTabKey: tab,
|
|
]
|
|
)
|
|
|
|
default:
|
|
assertionFailure()
|
|
}
|
|
|
|
return true
|
|
}
|
|
|
|
private static func gotoSplit(
|
|
_ app: ghostty_app_t,
|
|
target: ghostty_target_s,
|
|
direction: ghostty_action_goto_split_e) -> Bool {
|
|
switch (target.tag) {
|
|
case GHOSTTY_TARGET_APP:
|
|
Ghostty.logger.warning("goto split does nothing with an app target")
|
|
return false
|
|
|
|
case GHOSTTY_TARGET_SURFACE:
|
|
guard let surface = target.target.surface else { return false }
|
|
guard let surfaceView = self.surfaceView(from: surface) else { return false }
|
|
guard let controller = surfaceView.window?.windowController as? BaseTerminalController else { return false }
|
|
|
|
// For now, we return false if the window has no splits and we return
|
|
// true if the window has ANY splits. This isn't strictly correct because
|
|
// we should only be returning true if we actually performed the action,
|
|
// but this handles the most common case of caring about goto_split performability
|
|
// which is the no-split case.
|
|
guard controller.surfaceTree?.isSplit ?? false else { return false }
|
|
|
|
NotificationCenter.default.post(
|
|
name: Notification.ghosttyFocusSplit,
|
|
object: surfaceView,
|
|
userInfo: [
|
|
Notification.SplitDirectionKey: SplitFocusDirection.from(direction: direction) as Any,
|
|
]
|
|
)
|
|
|
|
default:
|
|
assertionFailure()
|
|
}
|
|
|
|
return true
|
|
}
|
|
|
|
private static func resizeSplit(
|
|
_ app: ghostty_app_t,
|
|
target: ghostty_target_s,
|
|
resize: ghostty_action_resize_split_s) {
|
|
switch (target.tag) {
|
|
case GHOSTTY_TARGET_APP:
|
|
Ghostty.logger.warning("resize split 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 }
|
|
guard let resizeDirection = SplitResizeDirection.from(direction: resize.direction) else { return }
|
|
NotificationCenter.default.post(
|
|
name: Notification.didResizeSplit,
|
|
object: surfaceView,
|
|
userInfo: [
|
|
Notification.ResizeSplitDirectionKey: resizeDirection,
|
|
Notification.ResizeSplitAmountKey: resize.amount,
|
|
]
|
|
)
|
|
|
|
default:
|
|
assertionFailure()
|
|
}
|
|
}
|
|
|
|
private static func equalizeSplits(
|
|
_ app: ghostty_app_t,
|
|
target: ghostty_target_s) {
|
|
switch (target.tag) {
|
|
case GHOSTTY_TARGET_APP:
|
|
Ghostty.logger.warning("equalize splits 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: Notification.didEqualizeSplits,
|
|
object: surfaceView
|
|
)
|
|
|
|
|
|
default:
|
|
assertionFailure()
|
|
}
|
|
}
|
|
|
|
private static func toggleSplitZoom(
|
|
_ app: ghostty_app_t,
|
|
target: ghostty_target_s) {
|
|
switch (target.tag) {
|
|
case GHOSTTY_TARGET_APP:
|
|
Ghostty.logger.warning("toggle split zoom 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: Notification.didToggleSplitZoom,
|
|
object: surfaceView
|
|
)
|
|
|
|
|
|
default:
|
|
assertionFailure()
|
|
}
|
|
}
|
|
|
|
private static func controlInspector(
|
|
_ app: ghostty_app_t,
|
|
target: ghostty_target_s,
|
|
mode: ghostty_action_inspector_e) {
|
|
switch (target.tag) {
|
|
case GHOSTTY_TARGET_APP:
|
|
Ghostty.logger.warning("toggle split zoom 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: Notification.didControlInspector,
|
|
object: surfaceView,
|
|
userInfo: ["mode": mode]
|
|
)
|
|
|
|
|
|
default:
|
|
assertionFailure()
|
|
}
|
|
}
|
|
|
|
private static func showDesktopNotification(
|
|
_ app: ghostty_app_t,
|
|
target: ghostty_target_s,
|
|
n: ghostty_action_desktop_notification_s) {
|
|
switch (target.tag) {
|
|
case GHOSTTY_TARGET_APP:
|
|
Ghostty.logger.warning("toggle split zoom 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 }
|
|
guard let title = String(cString: n.title!, encoding: .utf8) else { return }
|
|
guard let body = String(cString: n.body!, encoding: .utf8) else { return }
|
|
|
|
let center = UNUserNotificationCenter.current()
|
|
center.requestAuthorization(options: [.alert, .sound]) { _, error in
|
|
if let error = error {
|
|
Ghostty.logger.error("Error while requesting notification authorization: \(error)")
|
|
}
|
|
}
|
|
|
|
center.getNotificationSettings() { settings in
|
|
guard settings.authorizationStatus == .authorized else { return }
|
|
surfaceView.showUserNotification(title: title, body: body)
|
|
}
|
|
|
|
|
|
default:
|
|
assertionFailure()
|
|
}
|
|
}
|
|
|
|
private static func toggleSecureInput(
|
|
_ app: ghostty_app_t,
|
|
target: ghostty_target_s,
|
|
mode mode_raw: ghostty_action_secure_input_e
|
|
) {
|
|
guard let mode = SetSecureInput.from(mode_raw) else { return }
|
|
|
|
switch (target.tag) {
|
|
case GHOSTTY_TARGET_APP:
|
|
guard let appDelegate = NSApplication.shared.delegate as? AppDelegate else { return }
|
|
appDelegate.setSecureInput(mode)
|
|
|
|
case GHOSTTY_TARGET_SURFACE:
|
|
guard let surface = target.target.surface else { return }
|
|
guard let surfaceView = self.surfaceView(from: surface) else { return }
|
|
guard let appState = self.appState(fromView: surfaceView) else { return }
|
|
guard appState.config.autoSecureInput else { return }
|
|
|
|
switch (mode) {
|
|
case .on:
|
|
surfaceView.passwordInput = true
|
|
|
|
case .off:
|
|
surfaceView.passwordInput = false
|
|
|
|
case .toggle:
|
|
surfaceView.passwordInput = !surfaceView.passwordInput
|
|
}
|
|
|
|
default:
|
|
assertionFailure()
|
|
}
|
|
}
|
|
|
|
private static func toggleQuickTerminal(
|
|
_ app: ghostty_app_t,
|
|
target: ghostty_target_s
|
|
) {
|
|
guard let appDelegate = NSApplication.shared.delegate as? AppDelegate else { return }
|
|
appDelegate.toggleQuickTerminal(self)
|
|
}
|
|
|
|
private static func setTitle(
|
|
_ app: ghostty_app_t,
|
|
target: ghostty_target_s,
|
|
v: ghostty_action_set_title_s) {
|
|
switch (target.tag) {
|
|
case GHOSTTY_TARGET_APP:
|
|
Ghostty.logger.warning("set title 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 }
|
|
guard let title = String(cString: v.title!, encoding: .utf8) else { return }
|
|
surfaceView.setTitle(title)
|
|
|
|
default:
|
|
assertionFailure()
|
|
}
|
|
}
|
|
|
|
private static func promptTitle(
|
|
_ app: ghostty_app_t,
|
|
target: ghostty_target_s) -> Bool {
|
|
switch (target.tag) {
|
|
case GHOSTTY_TARGET_APP:
|
|
Ghostty.logger.warning("set title prompt does nothing with an app target")
|
|
return false
|
|
|
|
case GHOSTTY_TARGET_SURFACE:
|
|
guard let surface = target.target.surface else { return false }
|
|
guard let surfaceView = self.surfaceView(from: surface) else { return false }
|
|
surfaceView.promptTitle()
|
|
|
|
default:
|
|
assertionFailure()
|
|
}
|
|
|
|
return true
|
|
}
|
|
|
|
private static func pwdChanged(
|
|
_ app: ghostty_app_t,
|
|
target: ghostty_target_s,
|
|
v: ghostty_action_pwd_s) {
|
|
switch (target.tag) {
|
|
case GHOSTTY_TARGET_APP:
|
|
Ghostty.logger.warning("pwd change 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 }
|
|
guard let pwd = String(cString: v.pwd!, encoding: .utf8) else { return }
|
|
surfaceView.pwd = pwd
|
|
|
|
default:
|
|
assertionFailure()
|
|
}
|
|
}
|
|
|
|
private static func setMouseShape(
|
|
_ app: ghostty_app_t,
|
|
target: ghostty_target_s,
|
|
shape: ghostty_action_mouse_shape_e) {
|
|
switch (target.tag) {
|
|
case GHOSTTY_TARGET_APP:
|
|
Ghostty.logger.warning("set mouse shapes 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 }
|
|
surfaceView.setCursorShape(shape)
|
|
|
|
|
|
default:
|
|
assertionFailure()
|
|
}
|
|
}
|
|
|
|
private static func setMouseVisibility(
|
|
_ app: ghostty_app_t,
|
|
target: ghostty_target_s,
|
|
v: ghostty_action_mouse_visibility_e) {
|
|
switch (target.tag) {
|
|
case GHOSTTY_TARGET_APP:
|
|
Ghostty.logger.warning("set mouse shapes 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 }
|
|
switch (v) {
|
|
case GHOSTTY_MOUSE_VISIBLE:
|
|
surfaceView.setCursorVisibility(true)
|
|
|
|
case GHOSTTY_MOUSE_HIDDEN:
|
|
surfaceView.setCursorVisibility(false)
|
|
|
|
default:
|
|
return
|
|
}
|
|
|
|
|
|
default:
|
|
assertionFailure()
|
|
}
|
|
}
|
|
|
|
private static func setMouseOverLink(
|
|
_ app: ghostty_app_t,
|
|
target: ghostty_target_s,
|
|
v: ghostty_action_mouse_over_link_s) {
|
|
switch (target.tag) {
|
|
case GHOSTTY_TARGET_APP:
|
|
Ghostty.logger.warning("mouse over link 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 }
|
|
guard v.len > 0 else {
|
|
surfaceView.hoverUrl = nil
|
|
return
|
|
}
|
|
|
|
let buffer = Data(bytes: v.url!, count: v.len)
|
|
surfaceView.hoverUrl = String(data: buffer, encoding: .utf8)
|
|
|
|
|
|
default:
|
|
assertionFailure()
|
|
}
|
|
}
|
|
|
|
private static func setInitialSize(
|
|
_ app: ghostty_app_t,
|
|
target: ghostty_target_s,
|
|
v: ghostty_action_initial_size_s) {
|
|
switch (target.tag) {
|
|
case GHOSTTY_TARGET_APP:
|
|
Ghostty.logger.warning("initial size 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 }
|
|
surfaceView.initialSize = NSMakeSize(Double(v.width), Double(v.height))
|
|
|
|
|
|
default:
|
|
assertionFailure()
|
|
}
|
|
}
|
|
|
|
private static func resetWindowSize(
|
|
_ app: ghostty_app_t,
|
|
target: ghostty_target_s) {
|
|
switch (target.tag) {
|
|
case GHOSTTY_TARGET_APP:
|
|
Ghostty.logger.warning("reset window size 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: .ghosttyResetWindowSize,
|
|
object: surfaceView
|
|
)
|
|
|
|
|
|
default:
|
|
assertionFailure()
|
|
}
|
|
}
|
|
|
|
private static func setCellSize(
|
|
_ app: ghostty_app_t,
|
|
target: ghostty_target_s,
|
|
v: ghostty_action_cell_size_s) {
|
|
switch (target.tag) {
|
|
case GHOSTTY_TARGET_APP:
|
|
Ghostty.logger.warning("mouse over link 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 }
|
|
let backingSize = NSSize(width: Double(v.width), height: Double(v.height))
|
|
DispatchQueue.main.async { [weak surfaceView] in
|
|
guard let surfaceView else { return }
|
|
surfaceView.cellSize = surfaceView.convertFromBacking(backingSize)
|
|
}
|
|
|
|
default:
|
|
assertionFailure()
|
|
}
|
|
}
|
|
|
|
private static func renderInspector(
|
|
_ app: ghostty_app_t,
|
|
target: ghostty_target_s) {
|
|
switch (target.tag) {
|
|
case GHOSTTY_TARGET_APP:
|
|
Ghostty.logger.warning("mouse over link 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: Notification.inspectorNeedsDisplay,
|
|
object: surfaceView
|
|
)
|
|
|
|
default:
|
|
assertionFailure()
|
|
}
|
|
}
|
|
|
|
private static func rendererHealth(
|
|
_ app: ghostty_app_t,
|
|
target: ghostty_target_s,
|
|
v: ghostty_action_renderer_health_e) {
|
|
switch (target.tag) {
|
|
case GHOSTTY_TARGET_APP:
|
|
Ghostty.logger.warning("mouse over link 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: Notification.didUpdateRendererHealth,
|
|
object: surfaceView,
|
|
userInfo: [
|
|
"health": v,
|
|
]
|
|
)
|
|
|
|
default:
|
|
assertionFailure()
|
|
}
|
|
}
|
|
|
|
private static func keySequence(
|
|
_ app: ghostty_app_t,
|
|
target: ghostty_target_s,
|
|
v: ghostty_action_key_sequence_s) {
|
|
switch (target.tag) {
|
|
case GHOSTTY_TARGET_APP:
|
|
Ghostty.logger.warning("key sequence 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 }
|
|
if v.active {
|
|
NotificationCenter.default.post(
|
|
name: Notification.didContinueKeySequence,
|
|
object: surfaceView,
|
|
userInfo: [
|
|
Notification.KeySequenceKey: keyEquivalent(for: v.trigger) as Any
|
|
]
|
|
)
|
|
} else {
|
|
NotificationCenter.default.post(
|
|
name: Notification.didEndKeySequence,
|
|
object: surfaceView
|
|
)
|
|
}
|
|
|
|
default:
|
|
assertionFailure()
|
|
}
|
|
}
|
|
|
|
private static func configReload(
|
|
_ app: ghostty_app_t,
|
|
target: ghostty_target_s,
|
|
v: ghostty_action_reload_config_s)
|
|
{
|
|
logger.info("config reload notification")
|
|
|
|
guard let app_ud = ghostty_app_userdata(app) else { return }
|
|
let ghostty = Unmanaged<App>.fromOpaque(app_ud).takeUnretainedValue()
|
|
|
|
switch (target.tag) {
|
|
case GHOSTTY_TARGET_APP:
|
|
ghostty.reloadConfig(soft: v.soft)
|
|
return
|
|
|
|
case GHOSTTY_TARGET_SURFACE:
|
|
guard let surface = target.target.surface else { return }
|
|
ghostty.reloadConfig(surface: surface, soft: v.soft)
|
|
|
|
default:
|
|
assertionFailure()
|
|
}
|
|
}
|
|
|
|
private static func configChange(
|
|
_ app: ghostty_app_t,
|
|
target: ghostty_target_s,
|
|
v: ghostty_action_config_change_s) {
|
|
logger.info("config change notification")
|
|
|
|
// Clone the config so we own the memory. It'd be nicer to not have to do
|
|
// this but since we async send the config out below we have to own the lifetime.
|
|
// A future improvement might be to add reference counting to config or
|
|
// something so apprt's do not have to do this.
|
|
let config = Config(clone: v.config)
|
|
|
|
switch (target.tag) {
|
|
case GHOSTTY_TARGET_APP:
|
|
// Notify the world that the app config changed
|
|
NotificationCenter.default.post(
|
|
name: .ghosttyConfigDidChange,
|
|
object: nil,
|
|
userInfo: [
|
|
SwiftUI.Notification.Name.GhosttyConfigChangeKey: config,
|
|
]
|
|
)
|
|
|
|
// We also REPLACE our app-level config when this happens. This lets
|
|
// all the various things that depend on this but are still theme specific
|
|
// such as split border color work.
|
|
guard let app_ud = ghostty_app_userdata(app) else { return }
|
|
let ghostty = Unmanaged<App>.fromOpaque(app_ud).takeUnretainedValue()
|
|
ghostty.config = config
|
|
|
|
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: .ghosttyConfigDidChange,
|
|
object: surfaceView,
|
|
userInfo: [
|
|
SwiftUI.Notification.Name.GhosttyConfigChangeKey: config,
|
|
]
|
|
)
|
|
|
|
default:
|
|
assertionFailure()
|
|
}
|
|
}
|
|
|
|
private static func colorChange(
|
|
_ app: ghostty_app_t,
|
|
target: ghostty_target_s,
|
|
change: ghostty_action_color_change_s) {
|
|
switch (target.tag) {
|
|
case GHOSTTY_TARGET_APP:
|
|
Ghostty.logger.warning("color change 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: .ghosttyColorDidChange,
|
|
object: surfaceView,
|
|
userInfo: [
|
|
SwiftUI.Notification.Name.GhosttyColorChangeKey: Action.ColorChange(c: change)
|
|
]
|
|
)
|
|
|
|
default:
|
|
assertionFailure()
|
|
}
|
|
}
|
|
|
|
|
|
// MARK: User Notifications
|
|
|
|
/// Handle a received user notification. This is called when a user notification is clicked or dismissed by the user
|
|
func handleUserNotification(response: UNNotificationResponse) {
|
|
let userInfo = response.notification.request.content.userInfo
|
|
guard let uuidString = userInfo["surface"] as? String,
|
|
let uuid = UUID(uuidString: uuidString),
|
|
let surface = delegate?.findSurface(forUUID: uuid) else { return }
|
|
|
|
switch (response.actionIdentifier) {
|
|
case UNNotificationDefaultActionIdentifier, Ghostty.userNotificationActionShow:
|
|
// The user clicked on a notification
|
|
surface.handleUserNotification(notification: response.notification, focus: true)
|
|
case UNNotificationDismissActionIdentifier:
|
|
// The user dismissed the notification
|
|
surface.handleUserNotification(notification: response.notification, focus: false)
|
|
default:
|
|
break
|
|
}
|
|
}
|
|
|
|
#endif
|
|
}
|
|
}
|