Merge pull request #2320 from ghostty-org/slideterm

macOS: Quick Terminal ("Quake-style" terminal)
This commit is contained in:
Mitchell Hashimoto
2024-09-28 21:20:00 -07:00
committed by GitHub
20 changed files with 979 additions and 312 deletions

View File

@ -507,6 +507,7 @@ typedef enum {
GHOSTTY_ACTION_TOGGLE_FULLSCREEN, GHOSTTY_ACTION_TOGGLE_FULLSCREEN,
GHOSTTY_ACTION_TOGGLE_TAB_OVERVIEW, GHOSTTY_ACTION_TOGGLE_TAB_OVERVIEW,
GHOSTTY_ACTION_TOGGLE_WINDOW_DECORATIONS, GHOSTTY_ACTION_TOGGLE_WINDOW_DECORATIONS,
GHOSTTY_ACTION_TOGGLE_QUICK_TERMINAL,
GHOSTTY_ACTION_GOTO_TAB, GHOSTTY_ACTION_GOTO_TAB,
GHOSTTY_ACTION_GOTO_SPLIT, GHOSTTY_ACTION_GOTO_SPLIT,
GHOSTTY_ACTION_RESIZE_SPLIT, GHOSTTY_ACTION_RESIZE_SPLIT,

View File

@ -21,6 +21,7 @@
A51BFC272B30F1B800E92F16 /* Sparkle in Frameworks */ = {isa = PBXBuildFile; productRef = A51BFC262B30F1B800E92F16 /* Sparkle */; }; A51BFC272B30F1B800E92F16 /* Sparkle in Frameworks */ = {isa = PBXBuildFile; productRef = A51BFC262B30F1B800E92F16 /* Sparkle */; };
A51BFC2B2B30F6BE00E92F16 /* UpdateDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = A51BFC2A2B30F6BE00E92F16 /* UpdateDelegate.swift */; }; A51BFC2B2B30F6BE00E92F16 /* UpdateDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = A51BFC2A2B30F6BE00E92F16 /* UpdateDelegate.swift */; };
A5278A9B2AA05B2600CD3039 /* Ghostty.Input.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5278A9A2AA05B2600CD3039 /* Ghostty.Input.swift */; }; A5278A9B2AA05B2600CD3039 /* Ghostty.Input.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5278A9A2AA05B2600CD3039 /* Ghostty.Input.swift */; };
A52FFF572CA90484000C6A5B /* QuickTerminalScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = A52FFF562CA90481000C6A5B /* QuickTerminalScreen.swift */; };
A5333E1C2B5A1CE3008AEFF7 /* CrossKit.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5333E1B2B5A1CE3008AEFF7 /* CrossKit.swift */; }; A5333E1C2B5A1CE3008AEFF7 /* CrossKit.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5333E1B2B5A1CE3008AEFF7 /* CrossKit.swift */; };
A5333E1D2B5A1CE3008AEFF7 /* CrossKit.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5333E1B2B5A1CE3008AEFF7 /* CrossKit.swift */; }; A5333E1D2B5A1CE3008AEFF7 /* CrossKit.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5333E1B2B5A1CE3008AEFF7 /* CrossKit.swift */; };
A5333E202B5A2111008AEFF7 /* SurfaceView_UIKit.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5333E152B59DE8E008AEFF7 /* SurfaceView_UIKit.swift */; }; A5333E202B5A2111008AEFF7 /* SurfaceView_UIKit.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5333E152B59DE8E008AEFF7 /* SurfaceView_UIKit.swift */; };
@ -34,6 +35,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,6 +63,10 @@
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 */; };
A5CBD05C2CA0C5C70017A1AE /* QuickTerminal.xib in Resources */ = {isa = PBXBuildFile; fileRef = A5CBD05B2CA0C5C70017A1AE /* QuickTerminal.xib */; };
A5CBD05E2CA0C5EC0017A1AE /* QuickTerminalController.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5CBD05D2CA0C5E70017A1AE /* QuickTerminalController.swift */; };
A5CBD0602CA0C90A0017A1AE /* QuickTerminalWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5CBD05F2CA0C9080017A1AE /* QuickTerminalWindow.swift */; };
A5CBD0642CA122E70017A1AE /* QuickTerminalPosition.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5CBD0632CA122E70017A1AE /* QuickTerminalPosition.swift */; };
A5CBD06B2CA322430017A1AE /* GlobalEventTap.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5CBD06A2CA322320017A1AE /* GlobalEventTap.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 */; };
@ -98,6 +104,7 @@
A51BFC282B30F26D00E92F16 /* GhosttyDebug.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = GhosttyDebug.entitlements; sourceTree = "<group>"; }; A51BFC282B30F26D00E92F16 /* GhosttyDebug.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = GhosttyDebug.entitlements; sourceTree = "<group>"; };
A51BFC2A2B30F6BE00E92F16 /* UpdateDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UpdateDelegate.swift; sourceTree = "<group>"; }; A51BFC2A2B30F6BE00E92F16 /* UpdateDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UpdateDelegate.swift; sourceTree = "<group>"; };
A5278A9A2AA05B2600CD3039 /* Ghostty.Input.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Ghostty.Input.swift; sourceTree = "<group>"; }; A5278A9A2AA05B2600CD3039 /* Ghostty.Input.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Ghostty.Input.swift; sourceTree = "<group>"; };
A52FFF562CA90481000C6A5B /* QuickTerminalScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QuickTerminalScreen.swift; sourceTree = "<group>"; };
A5333E152B59DE8E008AEFF7 /* SurfaceView_UIKit.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SurfaceView_UIKit.swift; sourceTree = "<group>"; }; A5333E152B59DE8E008AEFF7 /* SurfaceView_UIKit.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SurfaceView_UIKit.swift; sourceTree = "<group>"; };
A5333E1B2B5A1CE3008AEFF7 /* CrossKit.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CrossKit.swift; sourceTree = "<group>"; }; A5333E1B2B5A1CE3008AEFF7 /* CrossKit.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CrossKit.swift; sourceTree = "<group>"; };
A5333E212B5A2128008AEFF7 /* SurfaceView_AppKit.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SurfaceView_AppKit.swift; sourceTree = "<group>"; }; A5333E212B5A2128008AEFF7 /* SurfaceView_AppKit.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SurfaceView_AppKit.swift; sourceTree = "<group>"; };
@ -105,6 +112,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>"; };
@ -132,6 +140,10 @@
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>"; };
A5CBD05B2CA0C5C70017A1AE /* QuickTerminal.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = QuickTerminal.xib; sourceTree = "<group>"; };
A5CBD05D2CA0C5E70017A1AE /* QuickTerminalController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QuickTerminalController.swift; sourceTree = "<group>"; };
A5CBD05F2CA0C9080017A1AE /* QuickTerminalWindow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QuickTerminalWindow.swift; sourceTree = "<group>"; };
A5CBD0632CA122E70017A1AE /* QuickTerminalPosition.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QuickTerminalPosition.swift; sourceTree = "<group>"; };
A5CBD06A2CA322320017A1AE /* GlobalEventTap.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GlobalEventTap.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>"; };
@ -204,6 +216,7 @@
A5CBD0672CA2704E0017A1AE /* Global Keybinds */, A5CBD0672CA2704E0017A1AE /* Global Keybinds */,
A56D58872ACDE6BE00508D2C /* Services */, A56D58872ACDE6BE00508D2C /* Services */,
A59630982AEE1C4400D64628 /* Terminal */, A59630982AEE1C4400D64628 /* Terminal */,
A5CBD05A2CA0C5910017A1AE /* QuickTerminal */,
A5E112912AF73E4D00C6E0C2 /* ClipboardConfirmation */, A5E112912AF73E4D00C6E0C2 /* ClipboardConfirmation */,
A57D79252C9C8782001D522E /* Secure Input */, A57D79252C9C8782001D522E /* Secure Input */,
A534263E2A7DCC5800EBB7A2 /* Settings */, A534263E2A7DCC5800EBB7A2 /* Settings */,
@ -333,6 +346,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>";
@ -373,6 +387,18 @@
name = Products; name = Products;
sourceTree = "<group>"; sourceTree = "<group>";
}; };
A5CBD05A2CA0C5910017A1AE /* QuickTerminal */ = {
isa = PBXGroup;
children = (
A5CBD05B2CA0C5C70017A1AE /* QuickTerminal.xib */,
A5CBD05D2CA0C5E70017A1AE /* QuickTerminalController.swift */,
A5CBD0632CA122E70017A1AE /* QuickTerminalPosition.swift */,
A52FFF562CA90481000C6A5B /* QuickTerminalScreen.swift */,
A5CBD05F2CA0C9080017A1AE /* QuickTerminalWindow.swift */,
);
path = QuickTerminal;
sourceTree = "<group>";
};
A5CBD0672CA2704E0017A1AE /* Global Keybinds */ = { A5CBD0672CA2704E0017A1AE /* Global Keybinds */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
@ -506,6 +532,7 @@
A5985CE62C33060F00C57AD3 /* man in Resources */, A5985CE62C33060F00C57AD3 /* man in Resources */,
A5A1F8852A489D6800D1E8BC /* terminfo in Resources */, A5A1F8852A489D6800D1E8BC /* terminfo in Resources */,
552964E62B34A9B400030505 /* vim in Resources */, 552964E62B34A9B400030505 /* vim in Resources */,
A5CBD05C2CA0C5C70017A1AE /* QuickTerminal.xib in Resources */,
); );
runOnlyForDeploymentPostprocessing = 0; runOnlyForDeploymentPostprocessing = 0;
}; };
@ -526,13 +553,17 @@
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 */,
C1F26EA72B738B9900404083 /* NSView+Extension.swift in Sources */, C1F26EA72B738B9900404083 /* NSView+Extension.swift in Sources */,
A5CBD0642CA122E70017A1AE /* QuickTerminalPosition.swift in Sources */,
A596309C2AEE1C9E00D64628 /* TerminalController.swift in Sources */, A596309C2AEE1C9E00D64628 /* TerminalController.swift in Sources */,
A5CC36152C9CDA06004D6760 /* View+Extension.swift in Sources */, A5CC36152C9CDA06004D6760 /* View+Extension.swift in Sources */,
A56D58892ACDE6CA00508D2C /* ServiceProvider.swift in Sources */, A56D58892ACDE6CA00508D2C /* ServiceProvider.swift in Sources */,
A5CBD0602CA0C90A0017A1AE /* QuickTerminalWindow.swift in Sources */,
A5CBD05E2CA0C5EC0017A1AE /* QuickTerminalController.swift in Sources */,
A51BFC222B2FB6B400E92F16 /* AboutView.swift in Sources */, A51BFC222B2FB6B400E92F16 /* AboutView.swift in Sources */,
A5278A9B2AA05B2600CD3039 /* Ghostty.Input.swift in Sources */, A5278A9B2AA05B2600CD3039 /* Ghostty.Input.swift in Sources */,
A5CBD0562C9E65B80017A1AE /* DraggableWindowView.swift in Sources */, A5CBD0562C9E65B80017A1AE /* DraggableWindowView.swift in Sources */,
@ -560,6 +591,7 @@
A5CDF1932AAF9E0800513312 /* ConfigurationErrorsController.swift in Sources */, A5CDF1932AAF9E0800513312 /* ConfigurationErrorsController.swift in Sources */,
A59FB5D12AE0DEA7009128F3 /* MetalView.swift in Sources */, A59FB5D12AE0DEA7009128F3 /* MetalView.swift in Sources */,
A55685E029A03A9F004303CE /* AppError.swift in Sources */, A55685E029A03A9F004303CE /* AppError.swift in Sources */,
A52FFF572CA90484000C6A5B /* QuickTerminalScreen.swift in Sources */,
A5CC36132C9CD72D004D6760 /* SecureInputOverlay.swift in Sources */, A5CC36132C9CD72D004D6760 /* SecureInputOverlay.swift in Sources */,
A535B9DA299C569B0017E2E4 /* ErrorView.swift in Sources */, A535B9DA299C569B0017E2E4 /* ErrorView.swift in Sources */,
A51BFC202B2FB64F00E92F16 /* AboutController.swift in Sources */, A51BFC202B2FB64F00E92F16 /* AboutController.swift in Sources */,

View File

@ -49,6 +49,7 @@ class AppDelegate: NSObject,
@IBOutlet private var menuIncreaseFontSize: NSMenuItem? @IBOutlet private var menuIncreaseFontSize: NSMenuItem?
@IBOutlet private var menuDecreaseFontSize: NSMenuItem? @IBOutlet private var menuDecreaseFontSize: NSMenuItem?
@IBOutlet private var menuResetFontSize: NSMenuItem? @IBOutlet private var menuResetFontSize: NSMenuItem?
@IBOutlet private var menuQuickTerminal: NSMenuItem?
@IBOutlet private var menuTerminalInspector: NSMenuItem? @IBOutlet private var menuTerminalInspector: NSMenuItem?
@IBOutlet private var menuEqualizeSplits: NSMenuItem? @IBOutlet private var menuEqualizeSplits: NSMenuItem?
@ -73,6 +74,9 @@ class AppDelegate: NSObject,
/// Manages our terminal windows. /// Manages our terminal windows.
let terminalManager: TerminalManager let terminalManager: TerminalManager
/// Our quick terminal. This starts out uninitialized and only initializes if used.
private var quickController: QuickTerminalController? = nil
/// Manages updates /// Manages updates
let updaterController: SPUStandardUpdaterController let updaterController: SPUStandardUpdaterController
let updaterDelegate: UpdaterDelegate = UpdaterDelegate() let updaterDelegate: UpdaterDelegate = UpdaterDelegate()
@ -310,6 +314,7 @@ class AppDelegate: NSObject,
syncMenuShortcut(action: "increase_font_size:1", menuItem: self.menuIncreaseFontSize) syncMenuShortcut(action: "increase_font_size:1", menuItem: self.menuIncreaseFontSize)
syncMenuShortcut(action: "decrease_font_size:1", menuItem: self.menuDecreaseFontSize) syncMenuShortcut(action: "decrease_font_size:1", menuItem: self.menuDecreaseFontSize)
syncMenuShortcut(action: "reset_font_size", menuItem: self.menuResetFontSize) syncMenuShortcut(action: "reset_font_size", menuItem: self.menuResetFontSize)
syncMenuShortcut(action: "toggle_quick_terminal", menuItem: self.menuQuickTerminal)
syncMenuShortcut(action: "inspector:toggle", menuItem: self.menuTerminalInspector) syncMenuShortcut(action: "inspector:toggle", menuItem: self.menuTerminalInspector)
syncMenuShortcut(action: "toggle_secure_input", menuItem: self.menuSecureInput) syncMenuShortcut(action: "toggle_secure_input", menuItem: self.menuSecureInput)
@ -545,4 +550,18 @@ class AppDelegate: NSObject,
@IBAction func toggleSecureInput(_ sender: Any) { @IBAction func toggleSecureInput(_ sender: Any) {
setSecureInput(.toggle) setSecureInput(.toggle)
} }
@IBAction func toggleQuickTerminal(_ sender: Any) {
if quickController == nil {
quickController = QuickTerminalController(
ghostty,
position: ghostty.config.quickTerminalPosition
)
}
guard let quickController = self.quickController else { return }
quickController.toggle()
self.menuQuickTerminal?.state = if (quickController.visible) { .on } else { .off }
}
} }

View File

@ -32,6 +32,7 @@
<outlet property="menuOpenConfig" destination="BOF-NM-1cW" id="Nze-Go-glw"/> <outlet property="menuOpenConfig" destination="BOF-NM-1cW" id="Nze-Go-glw"/>
<outlet property="menuPaste" destination="i27-pK-umN" id="ICc-X2-gV3"/> <outlet property="menuPaste" destination="i27-pK-umN" id="ICc-X2-gV3"/>
<outlet property="menuPreviousSplit" destination="Lic-px-1wg" id="Rto-CG-yRe"/> <outlet property="menuPreviousSplit" destination="Lic-px-1wg" id="Rto-CG-yRe"/>
<outlet property="menuQuickTerminal" destination="kvF-d2-JsP" id="a0u-tf-IEc"/>
<outlet property="menuQuit" destination="4sb-4s-VLi" id="qYN-S1-6UW"/> <outlet property="menuQuit" destination="4sb-4s-VLi" id="qYN-S1-6UW"/>
<outlet property="menuReloadConfig" destination="KKH-XX-5py" id="Wvp-7J-wqX"/> <outlet property="menuReloadConfig" destination="KKH-XX-5py" id="Wvp-7J-wqX"/>
<outlet property="menuResetFontSize" destination="Jah-MY-aLX" id="ger-qM-wrm"/> <outlet property="menuResetFontSize" destination="Jah-MY-aLX" id="ger-qM-wrm"/>
@ -216,6 +217,13 @@
</connections> </connections>
</menuItem> </menuItem>
<menuItem isSeparatorItem="YES" id="L3L-I8-sqk"/> <menuItem isSeparatorItem="YES" id="L3L-I8-sqk"/>
<menuItem title="Quick Terminal" id="kvF-d2-JsP">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="toggleQuickTerminal:" target="bbz-4X-AYv" id="gm3-mk-l8N"/>
</connections>
</menuItem>
<menuItem isSeparatorItem="YES" id="bC9-n9-RbJ"/>
<menuItem title="Terminal Inspector" id="QwP-M5-fvh"> <menuItem title="Terminal Inspector" id="QwP-M5-fvh">
<modifierMask key="keyEquivalentModifierMask"/> <modifierMask key="keyEquivalentModifierMask"/>
<connections> <connections>

View File

@ -0,0 +1,31 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.Cocoa.XIB" version="3.0" toolsVersion="23094" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES" customObjectInstantitationMethod="direct">
<dependencies>
<deployment identifier="macosx"/>
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="23094"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
<objects>
<customObject id="-2" userLabel="File's Owner" customClass="QuickTerminalController" customModule="Ghostty" customModuleProvider="target">
<connections>
<outlet property="window" destination="QvC-M9-y7g" id="JMU-zX-9Ie"/>
</connections>
</customObject>
<customObject id="-1" userLabel="First Responder" customClass="FirstResponder"/>
<customObject id="-3" userLabel="Application" customClass="NSObject"/>
<window title="👻 Ghostty" allowsToolTipsWhenApplicationIsInactive="NO" autorecalculatesKeyViewLoop="NO" restorable="NO" releasedWhenClosed="NO" visibleAtLaunch="NO" animationBehavior="default" id="QvC-M9-y7g" customClass="QuickTerminalWindow" customModule="Ghostty" customModuleProvider="target">
<windowStyleMask key="styleMask" titled="YES" closable="YES" miniaturizable="YES" resizable="YES"/>
<windowPositionMask key="initialPositionMask" leftStrut="YES" rightStrut="YES" topStrut="YES" bottomStrut="YES"/>
<rect key="contentRect" x="196" y="240" width="480" height="270"/>
<rect key="screenRect" x="0.0" y="0.0" width="3008" height="1667"/>
<view key="contentView" wantsLayer="YES" id="EiT-Mj-1SZ">
<rect key="frame" x="0.0" y="0.0" width="480" height="270"/>
<autoresizingMask key="autoresizingMask"/>
</view>
<connections>
<outlet property="delegate" destination="-2" id="u5f-FR-jJw"/>
</connections>
<point key="canvasLocation" x="132" y="-82"/>
</window>
</objects>
</document>

View File

@ -0,0 +1,211 @@
import Foundation
import Cocoa
import SwiftUI
import GhosttyKit
/// Controller for the "quick" terminal.
class QuickTerminalController: BaseTerminalController {
override var windowNibName: NSNib.Name? { "QuickTerminal" }
/// The position for the quick terminal.
let position: QuickTerminalPosition
/// The current state of the quick terminal
private(set) var visible: Bool = false
init(_ ghostty: Ghostty.App,
position: QuickTerminalPosition = .top,
baseConfig base: Ghostty.SurfaceConfiguration? = nil,
surfaceTree tree: Ghostty.SplitNode? = nil
) {
self.position = position
super.init(ghostty, baseConfig: base, surfaceTree: tree)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) is not supported for this view")
}
// MARK: NSWindowController
override func windowDidLoad() {
guard let window = self.window else { return }
// The controller is the window delegate so we can detect events such as
// window close so we can animate out.
window.delegate = self
// The quick window is not restorable (yet!). "Yet" because in theory we can
// make this restorable, but it isn't currently implemented.
window.isRestorable = false
// Setup our initial size based on our configured position
position.setLoaded(window)
// Setup our content
window.contentView = NSHostingView(rootView: TerminalView(
ghostty: self.ghostty,
viewModel: self,
delegate: self
))
// Animate the window in
animateIn()
}
// MARK: NSWindowDelegate
override func windowDidResignKey(_ notification: Notification) {
super.windowDidResignKey(notification)
// We don't animate out if there is a modal sheet being shown currently.
// This lets us show alerts without causing the window to disappear.
guard window?.attachedSheet == nil else { return }
animateOut()
}
func windowWillResize(_ sender: NSWindow, to frameSize: NSSize) -> NSSize {
// We use the actual screen the window is on for this, since it should
// be on the proper screen.
guard let screen = window?.screen ?? NSScreen.main else { return frameSize }
return position.restrictFrameSize(frameSize, on: screen)
}
// MARK: Base Controller Overrides
override func surfaceTreeDidChange(from: Ghostty.SplitNode?, to: Ghostty.SplitNode?) {
super.surfaceTreeDidChange(from: from, to: to)
// If our surface tree is nil then we animate the window out.
if (to == nil) {
animateOut()
}
}
// MARK: Methods
func toggle() {
if (visible) {
animateOut()
} else {
animateIn()
}
}
func animateIn() {
guard let window = self.window else { return }
// Set our visibility state
guard !visible else { return }
visible = true
// Animate the window in
animateWindowIn(window: window, from: position)
// If our surface tree is nil then we initialize a new terminal. The surface
// tree can be nil if for example we run "eixt" in the terminal and force
// animate out.
if (surfaceTree == nil) {
let leaf: Ghostty.SplitNode.Leaf = .init(ghostty.app!, baseConfig: nil)
surfaceTree = .leaf(leaf)
focusedSurface = leaf.surface
// We need to grab first responder but it takes a few loop cycles
// before the view is attached to the window so we do it async.
DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(250)) {
// We should probably retry here but I was never able to trigger this.
// If this happens though its a crash so let's avoid it.
guard let leafWindow = leaf.surface.window,
leafWindow == window else { return }
window.makeFirstResponder(leaf.surface)
}
}
}
func animateOut() {
guard let window = self.window else { return }
// Set our visibility state
guard visible else { return }
visible = false
animateWindowOut(window: window, to: position)
}
private func animateWindowIn(window: NSWindow, from position: QuickTerminalPosition) {
guard let screen = ghostty.config.quickTerminalScreen.screen else { return }
// Move our window off screen to the top
position.setInitial(in: window, on: screen)
// Move it to the visible position since animation requires this
window.makeKeyAndOrderFront(nil)
// Run the animation that moves our window into the proper place and makes
// it visible.
NSAnimationContext.runAnimationGroup { context in
context.duration = 0.2
context.timingFunction = .init(name: .easeIn)
position.setFinal(in: window.animator(), on: screen)
}
}
private func animateWindowOut(window: NSWindow, to position: QuickTerminalPosition) {
// We always animate out to whatever screen the window is actually on.
guard let screen = window.screen ?? NSScreen.main else { return }
// Keep track of if we were the key window. If we were the key window then we
// want to move focus to the next window so that focus is preserved somewhere
// in the app.
let wasKey = window.isKeyWindow
NSAnimationContext.runAnimationGroup({ context in
context.duration = 0.2
context.timingFunction = .init(name: .easeIn)
position.setInitial(in: window.animator(), on: screen)
}, completionHandler: {
guard wasKey else { return }
self.focusNextWindow()
})
}
private func focusNextWindow() {
// We only want to consider windows that are visible
let windows = NSApp.windows.filter { $0.isVisible }
// If we have no windows there is nothing to focus.
guard !windows.isEmpty else { return }
// Find the current key window (the window that is currently focused)
if let keyWindow = NSApp.keyWindow,
let currentIndex = windows.firstIndex(of: keyWindow) {
// Calculate the index of the next window (cycle through the list)
let nextIndex = (currentIndex + 1) % windows.count
let nextWindow = windows[nextIndex]
// Make the next window key and bring it to the front
nextWindow.makeKeyAndOrderFront(nil)
} else {
// If there's no key window, focus the first available window
windows.first?.makeKeyAndOrderFront(nil)
}
}
// MARK: First Responder
@IBAction override func closeWindow(_ sender: Any) {
// Instead of closing the window, we animate it out.
animateOut()
}
@IBAction func newTab(_ sender: Any?) {
guard let window else { return }
let alert = NSAlert()
alert.messageText = "Cannot Create New Tab"
alert.informativeText = "Tabs aren't supported in the Quick Terminal."
alert.addButton(withTitle: "OK")
alert.alertStyle = .warning
alert.beginSheetModal(for: window)
}
}

View File

@ -0,0 +1,102 @@
import Cocoa
enum QuickTerminalPosition : String {
case top
case bottom
case left
case right
/// Set the loaded state for a window.
func setLoaded(_ window: NSWindow) {
guard let screen = window.screen ?? NSScreen.main else { return }
switch (self) {
case .top, .bottom:
window.setFrame(.init(
origin: window.frame.origin,
size: .init(
width: screen.frame.width,
height: screen.frame.height / 4)
), display: false)
case .left, .right:
window.setFrame(.init(
origin: window.frame.origin,
size: .init(
width: screen.frame.width / 4,
height: screen.frame.height)
), display: false)
}
}
/// Set the initial state for a window for animating out of this position.
func setInitial(in window: NSWindow, on screen: NSScreen) {
// We always start invisible
window.alphaValue = 0
// Position depends
window.setFrame(.init(
origin: initialOrigin(for: window, on: screen),
size: restrictFrameSize(window.frame.size, on: screen)
), display: false)
}
/// Set the final state for a window in this position.
func setFinal(in window: NSWindow, on screen: NSScreen) {
// We always end visible
window.alphaValue = 1
// Position depends
window.setFrame(.init(
origin: finalOrigin(for: window, on: screen),
size: restrictFrameSize(window.frame.size, on: screen)
), display: true)
}
/// Restrict the frame size during resizing.
func restrictFrameSize(_ size: NSSize, on screen: NSScreen) -> NSSize {
var finalSize = size
switch (self) {
case .top, .bottom:
finalSize.width = screen.frame.width
case .left, .right:
finalSize.height = screen.frame.height
}
return finalSize
}
/// The initial point origin for this position.
func initialOrigin(for window: NSWindow, on screen: NSScreen) -> CGPoint {
switch (self) {
case .top:
return .init(x: screen.frame.minX, y: screen.frame.maxY)
case .bottom:
return .init(x: screen.frame.minX, y: -window.frame.height)
case .left:
return .init(x: -window.frame.width, y: 0)
case .right:
return .init(x: screen.frame.maxX, y: 0)
}
}
/// The final point origin for this position.
func finalOrigin(for window: NSWindow, on screen: NSScreen) -> CGPoint {
switch (self) {
case .top:
return .init(x: screen.frame.minX, y: screen.visibleFrame.maxY - window.frame.height)
case .bottom:
return .init(x: screen.frame.minX, y: screen.frame.minY)
case .left:
return .init(x: screen.frame.minX, y: window.frame.origin.y)
case .right:
return .init(x: screen.visibleFrame.maxX - window.frame.width, y: window.frame.origin.y)
}
}
}

View File

@ -0,0 +1,37 @@
import Cocoa
enum QuickTerminalScreen {
case main
case mouse
case menuBar
init?(fromGhosttyConfig string: String) {
switch (string) {
case "main":
self = .main
case "mouse":
self = .mouse
case "macos-menu-bar":
self = .menuBar
default:
return nil
}
}
var screen: NSScreen? {
switch (self) {
case .main:
return NSScreen.main
case .mouse:
let mouseLoc = NSEvent.mouseLocation
return NSScreen.screens.first(where: { $0.frame.contains(mouseLoc) })
case .menuBar:
return NSScreen.screens.first
}
}
}

View File

@ -0,0 +1,39 @@
import Cocoa
class QuickTerminalWindow: NSWindow {
// Both of these must be true for windows without decorations to be able to
// still become key/main and receive events.
override var canBecomeKey: Bool { return true }
override var canBecomeMain: Bool { return true }
override func awakeFromNib() {
super.awakeFromNib()
// Note: almost all of this stuff can be done in the nib/xib directly
// but I prefer to do it programmatically because the properties we
// care about are less hidden.
// Remove the title completely. This will make the window square. One
// downside is it also hides the cursor indications of resize but the
// window remains resizable.
self.styleMask.remove(.titled)
// We need to set our window level to a high value. In testing, only
// popUpMenu and above do what we want. This gets it above the menu bar
// and lets us render off screen.
self.level = .popUpMenu
// This plus the level above was what was needed for the animation to work,
// because it gets the window off screen properly. Plus we add some fields
// we just want the behavior of.
self.collectionBehavior = [
// We want this to be part of every space because it is a singleton.
.canJoinAllSpaces,
// We don't want to be part of command-tilde
.ignoresCycle,
// We never support fullscreen
.fullScreenNone]
}
}

View File

@ -0,0 +1,363 @@
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) {
guard let window else { return }
// 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.
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)
}
}

View File

@ -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!)
}
} }

View File

@ -18,17 +18,10 @@ protocol TerminalViewDelegate: AnyObject {
/// not called initially. /// not called initially.
func surfaceTreeDidChange() func surfaceTreeDidChange()
/// This is called when a split is zoomed.
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.

View File

@ -482,6 +482,9 @@ extension Ghostty {
case GHOSTTY_ACTION_RENDERER_HEALTH: case GHOSTTY_ACTION_RENDERER_HEALTH:
rendererHealth(app, target: target, v: action.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_CLOSE_ALL_WINDOWS: case GHOSTTY_ACTION_CLOSE_ALL_WINDOWS:
fallthrough fallthrough
case GHOSTTY_ACTION_TOGGLE_TAB_OVERVIEW: case GHOSTTY_ACTION_TOGGLE_TAB_OVERVIEW:
@ -830,6 +833,14 @@ extension Ghostty {
} }
} }
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( private static func setTitle(
_ app: ghostty_app_t, _ app: ghostty_app_t,
target: ghostty_target_s, target: ghostty_target_s,

View File

@ -332,6 +332,28 @@ extension Ghostty {
return Color(newColor) return Color(newColor)
} }
#if canImport(AppKit)
var quickTerminalPosition: QuickTerminalPosition {
guard let config = self.config else { return .top }
var v: UnsafePointer<Int8>? = nil
let key = "quick-terminal-position"
guard ghostty_config_get(config, &v, key, UInt(key.count)) else { return .top }
guard let ptr = v else { return .top }
let str = String(cString: ptr)
return QuickTerminalPosition(rawValue: str) ?? .top
}
var quickTerminalScreen: QuickTerminalScreen {
guard let config = self.config else { return .main }
var v: UnsafePointer<Int8>? = nil
let key = "quick-terminal-screen"
guard ghostty_config_get(config, &v, key, UInt(key.count)) else { return .main }
guard let ptr = v else { return .main }
let str = String(cString: ptr)
return QuickTerminalScreen(fromGhosttyConfig: str) ?? .main
}
#endif
var resizeOverlay: ResizeOverlay { var resizeOverlay: ResizeOverlay {
guard let config = self.config else { return .after_first } guard let config = self.config else { return .after_first }
var v: UnsafePointer<Int8>? = nil var v: UnsafePointer<Int8>? = nil

View File

@ -324,6 +324,7 @@ pub fn performAction(
.open_config => try rt_app.performAction(.app, .open_config, {}), .open_config => try rt_app.performAction(.app, .open_config, {}),
.reload_config => try self.reloadConfig(rt_app), .reload_config => try self.reloadConfig(rt_app),
.close_all_windows => try rt_app.performAction(.app, .close_all_windows, {}), .close_all_windows => try rt_app.performAction(.app, .close_all_windows, {}),
.toggle_quick_terminal => try rt_app.performAction(.app, .toggle_quick_terminal, {}),
} }
} }

View File

@ -93,6 +93,9 @@ pub const Action = union(Key) {
/// Toggle whether window directions are shown. /// Toggle whether window directions are shown.
toggle_window_decorations, toggle_window_decorations,
/// Toggle the quick terminal in or out.
toggle_quick_terminal,
/// Jump to a specific tab. Must handle the scenario that the tab /// Jump to a specific tab. Must handle the scenario that the tab
/// value is invalid. /// value is invalid.
goto_tab: GotoTab, goto_tab: GotoTab,
@ -176,6 +179,7 @@ pub const Action = union(Key) {
toggle_fullscreen, toggle_fullscreen,
toggle_tab_overview, toggle_tab_overview,
toggle_window_decorations, toggle_window_decorations,
toggle_quick_terminal,
goto_tab, goto_tab,
goto_split, goto_split,
resize_split, resize_split,

View File

@ -196,6 +196,7 @@ pub const App = struct {
.close_all_windows, .close_all_windows,
.toggle_tab_overview, .toggle_tab_overview,
.toggle_window_decorations, .toggle_window_decorations,
.toggle_quick_terminal,
.goto_tab, .goto_tab,
.inspector, .inspector,
.render_inspector, .render_inspector,

View File

@ -379,6 +379,7 @@ pub fn performAction(
// Unimplemented // Unimplemented
.close_all_windows, .close_all_windows,
.toggle_split_zoom, .toggle_split_zoom,
.toggle_quick_terminal,
.size_limit, .size_limit,
.cell_size, .cell_size,
.secure_input, .secure_input,

View File

@ -1220,6 +1220,40 @@ keybind: Keybinds = .{},
/// window is ever created. Only implemented on Linux. /// window is ever created. Only implemented on Linux.
@"initial-window": bool = true, @"initial-window": bool = true,
/// The position of the "quick" terminal window. To learn more about the
/// quick terminal, see the documentation for the `toggle_quick_terminal`
/// binding action.
///
/// Valid values are:
///
/// * `top` - Terminal appears at the top of the screen.
/// * `bottom` - Terminal appears at the bottom of the screen.
/// * `left` - Terminal appears at the left of the screen.
/// * `right` - Terminal appears at the right of the screen.
///
/// Changing this configuration requires restarting Ghostty completely.
@"quick-terminal-position": QuickTerminalPosition = .top,
/// The screen where the quick terminal should show up.
///
/// Valid values are:
///
/// * `main` - The screen that the operating system recommends as the main
/// screen. On macOS, this is the screen that is currently receiving
/// keyboard input. This screen is defined by the operating system and
/// not chosen by Ghostty.
///
/// * `mouse` - The screen that the mouse is currently hovered over.
///
/// * `macos-menu-bar` - The screen that contains the macOS menu bar as
/// set in the display settings on macOS. This is a bit confusing because
/// every screen on macOS has a menu bar, but this is the screen that
/// contains the primary menu bar.
///
/// The default value is `main` because this is the recommended screen
/// by the operating system.
@"quick-terminal-screen": QuickTerminalScreen = .main,
/// Whether to enable shell integration auto-injection or not. Shell integration /// Whether to enable shell integration auto-injection or not. Shell integration
/// greatly enhances the terminal experience by enabling a number of features: /// greatly enhances the terminal experience by enabling a number of features:
/// ///
@ -4401,6 +4435,21 @@ pub const ResizeOverlayPosition = enum {
@"bottom-right", @"bottom-right",
}; };
/// See quick-terminal-position
pub const QuickTerminalPosition = enum {
top,
bottom,
left,
right,
};
/// See quick-terminal-screen
pub const QuickTerminalScreen = enum {
main,
mouse,
@"macos-menu-bar",
};
/// See grapheme-width-method /// See grapheme-width-method
pub const GraphemeWidthMethod = enum { pub const GraphemeWidthMethod = enum {
legacy, legacy,

View File

@ -363,6 +363,27 @@ pub const Action = union(enum) {
/// This only works on macOS, since this is a system API on macOS. /// This only works on macOS, since this is a system API on macOS.
toggle_secure_input: void, toggle_secure_input: void,
/// Toggle the "quick" terminal. The quick terminal is a terminal that
/// appears on demand from a keybinding, often sliding in from a screen
/// edge such as the top. This is useful for quick access to a terminal
/// without having to open a new window or tab.
///
/// When the quick terminal loses focus, it disappears. The terminal state
/// is preserved between appearances, so you can always press the keybinding
/// to bring it back up.
///
/// The quick terminal has some limitations:
///
/// - It is a singleton; only one instance can exist at a time.
/// - It does not support tabs.
/// - It does not support fullscreen.
/// - It will not be restored when the application is restarted
/// (for systems that support window restoration).
///
/// See the various configurations for the quick terminal in the
/// configuration file to customize its behavior.
toggle_quick_terminal: void,
/// Quit ghostty. /// Quit ghostty.
quit: void, quit: void,
@ -563,6 +584,7 @@ pub const Action = union(enum) {
.reload_config, .reload_config,
.close_all_windows, .close_all_windows,
.quit, .quit,
.toggle_quick_terminal,
=> .app, => .app,
// These are app but can be special-cased in a surface context. // These are app but can be special-cased in a surface context.