mirror of
https://github.com/ghostty-org/ghostty.git
synced 2025-07-21 19:26:09 +03:00
Merge pull request #2320 from ghostty-org/slideterm
macOS: Quick Terminal ("Quake-style" terminal)
This commit is contained in:
@ -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,
|
||||||
|
@ -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 */,
|
||||||
|
@ -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 }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -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>
|
||||||
|
31
macos/Sources/Features/QuickTerminal/QuickTerminal.xib
Normal file
31
macos/Sources/Features/QuickTerminal/QuickTerminal.xib
Normal 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>
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
102
macos/Sources/Features/QuickTerminal/QuickTerminalPosition.swift
Normal file
102
macos/Sources/Features/QuickTerminal/QuickTerminalPosition.swift
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -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]
|
||||||
|
}
|
||||||
|
}
|
363
macos/Sources/Features/Terminal/BaseTerminalController.swift
Normal file
363
macos/Sources/Features/Terminal/BaseTerminalController.swift
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
@ -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!)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -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.
|
||||||
|
@ -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,
|
||||||
|
@ -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
|
||||||
|
@ -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, {}),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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,
|
||||||
|
@ -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,
|
||||||
|
@ -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,
|
||||||
|
@ -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,
|
||||||
|
@ -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.
|
||||||
|
Reference in New Issue
Block a user