mirror of
https://github.com/ghostty-org/ghostty.git
synced 2025-08-02 14:57:31 +03:00
macos: base class for terminal controller
This commit is contained in:
@ -34,6 +34,7 @@
|
|||||||
A53D0C952B53B4D800305CE6 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = A5B30538299BEAAB0047F10C /* Assets.xcassets */; };
|
A53D0C952B53B4D800305CE6 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = A5B30538299BEAAB0047F10C /* Assets.xcassets */; };
|
||||||
A53D0C9B2B543F3B00305CE6 /* Ghostty.App.swift in Sources */ = {isa = PBXBuildFile; fileRef = A53D0C992B543F3B00305CE6 /* Ghostty.App.swift */; };
|
A53D0C9B2B543F3B00305CE6 /* Ghostty.App.swift in Sources */ = {isa = PBXBuildFile; fileRef = A53D0C992B543F3B00305CE6 /* Ghostty.App.swift */; };
|
||||||
A53D0C9C2B543F7B00305CE6 /* Package.swift in Sources */ = {isa = PBXBuildFile; fileRef = A55B7BB729B6F53A0055DE60 /* Package.swift */; };
|
A53D0C9C2B543F7B00305CE6 /* Package.swift in Sources */ = {isa = PBXBuildFile; fileRef = A55B7BB729B6F53A0055DE60 /* Package.swift */; };
|
||||||
|
A54D786C2CA7978E001B19B1 /* BaseTerminalController.swift in Sources */ = {isa = PBXBuildFile; fileRef = A54D786B2CA79788001B19B1 /* BaseTerminalController.swift */; };
|
||||||
A55685E029A03A9F004303CE /* AppError.swift in Sources */ = {isa = PBXBuildFile; fileRef = A55685DF29A03A9F004303CE /* AppError.swift */; };
|
A55685E029A03A9F004303CE /* AppError.swift in Sources */ = {isa = PBXBuildFile; fileRef = A55685DF29A03A9F004303CE /* AppError.swift */; };
|
||||||
A55B7BB829B6F53A0055DE60 /* Package.swift in Sources */ = {isa = PBXBuildFile; fileRef = A55B7BB729B6F53A0055DE60 /* Package.swift */; };
|
A55B7BB829B6F53A0055DE60 /* Package.swift in Sources */ = {isa = PBXBuildFile; fileRef = A55B7BB729B6F53A0055DE60 /* Package.swift */; };
|
||||||
A55B7BBC29B6FC330055DE60 /* SurfaceView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A55B7BBB29B6FC330055DE60 /* SurfaceView.swift */; };
|
A55B7BBC29B6FC330055DE60 /* SurfaceView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A55B7BBB29B6FC330055DE60 /* SurfaceView.swift */; };
|
||||||
@ -61,11 +62,11 @@
|
|||||||
A5CBD0562C9E65B80017A1AE /* DraggableWindowView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5CBD0552C9E65A50017A1AE /* DraggableWindowView.swift */; };
|
A5CBD0562C9E65B80017A1AE /* DraggableWindowView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5CBD0552C9E65A50017A1AE /* DraggableWindowView.swift */; };
|
||||||
A5CBD0582C9F30960017A1AE /* Cursor.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5CBD0572C9F30860017A1AE /* Cursor.swift */; };
|
A5CBD0582C9F30960017A1AE /* Cursor.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5CBD0572C9F30860017A1AE /* Cursor.swift */; };
|
||||||
A5CBD0592C9F37B10017A1AE /* Backport.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5CEAFFE29C2410700646FDA /* Backport.swift */; };
|
A5CBD0592C9F37B10017A1AE /* Backport.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5CEAFFE29C2410700646FDA /* Backport.swift */; };
|
||||||
A5CBD06B2CA322430017A1AE /* GlobalEventTap.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5CBD06A2CA322320017A1AE /* GlobalEventTap.swift */; };
|
|
||||||
A5CBD05C2CA0C5C70017A1AE /* SlideTerminal.xib in Resources */ = {isa = PBXBuildFile; fileRef = A5CBD05B2CA0C5C70017A1AE /* SlideTerminal.xib */; };
|
A5CBD05C2CA0C5C70017A1AE /* SlideTerminal.xib in Resources */ = {isa = PBXBuildFile; fileRef = A5CBD05B2CA0C5C70017A1AE /* SlideTerminal.xib */; };
|
||||||
A5CBD05E2CA0C5EC0017A1AE /* SlideTerminalController.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5CBD05D2CA0C5E70017A1AE /* SlideTerminalController.swift */; };
|
A5CBD05E2CA0C5EC0017A1AE /* SlideTerminalController.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5CBD05D2CA0C5E70017A1AE /* SlideTerminalController.swift */; };
|
||||||
A5CBD0602CA0C90A0017A1AE /* SlideTerminalWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5CBD05F2CA0C9080017A1AE /* SlideTerminalWindow.swift */; };
|
A5CBD0602CA0C90A0017A1AE /* SlideTerminalWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5CBD05F2CA0C9080017A1AE /* SlideTerminalWindow.swift */; };
|
||||||
A5CBD0642CA122E70017A1AE /* SlideTerminalPosition.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5CBD0632CA122E70017A1AE /* SlideTerminalPosition.swift */; };
|
A5CBD0642CA122E70017A1AE /* SlideTerminalPosition.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5CBD0632CA122E70017A1AE /* SlideTerminalPosition.swift */; };
|
||||||
|
A5CBD06B2CA322430017A1AE /* GlobalEventTap.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5CBD06A2CA322320017A1AE /* GlobalEventTap.swift */; };
|
||||||
A5CC36132C9CD72D004D6760 /* SecureInputOverlay.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5CC36122C9CD729004D6760 /* SecureInputOverlay.swift */; };
|
A5CC36132C9CD72D004D6760 /* SecureInputOverlay.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5CC36122C9CD729004D6760 /* SecureInputOverlay.swift */; };
|
||||||
A5CC36152C9CDA06004D6760 /* View+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5CC36142C9CDA03004D6760 /* View+Extension.swift */; };
|
A5CC36152C9CDA06004D6760 /* View+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5CC36142C9CDA03004D6760 /* View+Extension.swift */; };
|
||||||
A5CDF1912AAF9A5800513312 /* ConfigurationErrors.xib in Resources */ = {isa = PBXBuildFile; fileRef = A5CDF1902AAF9A5800513312 /* ConfigurationErrors.xib */; };
|
A5CDF1912AAF9A5800513312 /* ConfigurationErrors.xib in Resources */ = {isa = PBXBuildFile; fileRef = A5CDF1902AAF9A5800513312 /* ConfigurationErrors.xib */; };
|
||||||
@ -109,6 +110,7 @@
|
|||||||
A535B9D9299C569B0017E2E4 /* ErrorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ErrorView.swift; sourceTree = "<group>"; };
|
A535B9D9299C569B0017E2E4 /* ErrorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ErrorView.swift; sourceTree = "<group>"; };
|
||||||
A53D0C932B53B43700305CE6 /* iOSApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = iOSApp.swift; sourceTree = "<group>"; };
|
A53D0C932B53B43700305CE6 /* iOSApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = iOSApp.swift; sourceTree = "<group>"; };
|
||||||
A53D0C992B543F3B00305CE6 /* Ghostty.App.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Ghostty.App.swift; sourceTree = "<group>"; };
|
A53D0C992B543F3B00305CE6 /* Ghostty.App.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Ghostty.App.swift; sourceTree = "<group>"; };
|
||||||
|
A54D786B2CA79788001B19B1 /* BaseTerminalController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BaseTerminalController.swift; sourceTree = "<group>"; };
|
||||||
A55685DF29A03A9F004303CE /* AppError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppError.swift; sourceTree = "<group>"; };
|
A55685DF29A03A9F004303CE /* AppError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppError.swift; sourceTree = "<group>"; };
|
||||||
A55B7BB729B6F53A0055DE60 /* Package.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Package.swift; sourceTree = "<group>"; };
|
A55B7BB729B6F53A0055DE60 /* Package.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Package.swift; sourceTree = "<group>"; };
|
||||||
A55B7BBB29B6FC330055DE60 /* SurfaceView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SurfaceView.swift; sourceTree = "<group>"; };
|
A55B7BBB29B6FC330055DE60 /* SurfaceView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SurfaceView.swift; sourceTree = "<group>"; };
|
||||||
@ -136,11 +138,11 @@
|
|||||||
A5B3053D299BEAAB0047F10C /* Ghostty.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Ghostty.entitlements; sourceTree = "<group>"; };
|
A5B3053D299BEAAB0047F10C /* Ghostty.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Ghostty.entitlements; sourceTree = "<group>"; };
|
||||||
A5CBD0552C9E65A50017A1AE /* DraggableWindowView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DraggableWindowView.swift; sourceTree = "<group>"; };
|
A5CBD0552C9E65A50017A1AE /* DraggableWindowView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DraggableWindowView.swift; sourceTree = "<group>"; };
|
||||||
A5CBD0572C9F30860017A1AE /* Cursor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Cursor.swift; sourceTree = "<group>"; };
|
A5CBD0572C9F30860017A1AE /* Cursor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Cursor.swift; sourceTree = "<group>"; };
|
||||||
A5CBD06A2CA322320017A1AE /* GlobalEventTap.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GlobalEventTap.swift; sourceTree = "<group>"; };
|
|
||||||
A5CBD05B2CA0C5C70017A1AE /* SlideTerminal.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = SlideTerminal.xib; sourceTree = "<group>"; };
|
A5CBD05B2CA0C5C70017A1AE /* SlideTerminal.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = SlideTerminal.xib; sourceTree = "<group>"; };
|
||||||
A5CBD05D2CA0C5E70017A1AE /* SlideTerminalController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SlideTerminalController.swift; sourceTree = "<group>"; };
|
A5CBD05D2CA0C5E70017A1AE /* SlideTerminalController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SlideTerminalController.swift; sourceTree = "<group>"; };
|
||||||
A5CBD05F2CA0C9080017A1AE /* SlideTerminalWindow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SlideTerminalWindow.swift; sourceTree = "<group>"; };
|
A5CBD05F2CA0C9080017A1AE /* SlideTerminalWindow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SlideTerminalWindow.swift; sourceTree = "<group>"; };
|
||||||
A5CBD0632CA122E70017A1AE /* SlideTerminalPosition.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SlideTerminalPosition.swift; sourceTree = "<group>"; };
|
A5CBD0632CA122E70017A1AE /* SlideTerminalPosition.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SlideTerminalPosition.swift; sourceTree = "<group>"; };
|
||||||
|
A5CBD06A2CA322320017A1AE /* GlobalEventTap.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GlobalEventTap.swift; sourceTree = "<group>"; };
|
||||||
A5CC36122C9CD729004D6760 /* SecureInputOverlay.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecureInputOverlay.swift; sourceTree = "<group>"; };
|
A5CC36122C9CD729004D6760 /* SecureInputOverlay.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecureInputOverlay.swift; sourceTree = "<group>"; };
|
||||||
A5CC36142C9CDA03004D6760 /* View+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "View+Extension.swift"; sourceTree = "<group>"; };
|
A5CC36142C9CDA03004D6760 /* View+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "View+Extension.swift"; sourceTree = "<group>"; };
|
||||||
A5CDF1902AAF9A5800513312 /* ConfigurationErrors.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = ConfigurationErrors.xib; sourceTree = "<group>"; };
|
A5CDF1902AAF9A5800513312 /* ConfigurationErrors.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = ConfigurationErrors.xib; sourceTree = "<group>"; };
|
||||||
@ -342,6 +344,7 @@
|
|||||||
A51B78462AF4B58B00F3EDB9 /* TerminalWindow.swift */,
|
A51B78462AF4B58B00F3EDB9 /* TerminalWindow.swift */,
|
||||||
AEF9CE232B6AD07A0017E195 /* TerminalToolbar.swift */,
|
AEF9CE232B6AD07A0017E195 /* TerminalToolbar.swift */,
|
||||||
A535B9D9299C569B0017E2E4 /* ErrorView.swift */,
|
A535B9D9299C569B0017E2E4 /* ErrorView.swift */,
|
||||||
|
A54D786B2CA79788001B19B1 /* BaseTerminalController.swift */,
|
||||||
);
|
);
|
||||||
path = Terminal;
|
path = Terminal;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
@ -382,14 +385,6 @@
|
|||||||
name = Products;
|
name = Products;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
};
|
};
|
||||||
A5CBD0672CA2704E0017A1AE /* Global Keybinds */ = {
|
|
||||||
isa = PBXGroup;
|
|
||||||
children = (
|
|
||||||
A5CBD06A2CA322320017A1AE /* GlobalEventTap.swift */,
|
|
||||||
);
|
|
||||||
path = "Global Keybinds";
|
|
||||||
sourceTree = "<group>";
|
|
||||||
};
|
|
||||||
A5CBD05A2CA0C5910017A1AE /* SlideTerminal */ = {
|
A5CBD05A2CA0C5910017A1AE /* SlideTerminal */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
@ -401,6 +396,14 @@
|
|||||||
path = SlideTerminal;
|
path = SlideTerminal;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
};
|
};
|
||||||
|
A5CBD0672CA2704E0017A1AE /* Global Keybinds */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
A5CBD06A2CA322320017A1AE /* GlobalEventTap.swift */,
|
||||||
|
);
|
||||||
|
path = "Global Keybinds";
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
A5CEAFDA29B8005900646FDA /* SplitView */ = {
|
A5CEAFDA29B8005900646FDA /* SplitView */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
@ -547,6 +550,7 @@
|
|||||||
files = (
|
files = (
|
||||||
A59630A42AF059BB00D64628 /* Ghostty.SplitNode.swift in Sources */,
|
A59630A42AF059BB00D64628 /* Ghostty.SplitNode.swift in Sources */,
|
||||||
A514C8D62B54A16400493A16 /* Ghostty.Config.swift in Sources */,
|
A514C8D62B54A16400493A16 /* Ghostty.Config.swift in Sources */,
|
||||||
|
A54D786C2CA7978E001B19B1 /* BaseTerminalController.swift in Sources */,
|
||||||
A59FB5CF2AE0DB50009128F3 /* InspectorView.swift in Sources */,
|
A59FB5CF2AE0DB50009128F3 /* InspectorView.swift in Sources */,
|
||||||
A5D0AF3D2B37804400D21823 /* CodableBridge.swift in Sources */,
|
A5D0AF3D2B37804400D21823 /* CodableBridge.swift in Sources */,
|
||||||
A5D0AF3B2B36A1DE00D21823 /* TerminalRestorable.swift in Sources */,
|
A5D0AF3B2B36A1DE00D21823 /* TerminalRestorable.swift in Sources */,
|
||||||
|
@ -4,31 +4,19 @@ import SwiftUI
|
|||||||
import GhosttyKit
|
import GhosttyKit
|
||||||
|
|
||||||
/// Controller for the slide-style terminal.
|
/// Controller for the slide-style terminal.
|
||||||
class SlideTerminalController: NSWindowController, NSWindowDelegate, TerminalViewDelegate, TerminalViewModel {
|
class SlideTerminalController: BaseTerminalController {
|
||||||
override var windowNibName: NSNib.Name? { "SlideTerminal" }
|
override var windowNibName: NSNib.Name? { "SlideTerminal" }
|
||||||
|
|
||||||
/// The app instance that this terminal view will represent.
|
|
||||||
let ghostty: Ghostty.App
|
|
||||||
|
|
||||||
/// The position for the slide terminal.
|
/// The position for the slide terminal.
|
||||||
let position: SlideTerminalPosition
|
let position: SlideTerminalPosition
|
||||||
|
|
||||||
/// The surface tree for this window.
|
|
||||||
@Published var surfaceTree: Ghostty.SplitNode? = nil
|
|
||||||
|
|
||||||
init(_ ghostty: Ghostty.App,
|
init(_ ghostty: Ghostty.App,
|
||||||
position: SlideTerminalPosition = .top,
|
position: SlideTerminalPosition = .top,
|
||||||
baseConfig base: Ghostty.SurfaceConfiguration? = nil,
|
baseConfig base: Ghostty.SurfaceConfiguration? = nil,
|
||||||
surfaceTree tree: Ghostty.SplitNode? = nil
|
surfaceTree tree: Ghostty.SplitNode? = nil
|
||||||
) {
|
) {
|
||||||
self.ghostty = ghostty
|
|
||||||
self.position = position
|
self.position = position
|
||||||
|
super.init(ghostty, baseConfig: base, surfaceTree: tree)
|
||||||
super.init(window: nil)
|
|
||||||
|
|
||||||
// Initialize our initial surface.
|
|
||||||
guard let ghostty_app = ghostty.app else { preconditionFailure("app must be loaded") }
|
|
||||||
self.surfaceTree = tree ?? .leaf(.init(ghostty_app, baseConfig: base))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
required init?(coder: NSCoder) {
|
required init?(coder: NSCoder) {
|
||||||
@ -61,7 +49,8 @@ class SlideTerminalController: NSWindowController, NSWindowDelegate, TerminalVie
|
|||||||
|
|
||||||
// MARK: NSWindowDelegate
|
// MARK: NSWindowDelegate
|
||||||
|
|
||||||
func windowDidResignKey(_ notification: Notification) {
|
override func windowDidResignKey(_ notification: Notification) {
|
||||||
|
super.windowDidResignKey(notification)
|
||||||
slideOut()
|
slideOut()
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -70,15 +59,13 @@ class SlideTerminalController: NSWindowController, NSWindowDelegate, TerminalVie
|
|||||||
return position.restrictFrameSize(frameSize, on: screen)
|
return position.restrictFrameSize(frameSize, on: screen)
|
||||||
}
|
}
|
||||||
|
|
||||||
//MARK: TerminalViewDelegate
|
// MARK: Base Controller Overrides
|
||||||
|
|
||||||
func cellSizeDidChange(to: NSSize) {
|
override func surfaceTreeDidChange(from: Ghostty.SplitNode?, to: Ghostty.SplitNode?) {
|
||||||
guard ghostty.config.windowStepResize else { return }
|
super.surfaceTreeDidChange(from: from, to: to)
|
||||||
self.window?.contentResizeIncrements = to
|
|
||||||
}
|
|
||||||
|
|
||||||
func surfaceTreeDidChange() {
|
// If our surface tree is now nil then we close our window.
|
||||||
if (surfaceTree == nil) {
|
if (to == nil) {
|
||||||
self.window?.close()
|
self.window?.close()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
361
macos/Sources/Features/Terminal/BaseTerminalController.swift
Normal file
361
macos/Sources/Features/Terminal/BaseTerminalController.swift
Normal file
@ -0,0 +1,361 @@
|
|||||||
|
import Cocoa
|
||||||
|
import SwiftUI
|
||||||
|
import GhosttyKit
|
||||||
|
|
||||||
|
/// A base class for windows that can contain Ghostty windows. This base class implements
|
||||||
|
/// the bare minimum functionality that every terminal window in Ghostty should implement.
|
||||||
|
///
|
||||||
|
/// Usage: Specify this as the base class of your window controller for the window that contains
|
||||||
|
/// a terminal. The window controller must also be the window delegate OR the window delegate
|
||||||
|
/// functions on this base class must be called by your own custom delegate. For the terminal
|
||||||
|
/// view the TerminalView SwiftUI view must be used and this class is the view model and
|
||||||
|
/// delegate.
|
||||||
|
///
|
||||||
|
/// Notably, things this class does NOT implement (not exhaustive):
|
||||||
|
///
|
||||||
|
/// - Tabbing, because there are many ways to get tabbed behavior in macOS and we
|
||||||
|
/// don't want to be opinionated about it.
|
||||||
|
/// - Fullscreen
|
||||||
|
/// - Window restoration or save state
|
||||||
|
/// - Window visual styles (such as titlebar colors)
|
||||||
|
///
|
||||||
|
/// The primary idea of all the behaviors we don't implement here are that subclasses may not
|
||||||
|
/// want these behaviors.
|
||||||
|
class BaseTerminalController: NSWindowController,
|
||||||
|
NSWindowDelegate,
|
||||||
|
TerminalViewDelegate,
|
||||||
|
TerminalViewModel,
|
||||||
|
ClipboardConfirmationViewDelegate
|
||||||
|
{
|
||||||
|
/// The app instance that this terminal view will represent.
|
||||||
|
let ghostty: Ghostty.App
|
||||||
|
|
||||||
|
/// The currently focused surface.
|
||||||
|
var focusedSurface: Ghostty.SurfaceView? = nil {
|
||||||
|
didSet { syncFocusToSurfaceTree() }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The surface tree for this window.
|
||||||
|
@Published var surfaceTree: Ghostty.SplitNode? = nil {
|
||||||
|
didSet { surfaceTreeDidChange(from: oldValue, to: surfaceTree) }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Non-nil when an alert is active so we don't overlap multiple.
|
||||||
|
private var alert: NSAlert? = nil
|
||||||
|
|
||||||
|
/// The clipboard confirmation window, if shown.
|
||||||
|
private var clipboardConfirmation: ClipboardConfirmationController? = nil
|
||||||
|
|
||||||
|
required init?(coder: NSCoder) {
|
||||||
|
fatalError("init(coder:) is not supported for this view")
|
||||||
|
}
|
||||||
|
|
||||||
|
init(_ ghostty: Ghostty.App,
|
||||||
|
baseConfig base: Ghostty.SurfaceConfiguration? = nil,
|
||||||
|
surfaceTree tree: Ghostty.SplitNode? = nil
|
||||||
|
) {
|
||||||
|
self.ghostty = ghostty
|
||||||
|
|
||||||
|
super.init(window: nil)
|
||||||
|
|
||||||
|
// Initialize our initial surface.
|
||||||
|
guard let ghostty_app = ghostty.app else { preconditionFailure("app must be loaded") }
|
||||||
|
self.surfaceTree = tree ?? .leaf(.init(ghostty_app, baseConfig: base))
|
||||||
|
|
||||||
|
// Setup our notifications for behaviors
|
||||||
|
let center = NotificationCenter.default
|
||||||
|
center.addObserver(
|
||||||
|
self,
|
||||||
|
selector: #selector(onConfirmClipboardRequest),
|
||||||
|
name: Ghostty.Notification.confirmClipboard,
|
||||||
|
object: nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Called when the surfaceTree variable changed.
|
||||||
|
///
|
||||||
|
/// Subclasses should call super first.
|
||||||
|
func surfaceTreeDidChange(from: Ghostty.SplitNode?, to: Ghostty.SplitNode?) {
|
||||||
|
// If our surface tree becomes nil then ensure all surfaces
|
||||||
|
// in the old tree have closed.
|
||||||
|
if (to == nil) {
|
||||||
|
from?.close()
|
||||||
|
focusedSurface = nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Update all surfaces with the focus state. This ensures that libghostty has an accurate view about
|
||||||
|
/// what surface is focused. This must be called whenever a surface OR window changes focus.
|
||||||
|
func syncFocusToSurfaceTree() {
|
||||||
|
guard let tree = self.surfaceTree else { return }
|
||||||
|
|
||||||
|
for leaf in tree {
|
||||||
|
// Our focus state requires that this window is key and our currently
|
||||||
|
// focused surface is the surface in this leaf.
|
||||||
|
let focused: Bool = (window?.isKeyWindow ?? false) &&
|
||||||
|
focusedSurface != nil &&
|
||||||
|
leaf.surface == focusedSurface!
|
||||||
|
leaf.surface.focusDidChange(focused)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: TerminalViewDelegate
|
||||||
|
|
||||||
|
// Note: this is different from surfaceDidTreeChange(from:,to:) because this is called
|
||||||
|
// when the currently set value changed in place and the from:to: variant is called
|
||||||
|
// when the variable was set.
|
||||||
|
func surfaceTreeDidChange() {}
|
||||||
|
|
||||||
|
func focusedSurfaceDidChange(to: Ghostty.SurfaceView?) {
|
||||||
|
focusedSurface = to
|
||||||
|
}
|
||||||
|
|
||||||
|
func titleDidChange(to: String) {
|
||||||
|
guard let window else { return }
|
||||||
|
|
||||||
|
// Set the main window title
|
||||||
|
window.title = to
|
||||||
|
}
|
||||||
|
|
||||||
|
func cellSizeDidChange(to: NSSize) {
|
||||||
|
guard ghostty.config.windowStepResize else { return }
|
||||||
|
self.window?.contentResizeIncrements = to
|
||||||
|
}
|
||||||
|
|
||||||
|
func zoomStateDidChange(to: Bool) {}
|
||||||
|
|
||||||
|
// MARK: Clipboard Confirmation
|
||||||
|
|
||||||
|
@objc private func onConfirmClipboardRequest(notification: SwiftUI.Notification) {
|
||||||
|
guard let target = notification.object as? Ghostty.SurfaceView else { return }
|
||||||
|
guard target == self.focusedSurface else { return }
|
||||||
|
guard let surface = target.surface else { return }
|
||||||
|
|
||||||
|
// We need a window
|
||||||
|
guard let window = self.window else { return }
|
||||||
|
|
||||||
|
// Check whether we use non-native fullscreen
|
||||||
|
guard let str = notification.userInfo?[Ghostty.Notification.ConfirmClipboardStrKey] as? String else { return }
|
||||||
|
guard let state = notification.userInfo?[Ghostty.Notification.ConfirmClipboardStateKey] as? UnsafeMutableRawPointer? else { return }
|
||||||
|
guard let request = notification.userInfo?[Ghostty.Notification.ConfirmClipboardRequestKey] as? Ghostty.ClipboardRequest else { return }
|
||||||
|
|
||||||
|
// If we already have a clipboard confirmation view up, we ignore this request.
|
||||||
|
// This shouldn't be possible...
|
||||||
|
guard self.clipboardConfirmation == nil else {
|
||||||
|
Ghostty.App.completeClipboardRequest(surface, data: "", state: state, confirmed: true)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show our paste confirmation
|
||||||
|
self.clipboardConfirmation = ClipboardConfirmationController(
|
||||||
|
surface: surface,
|
||||||
|
contents: str,
|
||||||
|
request: request,
|
||||||
|
state: state,
|
||||||
|
delegate: self
|
||||||
|
)
|
||||||
|
window.beginSheet(self.clipboardConfirmation!.window!)
|
||||||
|
}
|
||||||
|
|
||||||
|
func clipboardConfirmationComplete(_ action: ClipboardConfirmationView.Action, _ request: Ghostty.ClipboardRequest) {
|
||||||
|
// End our clipboard confirmation no matter what
|
||||||
|
guard let cc = self.clipboardConfirmation else { return }
|
||||||
|
self.clipboardConfirmation = nil
|
||||||
|
|
||||||
|
// Close the sheet
|
||||||
|
if let ccWindow = cc.window {
|
||||||
|
window?.endSheet(ccWindow)
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (request) {
|
||||||
|
case .osc_52_write:
|
||||||
|
guard case .confirm = action else { break }
|
||||||
|
let pb = NSPasteboard.general
|
||||||
|
pb.declareTypes([.string], owner: nil)
|
||||||
|
pb.setString(cc.contents, forType: .string)
|
||||||
|
case .osc_52_read, .paste:
|
||||||
|
let str: String
|
||||||
|
switch (action) {
|
||||||
|
case .cancel:
|
||||||
|
str = ""
|
||||||
|
|
||||||
|
case .confirm:
|
||||||
|
str = cc.contents
|
||||||
|
}
|
||||||
|
|
||||||
|
Ghostty.App.completeClipboardRequest(cc.surface, data: str, state: cc.state, confirmed: true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//MARK: - NSWindowDelegate
|
||||||
|
|
||||||
|
// This is called when performClose is called on a window (NOT when close()
|
||||||
|
// is called directly). performClose is called primarily when UI elements such
|
||||||
|
// as the "red X" are pressed.
|
||||||
|
func windowShouldClose(_ sender: NSWindow) -> Bool {
|
||||||
|
// We must have a window. Is it even possible not to?
|
||||||
|
guard let window = self.window else { return true }
|
||||||
|
|
||||||
|
// If we have no surfaces, close.
|
||||||
|
guard let node = self.surfaceTree else { return true }
|
||||||
|
|
||||||
|
// If we already have an alert, continue with it
|
||||||
|
guard alert == nil else { return false }
|
||||||
|
|
||||||
|
// If our surfaces don't require confirmation, close.
|
||||||
|
if (!node.needsConfirmQuit()) { return true }
|
||||||
|
|
||||||
|
// We require confirmation, so show an alert as long as we aren't already.
|
||||||
|
let alert = NSAlert()
|
||||||
|
alert.messageText = "Close Terminal?"
|
||||||
|
alert.informativeText = "The terminal still has a running process. If you close the " +
|
||||||
|
"terminal the process will be killed."
|
||||||
|
alert.addButton(withTitle: "Close the Terminal")
|
||||||
|
alert.addButton(withTitle: "Cancel")
|
||||||
|
alert.alertStyle = .warning
|
||||||
|
alert.beginSheetModal(for: window, completionHandler: { response in
|
||||||
|
self.alert = nil
|
||||||
|
switch (response) {
|
||||||
|
case .alertFirstButtonReturn:
|
||||||
|
window.close()
|
||||||
|
|
||||||
|
default:
|
||||||
|
break
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
self.alert = alert
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func windowWillClose(_ notification: Notification) {
|
||||||
|
// I don't know if this is required anymore. We previously had a ref cycle between
|
||||||
|
// the view and the window so we had to nil this out to break it but I think this
|
||||||
|
// may now be resolved. We should verify that no memory leaks and we can remove this.
|
||||||
|
self.window?.contentView = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func windowDidBecomeKey(_ notification: Notification) {
|
||||||
|
// Becoming/losing key means we have to notify our surface(s) that we have focus
|
||||||
|
// so things like cursors blink, pty events are sent, etc.
|
||||||
|
self.syncFocusToSurfaceTree()
|
||||||
|
}
|
||||||
|
|
||||||
|
func windowDidResignKey(_ notification: Notification) {
|
||||||
|
// Becoming/losing key means we have to notify our surface(s) that we have focus
|
||||||
|
// so things like cursors blink, pty events are sent, etc.
|
||||||
|
self.syncFocusToSurfaceTree()
|
||||||
|
}
|
||||||
|
|
||||||
|
func windowDidChangeOcclusionState(_ notification: Notification) {
|
||||||
|
guard let surfaceTree = self.surfaceTree else { return }
|
||||||
|
let visible = self.window?.occlusionState.contains(.visible) ?? false
|
||||||
|
for leaf in surfaceTree {
|
||||||
|
if let surface = leaf.surface.surface {
|
||||||
|
ghostty_surface_set_occlusion(surface, visible)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: First Responder
|
||||||
|
|
||||||
|
@IBAction func close(_ sender: Any) {
|
||||||
|
guard let surface = focusedSurface?.surface else { return }
|
||||||
|
ghostty.requestClose(surface: surface)
|
||||||
|
}
|
||||||
|
|
||||||
|
@IBAction func closeWindow(_ sender: Any) {
|
||||||
|
guard let window = window else { return }
|
||||||
|
window.performClose(sender)
|
||||||
|
}
|
||||||
|
|
||||||
|
@IBAction func splitRight(_ sender: Any) {
|
||||||
|
guard let surface = focusedSurface?.surface else { return }
|
||||||
|
ghostty.split(surface: surface, direction: GHOSTTY_SPLIT_DIRECTION_RIGHT)
|
||||||
|
}
|
||||||
|
|
||||||
|
@IBAction func splitDown(_ sender: Any) {
|
||||||
|
guard let surface = focusedSurface?.surface else { return }
|
||||||
|
ghostty.split(surface: surface, direction: GHOSTTY_SPLIT_DIRECTION_DOWN)
|
||||||
|
}
|
||||||
|
|
||||||
|
@IBAction func splitZoom(_ sender: Any) {
|
||||||
|
guard let surface = focusedSurface?.surface else { return }
|
||||||
|
ghostty.splitToggleZoom(surface: surface)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@IBAction func splitMoveFocusPrevious(_ sender: Any) {
|
||||||
|
splitMoveFocus(direction: .previous)
|
||||||
|
}
|
||||||
|
|
||||||
|
@IBAction func splitMoveFocusNext(_ sender: Any) {
|
||||||
|
splitMoveFocus(direction: .next)
|
||||||
|
}
|
||||||
|
|
||||||
|
@IBAction func splitMoveFocusAbove(_ sender: Any) {
|
||||||
|
splitMoveFocus(direction: .top)
|
||||||
|
}
|
||||||
|
|
||||||
|
@IBAction func splitMoveFocusBelow(_ sender: Any) {
|
||||||
|
splitMoveFocus(direction: .bottom)
|
||||||
|
}
|
||||||
|
|
||||||
|
@IBAction func splitMoveFocusLeft(_ sender: Any) {
|
||||||
|
splitMoveFocus(direction: .left)
|
||||||
|
}
|
||||||
|
|
||||||
|
@IBAction func splitMoveFocusRight(_ sender: Any) {
|
||||||
|
splitMoveFocus(direction: .right)
|
||||||
|
}
|
||||||
|
|
||||||
|
@IBAction func equalizeSplits(_ sender: Any) {
|
||||||
|
guard let surface = focusedSurface?.surface else { return }
|
||||||
|
ghostty.splitEqualize(surface: surface)
|
||||||
|
}
|
||||||
|
|
||||||
|
@IBAction func moveSplitDividerUp(_ sender: Any) {
|
||||||
|
guard let surface = focusedSurface?.surface else { return }
|
||||||
|
ghostty.splitResize(surface: surface, direction: .up, amount: 10)
|
||||||
|
}
|
||||||
|
|
||||||
|
@IBAction func moveSplitDividerDown(_ sender: Any) {
|
||||||
|
guard let surface = focusedSurface?.surface else { return }
|
||||||
|
ghostty.splitResize(surface: surface, direction: .down, amount: 10)
|
||||||
|
}
|
||||||
|
|
||||||
|
@IBAction func moveSplitDividerLeft(_ sender: Any) {
|
||||||
|
guard let surface = focusedSurface?.surface else { return }
|
||||||
|
ghostty.splitResize(surface: surface, direction: .left, amount: 10)
|
||||||
|
}
|
||||||
|
|
||||||
|
@IBAction func moveSplitDividerRight(_ sender: Any) {
|
||||||
|
guard let surface = focusedSurface?.surface else { return }
|
||||||
|
ghostty.splitResize(surface: surface, direction: .right, amount: 10)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func splitMoveFocus(direction: Ghostty.SplitFocusDirection) {
|
||||||
|
guard let surface = focusedSurface?.surface else { return }
|
||||||
|
ghostty.splitMoveFocus(surface: surface, direction: direction)
|
||||||
|
}
|
||||||
|
|
||||||
|
@IBAction func increaseFontSize(_ sender: Any) {
|
||||||
|
guard let surface = focusedSurface?.surface else { return }
|
||||||
|
ghostty.changeFontSize(surface: surface, .increase(1))
|
||||||
|
}
|
||||||
|
|
||||||
|
@IBAction func decreaseFontSize(_ sender: Any) {
|
||||||
|
guard let surface = focusedSurface?.surface else { return }
|
||||||
|
ghostty.changeFontSize(surface: surface, .decrease(1))
|
||||||
|
}
|
||||||
|
|
||||||
|
@IBAction func resetFontSize(_ sender: Any) {
|
||||||
|
guard let surface = focusedSurface?.surface else { return }
|
||||||
|
ghostty.changeFontSize(surface: surface, .reset)
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc func resetTerminal(_ sender: Any) {
|
||||||
|
guard let surface = focusedSurface?.surface else { return }
|
||||||
|
ghostty.resetTerminal(surface: surface)
|
||||||
|
}
|
||||||
|
}
|
@ -3,45 +3,14 @@ import Cocoa
|
|||||||
import SwiftUI
|
import SwiftUI
|
||||||
import GhosttyKit
|
import GhosttyKit
|
||||||
|
|
||||||
/// The terminal controller is an NSWindowController that maps 1:1 to a terminal window.
|
/// A classic, tabbed terminal experience.
|
||||||
class TerminalController: NSWindowController, NSWindowDelegate,
|
class TerminalController: BaseTerminalController
|
||||||
TerminalViewDelegate, TerminalViewModel,
|
|
||||||
ClipboardConfirmationViewDelegate
|
|
||||||
{
|
{
|
||||||
override var windowNibName: NSNib.Name? { "Terminal" }
|
override var windowNibName: NSNib.Name? { "Terminal" }
|
||||||
|
|
||||||
/// The app instance that this terminal view will represent.
|
|
||||||
let ghostty: Ghostty.App
|
|
||||||
|
|
||||||
/// The currently focused surface.
|
|
||||||
var focusedSurface: Ghostty.SurfaceView? = nil {
|
|
||||||
didSet {
|
|
||||||
syncFocusToSurfaceTree()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The surface tree for this window.
|
|
||||||
@Published var surfaceTree: Ghostty.SplitNode? = nil {
|
|
||||||
didSet {
|
|
||||||
// If our surface tree becomes nil then ensure all surfaces
|
|
||||||
// in the old tree have closed and then close the window.
|
|
||||||
if (surfaceTree == nil) {
|
|
||||||
oldValue?.close()
|
|
||||||
focusedSurface = nil
|
|
||||||
lastSurfaceDidClose()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Fullscreen state management.
|
/// Fullscreen state management.
|
||||||
let fullscreenHandler = FullScreenHandler()
|
let fullscreenHandler = FullScreenHandler()
|
||||||
|
|
||||||
/// True when an alert is active so we don't overlap multiple.
|
|
||||||
private var alert: NSAlert? = nil
|
|
||||||
|
|
||||||
/// The clipboard confirmation window, if shown.
|
|
||||||
private var clipboardConfirmation: ClipboardConfirmationController? = nil
|
|
||||||
|
|
||||||
/// This is set to true when we care about frame changes. This is a small optimization since
|
/// This is set to true when we care about frame changes. This is a small optimization since
|
||||||
/// this controller registers a listener for ALL frame change notifications and this lets us bail
|
/// this controller registers a listener for ALL frame change notifications and this lets us bail
|
||||||
/// early if we don't care.
|
/// early if we don't care.
|
||||||
@ -59,8 +28,6 @@ class TerminalController: NSWindowController, NSWindowDelegate,
|
|||||||
withBaseConfig base: Ghostty.SurfaceConfiguration? = nil,
|
withBaseConfig base: Ghostty.SurfaceConfiguration? = nil,
|
||||||
withSurfaceTree tree: Ghostty.SplitNode? = nil
|
withSurfaceTree tree: Ghostty.SplitNode? = nil
|
||||||
) {
|
) {
|
||||||
self.ghostty = ghostty
|
|
||||||
|
|
||||||
// The window we manage is not restorable if we've specified a command
|
// The window we manage is not restorable if we've specified a command
|
||||||
// to execute. We do this because the restored window is meaningless at the
|
// to execute. We do this because the restored window is meaningless at the
|
||||||
// time of writing this: it'd just restore to a shell in the same directory
|
// time of writing this: it'd just restore to a shell in the same directory
|
||||||
@ -68,11 +35,7 @@ class TerminalController: NSWindowController, NSWindowDelegate,
|
|||||||
// restoration.
|
// restoration.
|
||||||
self.restorable = (base?.command ?? "") == ""
|
self.restorable = (base?.command ?? "") == ""
|
||||||
|
|
||||||
super.init(window: nil)
|
super.init(ghostty, baseConfig: base, surfaceTree: tree)
|
||||||
|
|
||||||
// Initialize our initial surface.
|
|
||||||
guard let ghostty_app = ghostty.app else { preconditionFailure("app must be loaded") }
|
|
||||||
self.surfaceTree = tree ?? .leaf(.init(ghostty_app, baseConfig: base))
|
|
||||||
|
|
||||||
// Setup our notifications for behaviors
|
// Setup our notifications for behaviors
|
||||||
let center = NotificationCenter.default
|
let center = NotificationCenter.default
|
||||||
@ -86,11 +49,6 @@ class TerminalController: NSWindowController, NSWindowDelegate,
|
|||||||
selector: #selector(onGotoTab),
|
selector: #selector(onGotoTab),
|
||||||
name: Ghostty.Notification.ghosttyGotoTab,
|
name: Ghostty.Notification.ghosttyGotoTab,
|
||||||
object: nil)
|
object: nil)
|
||||||
center.addObserver(
|
|
||||||
self,
|
|
||||||
selector: #selector(onConfirmClipboardRequest),
|
|
||||||
name: Ghostty.Notification.confirmClipboard,
|
|
||||||
object: nil)
|
|
||||||
center.addObserver(
|
center.addObserver(
|
||||||
self,
|
self,
|
||||||
selector: #selector(onFrameDidChange),
|
selector: #selector(onFrameDidChange),
|
||||||
@ -108,6 +66,17 @@ class TerminalController: NSWindowController, NSWindowDelegate,
|
|||||||
center.removeObserver(self)
|
center.removeObserver(self)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MARK: Base Controller Overrides
|
||||||
|
|
||||||
|
override func surfaceTreeDidChange(from: Ghostty.SplitNode?, to: Ghostty.SplitNode?) {
|
||||||
|
super.surfaceTreeDidChange(from: from, to: to)
|
||||||
|
|
||||||
|
// If our surface tree is now nil then we close our window.
|
||||||
|
if (to == nil) {
|
||||||
|
self.window?.close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
//MARK: - Methods
|
//MARK: - Methods
|
||||||
|
|
||||||
func configDidReload() {
|
func configDidReload() {
|
||||||
@ -230,21 +199,6 @@ class TerminalController: NSWindowController, NSWindowDelegate,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Update all surfaces with the focus state. This ensures that libghostty has an accurate view about
|
|
||||||
/// what surface is focused. This must be called whenever a surface OR window changes focus.
|
|
||||||
private func syncFocusToSurfaceTree() {
|
|
||||||
guard let tree = self.surfaceTree else { return }
|
|
||||||
|
|
||||||
for leaf in tree {
|
|
||||||
// Our focus state requires that this window is key and our currently
|
|
||||||
// focused surface is the surface in this leaf.
|
|
||||||
let focused: Bool = (window?.isKeyWindow ?? false) &&
|
|
||||||
focusedSurface != nil &&
|
|
||||||
leaf.surface == focusedSurface!
|
|
||||||
leaf.surface.focusDidChange(focused)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
//MARK: - NSWindowController
|
//MARK: - NSWindowController
|
||||||
|
|
||||||
override func windowWillLoad() {
|
override func windowWillLoad() {
|
||||||
@ -397,84 +351,21 @@ class TerminalController: NSWindowController, NSWindowDelegate,
|
|||||||
|
|
||||||
//MARK: - NSWindowDelegate
|
//MARK: - NSWindowDelegate
|
||||||
|
|
||||||
// This is called when performClose is called on a window (NOT when close()
|
override func windowWillClose(_ notification: Notification) {
|
||||||
// is called directly). performClose is called primarily when UI elements such
|
super.windowWillClose(notification)
|
||||||
// as the "red X" are pressed.
|
|
||||||
func windowShouldClose(_ sender: NSWindow) -> Bool {
|
|
||||||
// We must have a window. Is it even possible not to?
|
|
||||||
guard let window = self.window else { return true }
|
|
||||||
|
|
||||||
// If we have no surfaces, close.
|
|
||||||
guard let node = self.surfaceTree else { return true }
|
|
||||||
|
|
||||||
// If we already have an alert, continue with it
|
|
||||||
guard alert == nil else { return false }
|
|
||||||
|
|
||||||
// If our surfaces don't require confirmation, close.
|
|
||||||
if (!node.needsConfirmQuit()) { return true }
|
|
||||||
|
|
||||||
// We require confirmation, so show an alert as long as we aren't already.
|
|
||||||
let alert = NSAlert()
|
|
||||||
alert.messageText = "Close Terminal?"
|
|
||||||
alert.informativeText = "The terminal still has a running process. If you close the " +
|
|
||||||
"terminal the process will be killed."
|
|
||||||
alert.addButton(withTitle: "Close the Terminal")
|
|
||||||
alert.addButton(withTitle: "Cancel")
|
|
||||||
alert.alertStyle = .warning
|
|
||||||
alert.beginSheetModal(for: window, completionHandler: { response in
|
|
||||||
self.alert = nil
|
|
||||||
switch (response) {
|
|
||||||
case .alertFirstButtonReturn:
|
|
||||||
window.close()
|
|
||||||
|
|
||||||
default:
|
|
||||||
break
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
self.alert = alert
|
|
||||||
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
func windowWillClose(_ notification: Notification) {
|
|
||||||
// I don't know if this is required anymore. We previously had a ref cycle between
|
|
||||||
// the view and the window so we had to nil this out to break it but I think this
|
|
||||||
// may now be resolved. We should verify that no memory leaks and we can remove this.
|
|
||||||
self.window?.contentView = nil
|
|
||||||
|
|
||||||
self.relabelTabs()
|
self.relabelTabs()
|
||||||
}
|
}
|
||||||
|
|
||||||
func windowDidBecomeKey(_ notification: Notification) {
|
override func windowDidBecomeKey(_ notification: Notification) {
|
||||||
|
super.windowDidBecomeKey(notification)
|
||||||
self.relabelTabs()
|
self.relabelTabs()
|
||||||
self.fixTabBar()
|
self.fixTabBar()
|
||||||
|
|
||||||
// Becoming/losing key means we have to notify our surface(s) that we have focus
|
|
||||||
// so things like cursors blink, pty events are sent, etc.
|
|
||||||
self.syncFocusToSurfaceTree()
|
|
||||||
}
|
|
||||||
|
|
||||||
func windowDidResignKey(_ notification: Notification) {
|
|
||||||
// Becoming/losing key means we have to notify our surface(s) that we have focus
|
|
||||||
// so things like cursors blink, pty events are sent, etc.
|
|
||||||
self.syncFocusToSurfaceTree()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func windowDidMove(_ notification: Notification) {
|
func windowDidMove(_ notification: Notification) {
|
||||||
self.fixTabBar()
|
self.fixTabBar()
|
||||||
}
|
}
|
||||||
|
|
||||||
func windowDidChangeOcclusionState(_ notification: Notification) {
|
|
||||||
guard let surfaceTree = self.surfaceTree else { return }
|
|
||||||
let visible = self.window?.occlusionState.contains(.visible) ?? false
|
|
||||||
for leaf in surfaceTree {
|
|
||||||
if let surface = leaf.surface.surface {
|
|
||||||
ghostty_surface_set_occlusion(surface, visible)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Called when the window will be encoded. We handle the data encoding here in the
|
// Called when the window will be encoded. We handle the data encoding here in the
|
||||||
// window controller.
|
// window controller.
|
||||||
func window(_ window: NSWindow, willEncodeRestorableState state: NSCoder) {
|
func window(_ window: NSWindow, willEncodeRestorableState state: NSCoder) {
|
||||||
@ -482,7 +373,7 @@ class TerminalController: NSWindowController, NSWindowDelegate,
|
|||||||
data.encode(with: state)
|
data.encode(with: state)
|
||||||
}
|
}
|
||||||
|
|
||||||
//MARK: - First Responder
|
// MARK: First Responder
|
||||||
|
|
||||||
@IBAction func newWindow(_ sender: Any?) {
|
@IBAction func newWindow(_ sender: Any?) {
|
||||||
guard let surface = focusedSurface?.surface else { return }
|
guard let surface = focusedSurface?.surface else { return }
|
||||||
@ -494,12 +385,7 @@ class TerminalController: NSWindowController, NSWindowDelegate,
|
|||||||
ghostty.newTab(surface: surface)
|
ghostty.newTab(surface: surface)
|
||||||
}
|
}
|
||||||
|
|
||||||
@IBAction func close(_ sender: Any) {
|
@IBAction override func closeWindow(_ sender: Any) {
|
||||||
guard let surface = focusedSurface?.surface else { return }
|
|
||||||
ghostty.requestClose(surface: surface)
|
|
||||||
}
|
|
||||||
|
|
||||||
@IBAction func closeWindow(_ sender: Any) {
|
|
||||||
guard let window = window else { return }
|
guard let window = window else { return }
|
||||||
guard let tabGroup = window.tabGroup else {
|
guard let tabGroup = window.tabGroup else {
|
||||||
// No tabs, no tab group, just perform a normal close.
|
// No tabs, no tab group, just perform a normal close.
|
||||||
@ -549,117 +435,23 @@ class TerminalController: NSWindowController, NSWindowDelegate,
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@IBAction func splitRight(_ sender: Any) {
|
|
||||||
guard let surface = focusedSurface?.surface else { return }
|
|
||||||
ghostty.split(surface: surface, direction: GHOSTTY_SPLIT_DIRECTION_RIGHT)
|
|
||||||
}
|
|
||||||
|
|
||||||
@IBAction func splitDown(_ sender: Any) {
|
|
||||||
guard let surface = focusedSurface?.surface else { return }
|
|
||||||
ghostty.split(surface: surface, direction: GHOSTTY_SPLIT_DIRECTION_DOWN)
|
|
||||||
}
|
|
||||||
|
|
||||||
@IBAction func splitZoom(_ sender: Any) {
|
|
||||||
guard let surface = focusedSurface?.surface else { return }
|
|
||||||
ghostty.splitToggleZoom(surface: surface)
|
|
||||||
}
|
|
||||||
|
|
||||||
@IBAction func splitMoveFocusPrevious(_ sender: Any) {
|
|
||||||
splitMoveFocus(direction: .previous)
|
|
||||||
}
|
|
||||||
|
|
||||||
@IBAction func splitMoveFocusNext(_ sender: Any) {
|
|
||||||
splitMoveFocus(direction: .next)
|
|
||||||
}
|
|
||||||
|
|
||||||
@IBAction func splitMoveFocusAbove(_ sender: Any) {
|
|
||||||
splitMoveFocus(direction: .top)
|
|
||||||
}
|
|
||||||
|
|
||||||
@IBAction func splitMoveFocusBelow(_ sender: Any) {
|
|
||||||
splitMoveFocus(direction: .bottom)
|
|
||||||
}
|
|
||||||
|
|
||||||
@IBAction func splitMoveFocusLeft(_ sender: Any) {
|
|
||||||
splitMoveFocus(direction: .left)
|
|
||||||
}
|
|
||||||
|
|
||||||
@IBAction func splitMoveFocusRight(_ sender: Any) {
|
|
||||||
splitMoveFocus(direction: .right)
|
|
||||||
}
|
|
||||||
|
|
||||||
@IBAction func equalizeSplits(_ sender: Any) {
|
|
||||||
guard let surface = focusedSurface?.surface else { return }
|
|
||||||
ghostty.splitEqualize(surface: surface)
|
|
||||||
}
|
|
||||||
|
|
||||||
@IBAction func moveSplitDividerUp(_ sender: Any) {
|
|
||||||
guard let surface = focusedSurface?.surface else { return }
|
|
||||||
ghostty.splitResize(surface: surface, direction: .up, amount: 10)
|
|
||||||
}
|
|
||||||
|
|
||||||
@IBAction func moveSplitDividerDown(_ sender: Any) {
|
|
||||||
guard let surface = focusedSurface?.surface else { return }
|
|
||||||
ghostty.splitResize(surface: surface, direction: .down, amount: 10)
|
|
||||||
}
|
|
||||||
|
|
||||||
@IBAction func moveSplitDividerLeft(_ sender: Any) {
|
|
||||||
guard let surface = focusedSurface?.surface else { return }
|
|
||||||
ghostty.splitResize(surface: surface, direction: .left, amount: 10)
|
|
||||||
}
|
|
||||||
|
|
||||||
@IBAction func moveSplitDividerRight(_ sender: Any) {
|
|
||||||
guard let surface = focusedSurface?.surface else { return }
|
|
||||||
ghostty.splitResize(surface: surface, direction: .right, amount: 10)
|
|
||||||
}
|
|
||||||
|
|
||||||
private func splitMoveFocus(direction: Ghostty.SplitFocusDirection) {
|
|
||||||
guard let surface = focusedSurface?.surface else { return }
|
|
||||||
ghostty.splitMoveFocus(surface: surface, direction: direction)
|
|
||||||
}
|
|
||||||
|
|
||||||
@IBAction func toggleGhosttyFullScreen(_ sender: Any) {
|
@IBAction func toggleGhosttyFullScreen(_ sender: Any) {
|
||||||
guard let surface = focusedSurface?.surface else { return }
|
guard let surface = focusedSurface?.surface else { return }
|
||||||
ghostty.toggleFullscreen(surface: surface)
|
ghostty.toggleFullscreen(surface: surface)
|
||||||
}
|
}
|
||||||
|
|
||||||
@IBAction func increaseFontSize(_ sender: Any) {
|
|
||||||
guard let surface = focusedSurface?.surface else { return }
|
|
||||||
ghostty.changeFontSize(surface: surface, .increase(1))
|
|
||||||
}
|
|
||||||
|
|
||||||
@IBAction func decreaseFontSize(_ sender: Any) {
|
|
||||||
guard let surface = focusedSurface?.surface else { return }
|
|
||||||
ghostty.changeFontSize(surface: surface, .decrease(1))
|
|
||||||
}
|
|
||||||
|
|
||||||
@IBAction func resetFontSize(_ sender: Any) {
|
|
||||||
guard let surface = focusedSurface?.surface else { return }
|
|
||||||
ghostty.changeFontSize(surface: surface, .reset)
|
|
||||||
}
|
|
||||||
|
|
||||||
@IBAction func toggleTerminalInspector(_ sender: Any) {
|
@IBAction func toggleTerminalInspector(_ sender: Any) {
|
||||||
guard let surface = focusedSurface?.surface else { return }
|
guard let surface = focusedSurface?.surface else { return }
|
||||||
ghostty.toggleTerminalInspector(surface: surface)
|
ghostty.toggleTerminalInspector(surface: surface)
|
||||||
}
|
}
|
||||||
|
|
||||||
@objc func resetTerminal(_ sender: Any) {
|
|
||||||
guard let surface = focusedSurface?.surface else { return }
|
|
||||||
ghostty.resetTerminal(surface: surface)
|
|
||||||
}
|
|
||||||
|
|
||||||
//MARK: - TerminalViewDelegate
|
//MARK: - TerminalViewDelegate
|
||||||
|
|
||||||
func focusedSurfaceDidChange(to: Ghostty.SurfaceView?) {
|
override func titleDidChange(to: String) {
|
||||||
self.focusedSurface = to
|
super.titleDidChange(to: to)
|
||||||
}
|
|
||||||
|
|
||||||
func titleDidChange(to: String) {
|
|
||||||
guard let window = window as? TerminalWindow else { return }
|
guard let window = window as? TerminalWindow else { return }
|
||||||
|
|
||||||
// Set the main window title
|
|
||||||
window.title = to
|
|
||||||
|
|
||||||
// Custom toolbar-based title used when titlebar tabs are enabled.
|
// Custom toolbar-based title used when titlebar tabs are enabled.
|
||||||
if let toolbar = window.toolbar as? TerminalToolbar {
|
if let toolbar = window.toolbar as? TerminalToolbar {
|
||||||
if (window.titlebarTabs || ghostty.config.macosTitlebarStyle == "hidden") {
|
if (window.titlebarTabs || ghostty.config.macosTitlebarStyle == "hidden") {
|
||||||
@ -672,58 +464,17 @@ class TerminalController: NSWindowController, NSWindowDelegate,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func cellSizeDidChange(to: NSSize) {
|
override func surfaceTreeDidChange() {
|
||||||
guard ghostty.config.windowStepResize else { return }
|
|
||||||
self.window?.contentResizeIncrements = to
|
|
||||||
}
|
|
||||||
|
|
||||||
func lastSurfaceDidClose() {
|
|
||||||
self.window?.close()
|
|
||||||
}
|
|
||||||
|
|
||||||
func surfaceTreeDidChange() {
|
|
||||||
// Whenever our surface tree changes in any way (new split, close split, etc.)
|
// Whenever our surface tree changes in any way (new split, close split, etc.)
|
||||||
// we want to invalidate our state.
|
// we want to invalidate our state.
|
||||||
invalidateRestorableState()
|
invalidateRestorableState()
|
||||||
}
|
}
|
||||||
|
|
||||||
func zoomStateDidChange(to: Bool) {
|
override func zoomStateDidChange(to: Bool) {
|
||||||
guard let window = window as? TerminalWindow else { return }
|
guard let window = window as? TerminalWindow else { return }
|
||||||
window.surfaceIsZoomed = to
|
window.surfaceIsZoomed = to
|
||||||
}
|
}
|
||||||
|
|
||||||
//MARK: - Clipboard Confirmation
|
|
||||||
|
|
||||||
func clipboardConfirmationComplete(_ action: ClipboardConfirmationView.Action, _ request: Ghostty.ClipboardRequest) {
|
|
||||||
// End our clipboard confirmation no matter what
|
|
||||||
guard let cc = self.clipboardConfirmation else { return }
|
|
||||||
self.clipboardConfirmation = nil
|
|
||||||
|
|
||||||
// Close the sheet
|
|
||||||
if let ccWindow = cc.window {
|
|
||||||
window?.endSheet(ccWindow)
|
|
||||||
}
|
|
||||||
|
|
||||||
switch (request) {
|
|
||||||
case .osc_52_write:
|
|
||||||
guard case .confirm = action else { break }
|
|
||||||
let pb = NSPasteboard.general
|
|
||||||
pb.declareTypes([.string], owner: nil)
|
|
||||||
pb.setString(cc.contents, forType: .string)
|
|
||||||
case .osc_52_read, .paste:
|
|
||||||
let str: String
|
|
||||||
switch (action) {
|
|
||||||
case .cancel:
|
|
||||||
str = ""
|
|
||||||
|
|
||||||
case .confirm:
|
|
||||||
str = cc.contents
|
|
||||||
}
|
|
||||||
|
|
||||||
Ghostty.App.completeClipboardRequest(cc.surface, data: str, state: cc.state, confirmed: true)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
//MARK: - Notifications
|
//MARK: - Notifications
|
||||||
|
|
||||||
@objc private func onGotoTab(notification: SwiftUI.Notification) {
|
@objc private func onGotoTab(notification: SwiftUI.Notification) {
|
||||||
@ -793,35 +544,4 @@ class TerminalController: NSWindowController, NSWindowDelegate,
|
|||||||
Ghostty.moveFocus(to: focusedSurface)
|
Ghostty.moveFocus(to: focusedSurface)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@objc private func onConfirmClipboardRequest(notification: SwiftUI.Notification) {
|
|
||||||
guard let target = notification.object as? Ghostty.SurfaceView else { return }
|
|
||||||
guard target == self.focusedSurface else { return }
|
|
||||||
guard let surface = target.surface else { return }
|
|
||||||
|
|
||||||
// We need a window
|
|
||||||
guard let window = self.window else { return }
|
|
||||||
|
|
||||||
// Check whether we use non-native fullscreen
|
|
||||||
guard let str = notification.userInfo?[Ghostty.Notification.ConfirmClipboardStrKey] as? String else { return }
|
|
||||||
guard let state = notification.userInfo?[Ghostty.Notification.ConfirmClipboardStateKey] as? UnsafeMutableRawPointer? else { return }
|
|
||||||
guard let request = notification.userInfo?[Ghostty.Notification.ConfirmClipboardRequestKey] as? Ghostty.ClipboardRequest else { return }
|
|
||||||
|
|
||||||
// If we already have a clipboard confirmation view up, we ignore this request.
|
|
||||||
// This shouldn't be possible...
|
|
||||||
guard self.clipboardConfirmation == nil else {
|
|
||||||
Ghostty.App.completeClipboardRequest(surface, data: "", state: state, confirmed: true)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Show our paste confirmation
|
|
||||||
self.clipboardConfirmation = ClipboardConfirmationController(
|
|
||||||
surface: surface,
|
|
||||||
contents: str,
|
|
||||||
request: request,
|
|
||||||
state: state,
|
|
||||||
delegate: self
|
|
||||||
)
|
|
||||||
window.beginSheet(self.clipboardConfirmation!.window!)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -22,14 +22,6 @@ protocol TerminalViewDelegate: AnyObject {
|
|||||||
func zoomStateDidChange(to: Bool)
|
func zoomStateDidChange(to: Bool)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Default all the functions so they're optional
|
|
||||||
extension TerminalViewDelegate {
|
|
||||||
func focusedSurfaceDidChange(to: Ghostty.SurfaceView?) {}
|
|
||||||
func titleDidChange(to: String) {}
|
|
||||||
func cellSizeDidChange(to: NSSize) {}
|
|
||||||
func zoomStateDidChange(to: Bool) {}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The view model is a required implementation for TerminalView callers. This contains
|
/// The view model is a required implementation for TerminalView callers. This contains
|
||||||
/// the main state between the TerminalView caller and SwiftUI. This abstraction is what
|
/// the main state between the TerminalView caller and SwiftUI. This abstraction is what
|
||||||
/// allows AppKit to own most of the data in SwiftUI.
|
/// allows AppKit to own most of the data in SwiftUI.
|
||||||
|
Reference in New Issue
Block a user