mirror of
https://github.com/ghostty-org/ghostty.git
synced 2025-07-25 13:16:11 +03:00
Merge pull request #215 from mitchellh/mrn/non-native-fs
macos: add support for non-native fullscreen
This commit is contained in:
@ -239,7 +239,7 @@ typedef void (*ghostty_runtime_new_split_cb)(void *, ghostty_split_direction_e);
|
|||||||
typedef void (*ghostty_runtime_close_surface_cb)(void *, bool);
|
typedef void (*ghostty_runtime_close_surface_cb)(void *, bool);
|
||||||
typedef void (*ghostty_runtime_focus_split_cb)(void *, ghostty_split_focus_direction_e);
|
typedef void (*ghostty_runtime_focus_split_cb)(void *, ghostty_split_focus_direction_e);
|
||||||
typedef void (*ghostty_runtime_goto_tab_cb)(void *, int32_t);
|
typedef void (*ghostty_runtime_goto_tab_cb)(void *, int32_t);
|
||||||
typedef void (*ghostty_runtime_toggle_fullscreen_cb)(void *);
|
typedef void (*ghostty_runtime_toggle_fullscreen_cb)(void *, bool);
|
||||||
|
|
||||||
typedef struct {
|
typedef struct {
|
||||||
void *userdata;
|
void *userdata;
|
||||||
|
@ -7,6 +7,12 @@
|
|||||||
objects = {
|
objects = {
|
||||||
|
|
||||||
/* Begin PBXBuildFile section */
|
/* Begin PBXBuildFile section */
|
||||||
|
8503D7C72A549C66006CFF3D /* FullScreenHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8503D7C62A549C66006CFF3D /* FullScreenHandler.swift */; };
|
||||||
|
85102A1C2A6E32890084AB3E /* PrimaryWindowController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85102A1B2A6E32890084AB3E /* PrimaryWindowController.swift */; };
|
||||||
|
857F63812A5E64F200CA4815 /* MainMenu.xib in Resources */ = {isa = PBXBuildFile; fileRef = 857F63802A5E64F200CA4815 /* MainMenu.xib */; };
|
||||||
|
85DE1C922A6A3DCA00493853 /* PrimaryWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85DE1C912A6A3DCA00493853 /* PrimaryWindow.swift */; };
|
||||||
|
A53426352A7DA53D00EBB7A2 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = A53426342A7DA53D00EBB7A2 /* AppDelegate.swift */; };
|
||||||
|
A53426392A7DC55C00EBB7A2 /* PrimaryWindowManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = A53426382A7DC55C00EBB7A2 /* PrimaryWindowManager.swift */; };
|
||||||
A535B9DA299C569B0017E2E4 /* ErrorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A535B9D9299C569B0017E2E4 /* ErrorView.swift */; };
|
A535B9DA299C569B0017E2E4 /* ErrorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A535B9D9299C569B0017E2E4 /* ErrorView.swift */; };
|
||||||
A545D1A22A5772CE006E0AE4 /* shell-integration in Resources */ = {isa = PBXBuildFile; fileRef = A545D1A12A5772CE006E0AE4 /* shell-integration */; };
|
A545D1A22A5772CE006E0AE4 /* shell-integration in Resources */ = {isa = PBXBuildFile; fileRef = A545D1A12A5772CE006E0AE4 /* shell-integration */; };
|
||||||
A55685E029A03A9F004303CE /* AppError.swift in Sources */ = {isa = PBXBuildFile; fileRef = A55685DF29A03A9F004303CE /* AppError.swift */; };
|
A55685E029A03A9F004303CE /* AppError.swift in Sources */ = {isa = PBXBuildFile; fileRef = A55685DF29A03A9F004303CE /* AppError.swift */; };
|
||||||
@ -17,16 +23,21 @@
|
|||||||
A571AB1D2A206FCF00248498 /* GhosttyKit.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = A5D495A1299BEC7E00DD1313 /* GhosttyKit.xcframework */; };
|
A571AB1D2A206FCF00248498 /* GhosttyKit.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = A5D495A1299BEC7E00DD1313 /* GhosttyKit.xcframework */; };
|
||||||
A59444F729A2ED5200725BBA /* SettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A59444F629A2ED5200725BBA /* SettingsView.swift */; };
|
A59444F729A2ED5200725BBA /* SettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A59444F629A2ED5200725BBA /* SettingsView.swift */; };
|
||||||
A5A1F8852A489D6800D1E8BC /* terminfo in Resources */ = {isa = PBXBuildFile; fileRef = A5A1F8842A489D6800D1E8BC /* terminfo */; };
|
A5A1F8852A489D6800D1E8BC /* terminfo in Resources */ = {isa = PBXBuildFile; fileRef = A5A1F8842A489D6800D1E8BC /* terminfo */; };
|
||||||
A5B30535299BEAAA0047F10C /* GhosttyApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5B30534299BEAAA0047F10C /* GhosttyApp.swift */; };
|
|
||||||
A5B30539299BEAAB0047F10C /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = A5B30538299BEAAB0047F10C /* Assets.xcassets */; };
|
A5B30539299BEAAB0047F10C /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = A5B30538299BEAAB0047F10C /* Assets.xcassets */; };
|
||||||
A5CEAFDC29B8009000646FDA /* SplitView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5CEAFDB29B8009000646FDA /* SplitView.swift */; };
|
A5CEAFDC29B8009000646FDA /* SplitView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5CEAFDB29B8009000646FDA /* SplitView.swift */; };
|
||||||
A5CEAFDE29B8058B00646FDA /* SplitView.Divider.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5CEAFDD29B8058B00646FDA /* SplitView.Divider.swift */; };
|
A5CEAFDE29B8058B00646FDA /* SplitView.Divider.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5CEAFDD29B8058B00646FDA /* SplitView.Divider.swift */; };
|
||||||
A5CEAFFF29C2410700646FDA /* Backport.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5CEAFFE29C2410700646FDA /* Backport.swift */; };
|
A5CEAFFF29C2410700646FDA /* Backport.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5CEAFFE29C2410700646FDA /* Backport.swift */; };
|
||||||
A5FECBD729D1FC3900022361 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5FECBD629D1FC3900022361 /* ContentView.swift */; };
|
A5FECBD729D1FC3900022361 /* PrimaryView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5FECBD629D1FC3900022361 /* PrimaryView.swift */; };
|
||||||
A5FECBD929D2010400022361 /* WindowAccessor.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5FECBD829D2010400022361 /* WindowAccessor.swift */; };
|
A5FECBD929D2010400022361 /* WindowAccessor.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5FECBD829D2010400022361 /* WindowAccessor.swift */; };
|
||||||
/* End PBXBuildFile section */
|
/* End PBXBuildFile section */
|
||||||
|
|
||||||
/* Begin PBXFileReference section */
|
/* Begin PBXFileReference section */
|
||||||
|
8503D7C62A549C66006CFF3D /* FullScreenHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FullScreenHandler.swift; sourceTree = "<group>"; };
|
||||||
|
85102A1B2A6E32890084AB3E /* PrimaryWindowController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PrimaryWindowController.swift; sourceTree = "<group>"; };
|
||||||
|
857F63802A5E64F200CA4815 /* MainMenu.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = MainMenu.xib; sourceTree = "<group>"; };
|
||||||
|
85DE1C912A6A3DCA00493853 /* PrimaryWindow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PrimaryWindow.swift; sourceTree = "<group>"; };
|
||||||
|
A53426342A7DA53D00EBB7A2 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
|
||||||
|
A53426382A7DC55C00EBB7A2 /* PrimaryWindowManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PrimaryWindowManager.swift; sourceTree = "<group>"; };
|
||||||
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>"; };
|
||||||
A545D1A12A5772CE006E0AE4 /* shell-integration */ = {isa = PBXFileReference; lastKnownFileType = folder; name = "shell-integration"; path = "../zig-out/share/shell-integration"; sourceTree = "<group>"; };
|
A545D1A12A5772CE006E0AE4 /* shell-integration */ = {isa = PBXFileReference; lastKnownFileType = folder; name = "shell-integration"; path = "../zig-out/share/shell-integration"; 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>"; };
|
||||||
@ -38,14 +49,13 @@
|
|||||||
A59444F629A2ED5200725BBA /* SettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsView.swift; sourceTree = "<group>"; };
|
A59444F629A2ED5200725BBA /* SettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsView.swift; sourceTree = "<group>"; };
|
||||||
A5A1F8842A489D6800D1E8BC /* terminfo */ = {isa = PBXFileReference; lastKnownFileType = folder; name = terminfo; path = "../zig-out/share/terminfo"; sourceTree = "<group>"; };
|
A5A1F8842A489D6800D1E8BC /* terminfo */ = {isa = PBXFileReference; lastKnownFileType = folder; name = terminfo; path = "../zig-out/share/terminfo"; sourceTree = "<group>"; };
|
||||||
A5B30531299BEAAA0047F10C /* Ghostty.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Ghostty.app; sourceTree = BUILT_PRODUCTS_DIR; };
|
A5B30531299BEAAA0047F10C /* Ghostty.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Ghostty.app; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||||
A5B30534299BEAAA0047F10C /* GhosttyApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GhosttyApp.swift; sourceTree = "<group>"; };
|
|
||||||
A5B30538299BEAAB0047F10C /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
|
A5B30538299BEAAB0047F10C /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
|
||||||
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>"; };
|
||||||
A5CEAFDB29B8009000646FDA /* SplitView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SplitView.swift; sourceTree = "<group>"; };
|
A5CEAFDB29B8009000646FDA /* SplitView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SplitView.swift; sourceTree = "<group>"; };
|
||||||
A5CEAFDD29B8058B00646FDA /* SplitView.Divider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SplitView.Divider.swift; sourceTree = "<group>"; };
|
A5CEAFDD29B8058B00646FDA /* SplitView.Divider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SplitView.Divider.swift; sourceTree = "<group>"; };
|
||||||
A5CEAFFE29C2410700646FDA /* Backport.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Backport.swift; sourceTree = "<group>"; };
|
A5CEAFFE29C2410700646FDA /* Backport.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Backport.swift; sourceTree = "<group>"; };
|
||||||
A5D495A1299BEC7E00DD1313 /* GhosttyKit.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; path = GhosttyKit.xcframework; sourceTree = "<group>"; };
|
A5D495A1299BEC7E00DD1313 /* GhosttyKit.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; path = GhosttyKit.xcframework; sourceTree = "<group>"; };
|
||||||
A5FECBD629D1FC3900022361 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = "<group>"; };
|
A5FECBD629D1FC3900022361 /* PrimaryView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PrimaryView.swift; sourceTree = "<group>"; };
|
||||||
A5FECBD829D2010400022361 /* WindowAccessor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WindowAccessor.swift; sourceTree = "<group>"; };
|
A5FECBD829D2010400022361 /* WindowAccessor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WindowAccessor.swift; sourceTree = "<group>"; };
|
||||||
/* End PBXFileReference section */
|
/* End PBXFileReference section */
|
||||||
|
|
||||||
@ -61,19 +71,54 @@
|
|||||||
/* End PBXFrameworksBuildPhase section */
|
/* End PBXFrameworksBuildPhase section */
|
||||||
|
|
||||||
/* Begin PBXGroup section */
|
/* Begin PBXGroup section */
|
||||||
|
A53426362A7DC53000EBB7A2 /* Features */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
A53426372A7DC53A00EBB7A2 /* Primary Window */,
|
||||||
|
A534263E2A7DCC5800EBB7A2 /* Settings */,
|
||||||
|
);
|
||||||
|
path = Features;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
|
A53426372A7DC53A00EBB7A2 /* Primary Window */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
A53426382A7DC55C00EBB7A2 /* PrimaryWindowManager.swift */,
|
||||||
|
85102A1B2A6E32890084AB3E /* PrimaryWindowController.swift */,
|
||||||
|
85DE1C912A6A3DCA00493853 /* PrimaryWindow.swift */,
|
||||||
|
A5FECBD629D1FC3900022361 /* PrimaryView.swift */,
|
||||||
|
A535B9D9299C569B0017E2E4 /* ErrorView.swift */,
|
||||||
|
);
|
||||||
|
path = "Primary Window";
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
|
A534263D2A7DCBB000EBB7A2 /* Helpers */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
A5CEAFFE29C2410700646FDA /* Backport.swift */,
|
||||||
|
8503D7C62A549C66006CFF3D /* FullScreenHandler.swift */,
|
||||||
|
A5FECBD829D2010400022361 /* WindowAccessor.swift */,
|
||||||
|
A5CEAFDA29B8005900646FDA /* SplitView */,
|
||||||
|
);
|
||||||
|
path = Helpers;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
|
A534263E2A7DCC5800EBB7A2 /* Settings */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
A59444F629A2ED5200725BBA /* SettingsView.swift */,
|
||||||
|
);
|
||||||
|
path = Settings;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
A54CD6ED299BEB14008C95BB /* Sources */ = {
|
A54CD6ED299BEB14008C95BB /* Sources */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
A5D495A0299BEC2200DD1313 /* Preview Content */,
|
A53426342A7DA53D00EBB7A2 /* AppDelegate.swift */,
|
||||||
A5CEAFDA29B8005900646FDA /* SplitView */,
|
857F63802A5E64F200CA4815 /* MainMenu.xib */,
|
||||||
|
A53426362A7DC53000EBB7A2 /* Features */,
|
||||||
|
A534263D2A7DCBB000EBB7A2 /* Helpers */,
|
||||||
A55B7BB429B6F4410055DE60 /* Ghostty */,
|
A55B7BB429B6F4410055DE60 /* Ghostty */,
|
||||||
A5B30534299BEAAA0047F10C /* GhosttyApp.swift */,
|
|
||||||
A535B9D9299C569B0017E2E4 /* ErrorView.swift */,
|
|
||||||
A55685DF29A03A9F004303CE /* AppError.swift */,
|
|
||||||
A59444F629A2ED5200725BBA /* SettingsView.swift */,
|
|
||||||
A5CEAFFE29C2410700646FDA /* Backport.swift */,
|
|
||||||
A5FECBD629D1FC3900022361 /* ContentView.swift */,
|
|
||||||
A5FECBD829D2010400022361 /* WindowAccessor.swift */,
|
|
||||||
);
|
);
|
||||||
path = Sources;
|
path = Sources;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
@ -85,6 +130,7 @@
|
|||||||
A55B7BB529B6F47F0055DE60 /* AppState.swift */,
|
A55B7BB529B6F47F0055DE60 /* AppState.swift */,
|
||||||
A55B7BBB29B6FC330055DE60 /* SurfaceView.swift */,
|
A55B7BBB29B6FC330055DE60 /* SurfaceView.swift */,
|
||||||
A55B7BBD29B701360055DE60 /* Ghostty.SplitView.swift */,
|
A55B7BBD29B701360055DE60 /* Ghostty.SplitView.swift */,
|
||||||
|
A55685DF29A03A9F004303CE /* AppError.swift */,
|
||||||
);
|
);
|
||||||
path = Ghostty;
|
path = Ghostty;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
@ -128,13 +174,6 @@
|
|||||||
path = SplitView;
|
path = SplitView;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
};
|
};
|
||||||
A5D495A0299BEC2200DD1313 /* Preview Content */ = {
|
|
||||||
isa = PBXGroup;
|
|
||||||
children = (
|
|
||||||
);
|
|
||||||
path = "Preview Content";
|
|
||||||
sourceTree = "<group>";
|
|
||||||
};
|
|
||||||
A5D495A3299BECBA00DD1313 /* Frameworks */ = {
|
A5D495A3299BECBA00DD1313 /* Frameworks */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
@ -204,6 +243,7 @@
|
|||||||
A545D1A22A5772CE006E0AE4 /* shell-integration in Resources */,
|
A545D1A22A5772CE006E0AE4 /* shell-integration in Resources */,
|
||||||
A5A1F8852A489D6800D1E8BC /* terminfo in Resources */,
|
A5A1F8852A489D6800D1E8BC /* terminfo in Resources */,
|
||||||
A5B30539299BEAAB0047F10C /* Assets.xcassets in Resources */,
|
A5B30539299BEAAB0047F10C /* Assets.xcassets in Resources */,
|
||||||
|
857F63812A5E64F200CA4815 /* MainMenu.xib in Resources */,
|
||||||
);
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
};
|
};
|
||||||
@ -214,9 +254,12 @@
|
|||||||
isa = PBXSourcesBuildPhase;
|
isa = PBXSourcesBuildPhase;
|
||||||
buildActionMask = 2147483647;
|
buildActionMask = 2147483647;
|
||||||
files = (
|
files = (
|
||||||
|
A53426392A7DC55C00EBB7A2 /* PrimaryWindowManager.swift in Sources */,
|
||||||
|
85DE1C922A6A3DCA00493853 /* PrimaryWindow.swift in Sources */,
|
||||||
|
A53426352A7DA53D00EBB7A2 /* AppDelegate.swift in Sources */,
|
||||||
A55B7BBC29B6FC330055DE60 /* SurfaceView.swift in Sources */,
|
A55B7BBC29B6FC330055DE60 /* SurfaceView.swift in Sources */,
|
||||||
A59444F729A2ED5200725BBA /* SettingsView.swift in Sources */,
|
A59444F729A2ED5200725BBA /* SettingsView.swift in Sources */,
|
||||||
A5FECBD729D1FC3900022361 /* ContentView.swift in Sources */,
|
A5FECBD729D1FC3900022361 /* PrimaryView.swift in Sources */,
|
||||||
A55B7BB829B6F53A0055DE60 /* Package.swift in Sources */,
|
A55B7BB829B6F53A0055DE60 /* Package.swift in Sources */,
|
||||||
A55B7BBE29B701360055DE60 /* Ghostty.SplitView.swift in Sources */,
|
A55B7BBE29B701360055DE60 /* Ghostty.SplitView.swift in Sources */,
|
||||||
A55B7BB629B6F47F0055DE60 /* AppState.swift in Sources */,
|
A55B7BB629B6F47F0055DE60 /* AppState.swift in Sources */,
|
||||||
@ -224,8 +267,9 @@
|
|||||||
A55685E029A03A9F004303CE /* AppError.swift in Sources */,
|
A55685E029A03A9F004303CE /* AppError.swift in Sources */,
|
||||||
A5FECBD929D2010400022361 /* WindowAccessor.swift in Sources */,
|
A5FECBD929D2010400022361 /* WindowAccessor.swift in Sources */,
|
||||||
A535B9DA299C569B0017E2E4 /* ErrorView.swift in Sources */,
|
A535B9DA299C569B0017E2E4 /* ErrorView.swift in Sources */,
|
||||||
A5B30535299BEAAA0047F10C /* GhosttyApp.swift in Sources */,
|
85102A1C2A6E32890084AB3E /* PrimaryWindowController.swift in Sources */,
|
||||||
A5CEAFFF29C2410700646FDA /* Backport.swift in Sources */,
|
A5CEAFFF29C2410700646FDA /* Backport.swift in Sources */,
|
||||||
|
8503D7C72A549C66006CFF3D /* FullScreenHandler.swift in Sources */,
|
||||||
A5CEAFDE29B8058B00646FDA /* SplitView.Divider.swift in Sources */,
|
A5CEAFDE29B8058B00646FDA /* SplitView.Divider.swift in Sources */,
|
||||||
);
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
@ -357,7 +401,6 @@
|
|||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
COMBINE_HIDPI_IMAGES = YES;
|
COMBINE_HIDPI_IMAGES = YES;
|
||||||
CURRENT_PROJECT_VERSION = 1;
|
CURRENT_PROJECT_VERSION = 1;
|
||||||
DEVELOPMENT_ASSET_PATHS = "\"Sources/Preview Content\"";
|
|
||||||
DEVELOPMENT_TEAM = "";
|
DEVELOPMENT_TEAM = "";
|
||||||
ENABLE_HARDENED_RUNTIME = YES;
|
ENABLE_HARDENED_RUNTIME = YES;
|
||||||
ENABLE_PREVIEWS = YES;
|
ENABLE_PREVIEWS = YES;
|
||||||
@ -391,7 +434,6 @@
|
|||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
COMBINE_HIDPI_IMAGES = YES;
|
COMBINE_HIDPI_IMAGES = YES;
|
||||||
CURRENT_PROJECT_VERSION = 1;
|
CURRENT_PROJECT_VERSION = 1;
|
||||||
DEVELOPMENT_ASSET_PATHS = "\"Sources/Preview Content\"";
|
|
||||||
DEVELOPMENT_TEAM = "";
|
DEVELOPMENT_TEAM = "";
|
||||||
ENABLE_HARDENED_RUNTIME = YES;
|
ENABLE_HARDENED_RUNTIME = YES;
|
||||||
ENABLE_PREVIEWS = YES;
|
ENABLE_PREVIEWS = YES;
|
||||||
|
@ -0,0 +1,84 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<Scheme
|
||||||
|
LastUpgradeVersion = "1430"
|
||||||
|
version = "1.7">
|
||||||
|
<BuildAction
|
||||||
|
parallelizeBuildables = "YES"
|
||||||
|
buildImplicitDependencies = "YES">
|
||||||
|
<BuildActionEntries>
|
||||||
|
<BuildActionEntry
|
||||||
|
buildForTesting = "YES"
|
||||||
|
buildForRunning = "YES"
|
||||||
|
buildForProfiling = "YES"
|
||||||
|
buildForArchiving = "YES"
|
||||||
|
buildForAnalyzing = "YES">
|
||||||
|
<BuildableReference
|
||||||
|
BuildableIdentifier = "primary"
|
||||||
|
BlueprintIdentifier = "A5B30530299BEAAA0047F10C"
|
||||||
|
BuildableName = "Ghostty.app"
|
||||||
|
BlueprintName = "Ghostty"
|
||||||
|
ReferencedContainer = "container:Ghostty.xcodeproj">
|
||||||
|
</BuildableReference>
|
||||||
|
</BuildActionEntry>
|
||||||
|
</BuildActionEntries>
|
||||||
|
</BuildAction>
|
||||||
|
<TestAction
|
||||||
|
buildConfiguration = "Debug"
|
||||||
|
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||||
|
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||||
|
shouldUseLaunchSchemeArgsEnv = "YES"
|
||||||
|
shouldAutocreateTestPlan = "YES">
|
||||||
|
</TestAction>
|
||||||
|
<LaunchAction
|
||||||
|
buildConfiguration = "Debug"
|
||||||
|
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||||
|
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||||
|
launchStyle = "0"
|
||||||
|
useCustomWorkingDirectory = "NO"
|
||||||
|
ignoresPersistentStateOnLaunch = "NO"
|
||||||
|
debugDocumentVersioning = "YES"
|
||||||
|
debugServiceExtension = "internal"
|
||||||
|
allowLocationSimulation = "YES">
|
||||||
|
<BuildableProductRunnable
|
||||||
|
runnableDebuggingMode = "0">
|
||||||
|
<BuildableReference
|
||||||
|
BuildableIdentifier = "primary"
|
||||||
|
BlueprintIdentifier = "A5B30530299BEAAA0047F10C"
|
||||||
|
BuildableName = "Ghostty.app"
|
||||||
|
BlueprintName = "Ghostty"
|
||||||
|
ReferencedContainer = "container:Ghostty.xcodeproj">
|
||||||
|
</BuildableReference>
|
||||||
|
</BuildableProductRunnable>
|
||||||
|
<AdditionalOptions>
|
||||||
|
<AdditionalOption
|
||||||
|
key = "NSZombieEnabled"
|
||||||
|
value = "YES"
|
||||||
|
isEnabled = "YES">
|
||||||
|
</AdditionalOption>
|
||||||
|
</AdditionalOptions>
|
||||||
|
</LaunchAction>
|
||||||
|
<ProfileAction
|
||||||
|
buildConfiguration = "Release"
|
||||||
|
shouldUseLaunchSchemeArgsEnv = "YES"
|
||||||
|
savedToolIdentifier = ""
|
||||||
|
useCustomWorkingDirectory = "NO"
|
||||||
|
debugDocumentVersioning = "YES">
|
||||||
|
<BuildableProductRunnable
|
||||||
|
runnableDebuggingMode = "0">
|
||||||
|
<BuildableReference
|
||||||
|
BuildableIdentifier = "primary"
|
||||||
|
BlueprintIdentifier = "A5B30530299BEAAA0047F10C"
|
||||||
|
BuildableName = "Ghostty.app"
|
||||||
|
BlueprintName = "Ghostty"
|
||||||
|
ReferencedContainer = "container:Ghostty.xcodeproj">
|
||||||
|
</BuildableReference>
|
||||||
|
</BuildableProductRunnable>
|
||||||
|
</ProfileAction>
|
||||||
|
<AnalyzeAction
|
||||||
|
buildConfiguration = "Debug">
|
||||||
|
</AnalyzeAction>
|
||||||
|
<ArchiveAction
|
||||||
|
buildConfiguration = "Release"
|
||||||
|
revealArchiveInOrganizer = "YES">
|
||||||
|
</ArchiveAction>
|
||||||
|
</Scheme>
|
143
macos/Sources/AppDelegate.swift
Normal file
143
macos/Sources/AppDelegate.swift
Normal file
@ -0,0 +1,143 @@
|
|||||||
|
import AppKit
|
||||||
|
import OSLog
|
||||||
|
import GhosttyKit
|
||||||
|
|
||||||
|
@NSApplicationMain
|
||||||
|
class AppDelegate: NSObject, NSApplicationDelegate, ObservableObject {
|
||||||
|
// The application logger. We should probably move this at some point to a dedicated
|
||||||
|
// class/struct but for now it lives here! 🤷♂️
|
||||||
|
static let logger = Logger(
|
||||||
|
subsystem: Bundle.main.bundleIdentifier!,
|
||||||
|
category: String(describing: AppDelegate.self)
|
||||||
|
)
|
||||||
|
|
||||||
|
// confirmQuit published so other views can check whether quit needs to be confirmed.
|
||||||
|
@Published var confirmQuit: Bool = false
|
||||||
|
|
||||||
|
/// The ghostty global state. Only one per process.
|
||||||
|
private var ghostty: Ghostty.AppState = Ghostty.AppState()
|
||||||
|
|
||||||
|
/// Manages windows and tabs, ensuring they're allocated/deallocated correctly
|
||||||
|
private var windowManager: PrimaryWindowManager!
|
||||||
|
|
||||||
|
override init() {
|
||||||
|
super.init()
|
||||||
|
|
||||||
|
windowManager = PrimaryWindowManager(ghostty: self.ghostty)
|
||||||
|
}
|
||||||
|
|
||||||
|
func applicationDidFinishLaunching(_ notification: Notification) {
|
||||||
|
// System settings overrides
|
||||||
|
UserDefaults.standard.register(defaults: [
|
||||||
|
// Disable this so that repeated key events make it through to our terminal views.
|
||||||
|
"ApplePressAndHoldEnabled": false,
|
||||||
|
])
|
||||||
|
|
||||||
|
// Let's launch our first window.
|
||||||
|
// TODO: we should detect if we restored windows and if so not launch a new window.
|
||||||
|
windowManager.addInitialWindow()
|
||||||
|
}
|
||||||
|
|
||||||
|
func applicationShouldTerminate(_ sender: NSApplication) -> NSApplication.TerminateReply {
|
||||||
|
let windows = NSApplication.shared.windows
|
||||||
|
if (windows.isEmpty) { return .terminateNow }
|
||||||
|
|
||||||
|
// This probably isn't fully safe. The isEmpty check above is aspirational, it doesn't
|
||||||
|
// quite work with SwiftUI because windows are retained on close. So instead we check
|
||||||
|
// if there are any that are visible. I'm guessing this breaks under certain scenarios.
|
||||||
|
if (windows.allSatisfy { !$0.isVisible }) { return .terminateNow }
|
||||||
|
|
||||||
|
// If the user is shutting down, restarting, or logging out, we don't confirm quit.
|
||||||
|
if let event = NSAppleEventManager.shared().currentAppleEvent {
|
||||||
|
if let why = event.attributeDescriptor(forKeyword: AEKeyword("why?")!) {
|
||||||
|
switch (why.typeCodeValue) {
|
||||||
|
case kAEShutDown:
|
||||||
|
fallthrough
|
||||||
|
|
||||||
|
case kAERestart:
|
||||||
|
fallthrough
|
||||||
|
|
||||||
|
case kAEReallyLogOut:
|
||||||
|
return .terminateNow
|
||||||
|
|
||||||
|
default:
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// We have some visible window, and all our windows will watch the confirmQuit.
|
||||||
|
confirmQuit = true
|
||||||
|
return .terminateLater
|
||||||
|
}
|
||||||
|
|
||||||
|
@IBAction func newWindow(_ sender: Any?) {
|
||||||
|
windowManager.addNewWindow()
|
||||||
|
}
|
||||||
|
|
||||||
|
@IBAction func newTab(_ sender: Any?) {
|
||||||
|
if let existingWindow = windowManager.mainWindow {
|
||||||
|
windowManager.addNewTab(to: existingWindow)
|
||||||
|
} else {
|
||||||
|
windowManager.addNewWindow()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@IBAction func closeWindow(_ sender: Any) {
|
||||||
|
guard let currentWindow = NSApp.keyWindow else { return }
|
||||||
|
currentWindow.close()
|
||||||
|
}
|
||||||
|
|
||||||
|
@IBAction func close(_ sender: Any) {
|
||||||
|
guard let surface = focusedSurface() else {
|
||||||
|
self.closeWindow(self)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
ghostty.requestClose(surface: surface)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func focusedSurface() -> ghostty_surface_t? {
|
||||||
|
guard let window = NSApp.keyWindow as? PrimaryWindow else { return nil }
|
||||||
|
return window.focusedSurfaceWrapper.surface
|
||||||
|
}
|
||||||
|
|
||||||
|
@IBAction func splitHorizontally(_ sender: Any) {
|
||||||
|
guard let surface = focusedSurface() else { return }
|
||||||
|
ghostty.split(surface: surface, direction: GHOSTTY_SPLIT_RIGHT)
|
||||||
|
}
|
||||||
|
|
||||||
|
@IBAction func splitVertically(_ sender: Any) {
|
||||||
|
guard let surface = focusedSurface() else { return }
|
||||||
|
ghostty.split(surface: surface, direction: GHOSTTY_SPLIT_DOWN)
|
||||||
|
}
|
||||||
|
|
||||||
|
@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)
|
||||||
|
}
|
||||||
|
|
||||||
|
func splitMoveFocus(direction: Ghostty.SplitFocusDirection) {
|
||||||
|
guard let surface = focusedSurface() else { return }
|
||||||
|
ghostty.splitMoveFocus(surface: surface, direction: direction)
|
||||||
|
}
|
||||||
|
}
|
@ -1,16 +1,29 @@
|
|||||||
import SwiftUI
|
import SwiftUI
|
||||||
import GhosttyKit
|
import GhosttyKit
|
||||||
|
|
||||||
struct ContentView: View {
|
struct PrimaryView: View {
|
||||||
let ghostty: Ghostty.AppState
|
let ghostty: Ghostty.AppState
|
||||||
|
|
||||||
// We need access to our app delegate to know if we're quitting or not.
|
// We need access to our app delegate to know if we're quitting or not.
|
||||||
@EnvironmentObject private var appDelegate: AppDelegate
|
// Make sure to use `@ObservedObject` so we can keep track of `appDelegate.confirmQuit`.
|
||||||
|
@ObservedObject var appDelegate: AppDelegate
|
||||||
|
|
||||||
|
// We need this to report back up the app controller which surface in this view is focused.
|
||||||
|
let focusedSurfaceWrapper: FocusedSurfaceWrapper
|
||||||
|
|
||||||
// We need access to our window to know if we're the key window to determine
|
// We need access to our window to know if we're the key window to determine
|
||||||
// if we show the quit confirmation or not.
|
// if we show the quit confirmation or not.
|
||||||
@State private var window: NSWindow?
|
@State private var window: NSWindow?
|
||||||
|
|
||||||
|
// This handles non-native fullscreen
|
||||||
|
@State private var fullScreen = FullScreenHandler()
|
||||||
|
|
||||||
|
// This seems like a crutch after switchign from SwiftUI to AppKit lifecycle.
|
||||||
|
@FocusState private var focused: Bool
|
||||||
|
|
||||||
|
@FocusedValue(\.ghosttySurfaceView) private var focusedSurface
|
||||||
|
@FocusedValue(\.ghosttySurfaceTitle) private var surfaceTitle
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
switch ghostty.readiness {
|
switch ghostty.readiness {
|
||||||
case .loading:
|
case .loading:
|
||||||
@ -35,12 +48,24 @@ struct ContentView: View {
|
|||||||
}, set: {
|
}, set: {
|
||||||
self.appDelegate.confirmQuit = $0
|
self.appDelegate.confirmQuit = $0
|
||||||
})
|
})
|
||||||
|
|
||||||
Ghostty.TerminalSplit(onClose: Self.closeWindow)
|
Ghostty.TerminalSplit(onClose: Self.closeWindow)
|
||||||
.ghosttyApp(ghostty.app!)
|
.ghosttyApp(ghostty.app!)
|
||||||
.background(WindowAccessor(window: $window))
|
.background(WindowAccessor(window: $window))
|
||||||
.onReceive(gotoTab) { onGotoTab(notification: $0) }
|
.onReceive(gotoTab) { onGotoTab(notification: $0) }
|
||||||
.onReceive(toggleFullscreen) { onToggleFullscreen(notification: $0) }
|
.onReceive(toggleFullscreen) { onToggleFullscreen(notification: $0) }
|
||||||
|
.focused($focused)
|
||||||
|
.onAppear { self.focused = true }
|
||||||
|
.onChange(of: focusedSurface) { newValue in
|
||||||
|
self.focusedSurfaceWrapper.surface = newValue?.surface
|
||||||
|
}
|
||||||
|
.onChange(of: surfaceTitle) { newValue in
|
||||||
|
// We need to handle this manually because we are using AppKit lifecycle
|
||||||
|
// so navigationTitle no longer works.
|
||||||
|
guard let window = self.window else { return }
|
||||||
|
guard let title = newValue else { return }
|
||||||
|
window.title = title
|
||||||
|
}
|
||||||
.confirmationDialog(
|
.confirmationDialog(
|
||||||
"Quit Ghostty?",
|
"Quit Ghostty?",
|
||||||
isPresented: confirmQuitting) {
|
isPresented: confirmQuitting) {
|
||||||
@ -63,7 +88,7 @@ struct ContentView: View {
|
|||||||
guard let currentWindow = NSApp.keyWindow else { return }
|
guard let currentWindow = NSApp.keyWindow else { return }
|
||||||
currentWindow.close()
|
currentWindow.close()
|
||||||
}
|
}
|
||||||
|
|
||||||
private func onGotoTab(notification: SwiftUI.Notification) {
|
private func onGotoTab(notification: SwiftUI.Notification) {
|
||||||
// Notification center indiscriminately sends to every subscriber (makes sense)
|
// Notification center indiscriminately sends to every subscriber (makes sense)
|
||||||
// but we only want to process this once. In order to process it once lets only
|
// but we only want to process this once. In order to process it once lets only
|
||||||
@ -93,7 +118,13 @@ struct ContentView: View {
|
|||||||
// currently focused window.
|
// currently focused window.
|
||||||
guard let window = self.window else { return }
|
guard let window = self.window else { return }
|
||||||
guard window.isKeyWindow else { return }
|
guard window.isKeyWindow else { return }
|
||||||
|
|
||||||
window.toggleFullScreen(nil)
|
// Check whether we use non-native fullscreen
|
||||||
|
guard let useNonNativeFullscreenAny = notification.userInfo?[Ghostty.Notification.NonNativeFullscreenKey] else { return }
|
||||||
|
guard let useNonNativeFullscreen = useNonNativeFullscreenAny as? Bool else { return }
|
||||||
|
|
||||||
|
self.fullScreen.toggleFullscreen(window: window, nonNativeFullscreen: useNonNativeFullscreen)
|
||||||
|
// After toggling fullscreen we need to focus the terminal again.
|
||||||
|
self.focused = true
|
||||||
}
|
}
|
||||||
}
|
}
|
48
macos/Sources/Features/Primary Window/PrimaryWindow.swift
Normal file
48
macos/Sources/Features/Primary Window/PrimaryWindow.swift
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
import Cocoa
|
||||||
|
import SwiftUI
|
||||||
|
import GhosttyKit
|
||||||
|
|
||||||
|
// FocusedSurfaceWrapper is here so that we can pass a reference down
|
||||||
|
// the view hierarchy and keep track of which surface is focused.
|
||||||
|
class FocusedSurfaceWrapper {
|
||||||
|
var surface: ghostty_surface_t?
|
||||||
|
}
|
||||||
|
|
||||||
|
// PrimaryWindow is the primary window you'd associate with a terminal: the window
|
||||||
|
// that contains one or more terminals (splits, and such).
|
||||||
|
//
|
||||||
|
// We need to subclass NSWindow so that we can override some methods for features
|
||||||
|
// such as non-native fullscreen.
|
||||||
|
class PrimaryWindow: NSWindow {
|
||||||
|
var focusedSurfaceWrapper: FocusedSurfaceWrapper = FocusedSurfaceWrapper()
|
||||||
|
|
||||||
|
static func create(ghostty: Ghostty.AppState, appDelegate: AppDelegate) -> PrimaryWindow {
|
||||||
|
let window = PrimaryWindow(
|
||||||
|
contentRect: NSRect(x: 0, y: 0, width: 800, height: 600),
|
||||||
|
styleMask: [.titled, .closable, .miniaturizable, .resizable],
|
||||||
|
backing: .buffered,
|
||||||
|
defer: false)
|
||||||
|
window.center()
|
||||||
|
|
||||||
|
window.contentView = NSHostingView(rootView: PrimaryView(
|
||||||
|
ghostty: ghostty,
|
||||||
|
appDelegate: appDelegate,
|
||||||
|
focusedSurfaceWrapper: window.focusedSurfaceWrapper))
|
||||||
|
|
||||||
|
// We do want to cascade when new windows are created
|
||||||
|
window.windowController?.shouldCascadeWindows = true
|
||||||
|
|
||||||
|
// A default title. This should be overwritten quickly by the Ghostty core.
|
||||||
|
window.title = "Ghostty 👻"
|
||||||
|
|
||||||
|
return window
|
||||||
|
}
|
||||||
|
|
||||||
|
override var canBecomeKey: Bool {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
override var canBecomeMain: Bool {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,19 @@
|
|||||||
|
import Cocoa
|
||||||
|
|
||||||
|
class PrimaryWindowController: NSWindowController {
|
||||||
|
// Keep track of the last point that our window was launched at so that new
|
||||||
|
// windows "cascade" over each other and don't just launch directly on top
|
||||||
|
// of each other.
|
||||||
|
static var lastCascadePoint = NSPoint(x: 0, y: 0)
|
||||||
|
|
||||||
|
// This is used to programmatically control tabs.
|
||||||
|
weak var windowManager: PrimaryWindowManager?
|
||||||
|
|
||||||
|
// This is required for the "+" button to show up in the tab bar to add a
|
||||||
|
// new tab.
|
||||||
|
override func newWindowForTab(_ sender: Any?) {
|
||||||
|
guard let window = self.window else { preconditionFailure("Expected window to be loaded") }
|
||||||
|
guard let manager = self.windowManager else { return }
|
||||||
|
manager.addNewTab(to: window)
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,98 @@
|
|||||||
|
import Cocoa
|
||||||
|
import Combine
|
||||||
|
|
||||||
|
// PrimaryWindowManager manages the windows and tabs in the primary window
|
||||||
|
// of the application. It keeps references to windows and cleans them up when
|
||||||
|
// they're cloned.
|
||||||
|
//
|
||||||
|
// If we ever have multiple tabbed window types we can make this generic but
|
||||||
|
// right now only our primary window is ever duplicated or tabbed so we're not
|
||||||
|
// doing that.
|
||||||
|
//
|
||||||
|
// It is based on the patterns presented in this blog post:
|
||||||
|
// https://christiantietze.de/posts/2019/07/nswindow-tabbing-multiple-nswindowcontroller/
|
||||||
|
class PrimaryWindowManager {
|
||||||
|
struct ManagedWindow {
|
||||||
|
let windowController: NSWindowController
|
||||||
|
let window: NSWindow
|
||||||
|
let closePublisher: AnyCancellable
|
||||||
|
}
|
||||||
|
|
||||||
|
// Keep track of the last point that our window was launched at so that new
|
||||||
|
// windows "cascade" over each other and don't just launch directly on top
|
||||||
|
// of each other.
|
||||||
|
static var lastCascadePoint = NSPoint(x: 0, y: 0)
|
||||||
|
|
||||||
|
/// Returns the main window of the managed window stack.
|
||||||
|
/// Falls back the first element if no window is main. Note that this would
|
||||||
|
/// likely be an internal inconsistency we gracefully handle here.
|
||||||
|
var mainWindow: NSWindow? {
|
||||||
|
let mainManagedWindow = managedWindows
|
||||||
|
.first { $0.window.isMainWindow }
|
||||||
|
|
||||||
|
// In case we run into the inconsistency, let it crash in debug mode so we
|
||||||
|
// can fix our window management setup to prevent this from happening.
|
||||||
|
assert(mainManagedWindow != nil || managedWindows.isEmpty)
|
||||||
|
|
||||||
|
return (mainManagedWindow ?? managedWindows.first)
|
||||||
|
.map { $0.window }
|
||||||
|
}
|
||||||
|
|
||||||
|
private var ghostty: Ghostty.AppState
|
||||||
|
private var managedWindows: [ManagedWindow] = []
|
||||||
|
|
||||||
|
init(ghostty: Ghostty.AppState) {
|
||||||
|
self.ghostty = ghostty
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Add the initial window for the application. This should only be called once from the AppDelegate.
|
||||||
|
func addInitialWindow() {
|
||||||
|
guard let controller = createWindowController() else { return }
|
||||||
|
controller.showWindow(self)
|
||||||
|
let result = addManagedWindow(windowController: controller)
|
||||||
|
if result == nil {
|
||||||
|
preconditionFailure("Failed to create initial window")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func addNewWindow() {
|
||||||
|
guard let controller = createWindowController() else { return }
|
||||||
|
guard let newWindow = addManagedWindow(windowController: controller)?.window else { return }
|
||||||
|
newWindow.makeKeyAndOrderFront(nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
func addNewTab(to window: NSWindow) {
|
||||||
|
guard let controller = createWindowController() else { return }
|
||||||
|
guard let newWindow = addManagedWindow(windowController: controller)?.window else { return }
|
||||||
|
window.addTabbedWindow(newWindow, ordered: .above)
|
||||||
|
newWindow.makeKeyAndOrderFront(nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func createWindowController() -> PrimaryWindowController? {
|
||||||
|
guard let appDelegate = NSApplication.shared.delegate as? AppDelegate else { return nil }
|
||||||
|
let window = PrimaryWindow.create(ghostty: ghostty, appDelegate: appDelegate)
|
||||||
|
Self.lastCascadePoint = window.cascadeTopLeft(from: Self.lastCascadePoint)
|
||||||
|
let controller = PrimaryWindowController(window: window)
|
||||||
|
controller.windowManager = self
|
||||||
|
return controller
|
||||||
|
}
|
||||||
|
|
||||||
|
private func addManagedWindow(windowController: PrimaryWindowController) -> ManagedWindow? {
|
||||||
|
guard let window = windowController.window else { return nil }
|
||||||
|
|
||||||
|
let pubClose = NotificationCenter.default.publisher(for: NSWindow.willCloseNotification, object: window)
|
||||||
|
.sink { notification in
|
||||||
|
guard let window = notification.object as? NSWindow else { return }
|
||||||
|
self.removeWindow(window: window)
|
||||||
|
}
|
||||||
|
|
||||||
|
let managed = ManagedWindow(windowController: windowController, window: window, closePublisher: pubClose)
|
||||||
|
managedWindows.append(managed)
|
||||||
|
|
||||||
|
return managed
|
||||||
|
}
|
||||||
|
|
||||||
|
private func removeWindow(window: NSWindow) {
|
||||||
|
self.managedWindows.removeAll(where: { $0.window === window })
|
||||||
|
}
|
||||||
|
}
|
@ -38,7 +38,7 @@ extension Ghostty {
|
|||||||
init() {
|
init() {
|
||||||
// Initialize ghostty global state. This happens once per process.
|
// Initialize ghostty global state. This happens once per process.
|
||||||
guard ghostty_init() == GHOSTTY_SUCCESS else {
|
guard ghostty_init() == GHOSTTY_SUCCESS else {
|
||||||
GhosttyApp.logger.critical("ghostty_init failed")
|
AppDelegate.logger.critical("ghostty_init failed")
|
||||||
readiness = .error
|
readiness = .error
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -63,12 +63,12 @@ extension Ghostty {
|
|||||||
close_surface_cb: { userdata, processAlive in AppState.closeSurface(userdata, processAlive: processAlive) },
|
close_surface_cb: { userdata, processAlive in AppState.closeSurface(userdata, processAlive: processAlive) },
|
||||||
focus_split_cb: { userdata, direction in AppState.focusSplit(userdata, direction: direction) },
|
focus_split_cb: { userdata, direction in AppState.focusSplit(userdata, direction: direction) },
|
||||||
goto_tab_cb: { userdata, n in AppState.gotoTab(userdata, n: n) },
|
goto_tab_cb: { userdata, n in AppState.gotoTab(userdata, n: n) },
|
||||||
toggle_fullscreen_cb: { userdata in AppState.toggleFullscreen(userdata) }
|
toggle_fullscreen_cb: { userdata, nonNativeFullscreen in AppState.toggleFullscreen(userdata, useNonNativeFullscreen: nonNativeFullscreen) }
|
||||||
)
|
)
|
||||||
|
|
||||||
// Create the ghostty app.
|
// Create the ghostty app.
|
||||||
guard let app = ghostty_app_new(&runtime_cfg, cfg) else {
|
guard let app = ghostty_app_new(&runtime_cfg, cfg) else {
|
||||||
GhosttyApp.logger.critical("ghostty_app_new failed")
|
AppDelegate.logger.critical("ghostty_app_new failed")
|
||||||
readiness = .error
|
readiness = .error
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -87,7 +87,7 @@ extension Ghostty {
|
|||||||
static func reloadConfig() -> ghostty_config_t? {
|
static func reloadConfig() -> ghostty_config_t? {
|
||||||
// Initialize the global configuration.
|
// Initialize the global configuration.
|
||||||
guard let cfg = ghostty_config_new() else {
|
guard let cfg = ghostty_config_new() else {
|
||||||
GhosttyApp.logger.critical("ghostty_config_new failed")
|
AppDelegate.logger.critical("ghostty_config_new failed")
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -189,7 +189,7 @@ extension Ghostty {
|
|||||||
|
|
||||||
static func reloadConfig(_ userdata: UnsafeMutableRawPointer?) -> ghostty_config_t? {
|
static func reloadConfig(_ userdata: UnsafeMutableRawPointer?) -> ghostty_config_t? {
|
||||||
guard let newConfig = AppState.reloadConfig() else {
|
guard let newConfig = AppState.reloadConfig() else {
|
||||||
GhosttyApp.logger.warning("failed to reload configuration")
|
AppDelegate.logger.warning("failed to reload configuration")
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -219,12 +219,15 @@ extension Ghostty {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static func toggleFullscreen(_ userdata: UnsafeMutableRawPointer?) {
|
static func toggleFullscreen(_ userdata: UnsafeMutableRawPointer?, useNonNativeFullscreen: Bool) {
|
||||||
|
// togo: use non-native fullscreen
|
||||||
guard let surface = self.surfaceUserdata(from: userdata) else { return }
|
guard let surface = self.surfaceUserdata(from: userdata) else { return }
|
||||||
NotificationCenter.default.post(
|
NotificationCenter.default.post(
|
||||||
name: Notification.ghosttyToggleFullscreen,
|
name: Notification.ghosttyToggleFullscreen,
|
||||||
object: surface,
|
object: surface,
|
||||||
userInfo: [:]
|
userInfo: [
|
||||||
|
Notification.NonNativeFullscreenKey: useNonNativeFullscreen,
|
||||||
|
]
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -81,6 +81,7 @@ extension Ghostty.Notification {
|
|||||||
|
|
||||||
/// Toggle fullscreen of current window
|
/// Toggle fullscreen of current window
|
||||||
static let ghosttyToggleFullscreen = Notification.Name("com.mitchellh.ghostty.toggleFullscreen")
|
static let ghosttyToggleFullscreen = Notification.Name("com.mitchellh.ghostty.toggleFullscreen")
|
||||||
|
static let NonNativeFullscreenKey = ghosttyToggleFullscreen.rawValue
|
||||||
}
|
}
|
||||||
|
|
||||||
// Make the input enum hashable.
|
// Make the input enum hashable.
|
||||||
|
@ -1,197 +0,0 @@
|
|||||||
import OSLog
|
|
||||||
import SwiftUI
|
|
||||||
import GhosttyKit
|
|
||||||
|
|
||||||
@main
|
|
||||||
struct GhosttyApp: App {
|
|
||||||
static let logger = Logger(
|
|
||||||
subsystem: Bundle.main.bundleIdentifier!,
|
|
||||||
category: String(describing: GhosttyApp.self)
|
|
||||||
)
|
|
||||||
|
|
||||||
/// The ghostty global state. Only one per process.
|
|
||||||
@StateObject private var ghostty = Ghostty.AppState()
|
|
||||||
@NSApplicationDelegateAdaptor(AppDelegate.self) private var appDelegate
|
|
||||||
|
|
||||||
/// The current focused Ghostty surface in this app
|
|
||||||
@FocusedValue(\.ghosttySurfaceView) private var focusedSurface
|
|
||||||
|
|
||||||
var body: some Scene {
|
|
||||||
WindowGroup {
|
|
||||||
ContentView(ghostty: ghostty)
|
|
||||||
}
|
|
||||||
|
|
||||||
.backport.defaultSize(width: 800, height: 600)
|
|
||||||
|
|
||||||
.commands {
|
|
||||||
CommandGroup(after: .newItem) {
|
|
||||||
Button("New Tab", action: Self.newTab).keyboardShortcut("t", modifiers: [.command])
|
|
||||||
Divider()
|
|
||||||
Button("Split Horizontally", action: splitHorizontally).keyboardShortcut("d", modifiers: [.command])
|
|
||||||
Button("Split Vertically", action: splitVertically).keyboardShortcut("d", modifiers: [.command, .shift])
|
|
||||||
Divider()
|
|
||||||
Button("Close", action: close).keyboardShortcut("w", modifiers: [.command])
|
|
||||||
Button("Close Window", action: Self.closeWindow).keyboardShortcut("w", modifiers: [.command, .shift])
|
|
||||||
}
|
|
||||||
|
|
||||||
CommandGroup(before: .windowArrangement) {
|
|
||||||
Divider()
|
|
||||||
Button("Select Previous Split") { splitMoveFocus(direction: .previous) }
|
|
||||||
.keyboardShortcut("[", modifiers: .command)
|
|
||||||
Button("Select Next Split") { splitMoveFocus(direction: .next) }
|
|
||||||
.keyboardShortcut("]", modifiers: .command)
|
|
||||||
Menu("Select Split") {
|
|
||||||
Button("Select Split Above") { splitMoveFocus(direction: .top) }
|
|
||||||
.keyboardShortcut(.upArrow, modifiers: [.command, .option])
|
|
||||||
Button("Select Split Below") { splitMoveFocus(direction: .bottom) }
|
|
||||||
.keyboardShortcut(.downArrow, modifiers: [.command, .option])
|
|
||||||
Button("Select Split Left") { splitMoveFocus(direction: .left) }
|
|
||||||
.keyboardShortcut(.leftArrow, modifiers: [.command, .option])
|
|
||||||
Button("Select Split Right") { splitMoveFocus(direction: .right)}
|
|
||||||
.keyboardShortcut(.rightArrow, modifiers: [.command, .option])
|
|
||||||
}
|
|
||||||
|
|
||||||
Divider()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Settings {
|
|
||||||
SettingsView()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create a new tab in the currently active window
|
|
||||||
static func newTab() {
|
|
||||||
guard let currentWindow = NSApp.keyWindow else { return }
|
|
||||||
guard let windowController = currentWindow.windowController else { return }
|
|
||||||
windowController.newWindowForTab(nil)
|
|
||||||
if let newWindow = NSApp.keyWindow, currentWindow != newWindow {
|
|
||||||
currentWindow.addTabbedWindow(newWindow, ordered: .above)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static func closeWindow() {
|
|
||||||
guard let currentWindow = NSApp.keyWindow else { return }
|
|
||||||
currentWindow.close()
|
|
||||||
}
|
|
||||||
|
|
||||||
func close() {
|
|
||||||
guard let surfaceView = focusedSurface else {
|
|
||||||
Self.closeWindow()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
guard let surface = surfaceView.surface else { return }
|
|
||||||
ghostty.requestClose(surface: surface)
|
|
||||||
}
|
|
||||||
|
|
||||||
func splitHorizontally() {
|
|
||||||
guard let surfaceView = focusedSurface else { return }
|
|
||||||
guard let surface = surfaceView.surface else { return }
|
|
||||||
ghostty.split(surface: surface, direction: GHOSTTY_SPLIT_RIGHT)
|
|
||||||
}
|
|
||||||
|
|
||||||
func splitVertically() {
|
|
||||||
guard let surfaceView = focusedSurface else { return }
|
|
||||||
guard let surface = surfaceView.surface else { return }
|
|
||||||
ghostty.split(surface: surface, direction: GHOSTTY_SPLIT_DOWN)
|
|
||||||
}
|
|
||||||
|
|
||||||
func splitMoveFocus(direction: Ghostty.SplitFocusDirection) {
|
|
||||||
guard let surfaceView = focusedSurface else { return }
|
|
||||||
guard let surface = surfaceView.surface else { return }
|
|
||||||
ghostty.splitMoveFocus(surface: surface, direction: direction)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class AppDelegate: NSObject, NSApplicationDelegate, ObservableObject {
|
|
||||||
@Published var confirmQuit: Bool = false
|
|
||||||
|
|
||||||
// See CursedMenuManager for more information.
|
|
||||||
private var menuManager: CursedMenuManager?
|
|
||||||
|
|
||||||
func applicationDidFinishLaunching(_ notification: Notification) {
|
|
||||||
UserDefaults.standard.register(defaults: [
|
|
||||||
// Disable this so that repeated key events make it through to our terminal views.
|
|
||||||
"ApplePressAndHoldEnabled": false,
|
|
||||||
])
|
|
||||||
|
|
||||||
// Create our menu manager to create some custom menu items that
|
|
||||||
// we can't create from SwiftUI.
|
|
||||||
menuManager = CursedMenuManager()
|
|
||||||
}
|
|
||||||
|
|
||||||
func applicationShouldTerminate(_ sender: NSApplication) -> NSApplication.TerminateReply {
|
|
||||||
let windows = NSApplication.shared.windows
|
|
||||||
if (windows.isEmpty) { return .terminateNow }
|
|
||||||
|
|
||||||
// This probably isn't fully safe. The isEmpty check above is aspirational, it doesn't
|
|
||||||
// quite work with SwiftUI because windows are retained on close. So instead we check
|
|
||||||
// if there are any that are visible. I'm guessing this breaks under certain scenarios.
|
|
||||||
if (windows.allSatisfy { !$0.isVisible }) { return .terminateNow }
|
|
||||||
|
|
||||||
// If the user is shutting down, restarting, or logging out, we don't confirm quit.
|
|
||||||
if let event = NSAppleEventManager.shared().currentAppleEvent {
|
|
||||||
if let why = event.attributeDescriptor(forKeyword: AEKeyword("why?")!) {
|
|
||||||
switch (why.typeCodeValue) {
|
|
||||||
case kAEShutDown:
|
|
||||||
fallthrough
|
|
||||||
|
|
||||||
case kAERestart:
|
|
||||||
fallthrough
|
|
||||||
|
|
||||||
case kAEReallyLogOut:
|
|
||||||
return .terminateNow
|
|
||||||
|
|
||||||
default:
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// We have some visible window, and all our windows will watch the confirmQuit.
|
|
||||||
confirmQuit = true
|
|
||||||
return .terminateLater
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// SwiftUI as of macOS 13.x provides no way to manage the default menu items that are created
|
|
||||||
/// as part of a WindowGroup. This class is prefixed with "Cursed" because this is a truly cursed
|
|
||||||
/// solution to the problem and I think its quite brittle. As soon as SwiftUI supports a better option
|
|
||||||
/// we should conditionally compile for that when supported.
|
|
||||||
///
|
|
||||||
/// The way this works is by setting up KVO on various menu objects and reacting to it. For example,
|
|
||||||
/// when SwiftUI tries to add a "Close" menu, we intercept it and delete it. Nice try!
|
|
||||||
private class CursedMenuManager {
|
|
||||||
var mainToken: NSKeyValueObservation?
|
|
||||||
var fileToken: NSKeyValueObservation?
|
|
||||||
|
|
||||||
init() {
|
|
||||||
// If the whole menu changed we want to setup our new KVO
|
|
||||||
self.mainToken = NSApp.observe(\.mainMenu, options: .new) { app, change in
|
|
||||||
self.onNewMenu()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Initial setup
|
|
||||||
onNewMenu()
|
|
||||||
}
|
|
||||||
|
|
||||||
private func onNewMenu() {
|
|
||||||
guard let menu = NSApp.mainMenu else { return }
|
|
||||||
guard let file = menu.item(withTitle: "File") else { return }
|
|
||||||
guard let submenu = file.submenu else { return }
|
|
||||||
fileToken = submenu.observe(\.items) { (_, _) in
|
|
||||||
let remove = ["Close", "Close All"]
|
|
||||||
|
|
||||||
// We look for the items in reverse since we're removing only the
|
|
||||||
// ones SwiftUI inserts which are at the end. We make replacements
|
|
||||||
// which we DON'T want deleted.
|
|
||||||
let items = submenu.items.reversed()
|
|
||||||
remove.forEach { title in
|
|
||||||
if let item = items.first(where: { $0.title.caseInsensitiveCompare(title) == .orderedSame }) {
|
|
||||||
submenu.removeItem(item)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
103
macos/Sources/Helpers/FullScreenHandler.swift
Normal file
103
macos/Sources/Helpers/FullScreenHandler.swift
Normal file
@ -0,0 +1,103 @@
|
|||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
class FullScreenHandler {
|
||||||
|
var previousTabGroup: NSWindowTabGroup?
|
||||||
|
var previousTabGroupIndex: Int?
|
||||||
|
var previousContentFrame: NSRect?
|
||||||
|
var previousStyleMask: NSWindow.StyleMask?
|
||||||
|
var isInFullscreen: Bool = false
|
||||||
|
|
||||||
|
// We keep track of whether we entered non-native fullscreen in case
|
||||||
|
// a user goes to fullscreen, changes the config to disable non-native fullscreen
|
||||||
|
// and then wants to toggle it off
|
||||||
|
var isInNonNativeFullscreen: Bool = false
|
||||||
|
|
||||||
|
func toggleFullscreen(window: NSWindow, nonNativeFullscreen: Bool) {
|
||||||
|
if isInFullscreen {
|
||||||
|
if nonNativeFullscreen || isInNonNativeFullscreen {
|
||||||
|
leaveFullscreen(window: window)
|
||||||
|
isInNonNativeFullscreen = false
|
||||||
|
} else {
|
||||||
|
window.toggleFullScreen(nil)
|
||||||
|
}
|
||||||
|
isInFullscreen = false
|
||||||
|
} else {
|
||||||
|
if nonNativeFullscreen {
|
||||||
|
enterFullscreen(window: window)
|
||||||
|
isInNonNativeFullscreen = true
|
||||||
|
} else {
|
||||||
|
window.toggleFullScreen(nil)
|
||||||
|
}
|
||||||
|
isInFullscreen = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func enterFullscreen(window: NSWindow) {
|
||||||
|
guard let screen = window.screen else { return }
|
||||||
|
guard let contentView = window.contentView else { return }
|
||||||
|
|
||||||
|
previousTabGroup = window.tabGroup
|
||||||
|
previousTabGroupIndex = window.tabGroup?.windows.firstIndex(of: window)
|
||||||
|
|
||||||
|
// Save previous style mask
|
||||||
|
previousStyleMask = window.styleMask
|
||||||
|
// Save previous contentViewFrame and screen
|
||||||
|
previousContentFrame = window.convertToScreen(contentView.frame)
|
||||||
|
|
||||||
|
// Change presentation style to hide menu bar and dock
|
||||||
|
NSApp.presentationOptions = [.autoHideMenuBar, .autoHideDock]
|
||||||
|
// Turn it into borderless window
|
||||||
|
window.styleMask.insert(.borderless)
|
||||||
|
// This is important: it gives us the full screen, including the
|
||||||
|
// notch area on MacBooks.
|
||||||
|
window.styleMask.remove(.titled)
|
||||||
|
|
||||||
|
// Set frame to screen size
|
||||||
|
window.setFrame(screen.frame, display: true)
|
||||||
|
|
||||||
|
// Focus window
|
||||||
|
window.makeKeyAndOrderFront(nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
func leaveFullscreen(window: NSWindow) {
|
||||||
|
guard let previousFrame = previousContentFrame else { return }
|
||||||
|
guard let previousStyleMask = previousStyleMask else { return }
|
||||||
|
|
||||||
|
// Restore previous style
|
||||||
|
window.styleMask = previousStyleMask
|
||||||
|
|
||||||
|
// Restore previous presentation options
|
||||||
|
NSApp.presentationOptions = []
|
||||||
|
|
||||||
|
// Restore frame
|
||||||
|
window.setFrame(window.frameRect(forContentRect: previousFrame), display: true)
|
||||||
|
|
||||||
|
// If the window was previously in a tab group that isn't empty now, we re-add it
|
||||||
|
if let group = previousTabGroup, let tabIndex = previousTabGroupIndex, !group.windows.isEmpty {
|
||||||
|
var tabWindow: NSWindow?
|
||||||
|
var order: NSWindow.OrderingMode = .below
|
||||||
|
|
||||||
|
// Index of the window before `window`
|
||||||
|
let tabIndexBefore = tabIndex-1
|
||||||
|
if tabIndexBefore < 0 {
|
||||||
|
// If we were the first tab, we add the window *before* (.below) the first one.
|
||||||
|
tabWindow = group.windows.first
|
||||||
|
} else if tabIndexBefore < group.windows.count {
|
||||||
|
// If we weren't the first tab in the group, we add our window after
|
||||||
|
// the tab that was before it.
|
||||||
|
tabWindow = group.windows[tabIndexBefore]
|
||||||
|
order = .above
|
||||||
|
} else {
|
||||||
|
// If index is after group, add it after last window
|
||||||
|
tabWindow = group.windows.last
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add the window
|
||||||
|
tabWindow?.addTabbedWindow(window, ordered: order)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Focus window
|
||||||
|
window.makeKeyAndOrderFront(nil)
|
||||||
|
}
|
||||||
|
}
|
180
macos/Sources/MainMenu.xib
Normal file
180
macos/Sources/MainMenu.xib
Normal file
@ -0,0 +1,180 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<document type="com.apple.InterfaceBuilder3.Cocoa.XIB" version="3.0" toolsVersion="21701" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES" customObjectInstantitationMethod="direct">
|
||||||
|
<dependencies>
|
||||||
|
<deployment identifier="macosx"/>
|
||||||
|
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="21701"/>
|
||||||
|
</dependencies>
|
||||||
|
<objects>
|
||||||
|
<customObject id="-2" userLabel="File's Owner" customClass="NSApplication">
|
||||||
|
<connections>
|
||||||
|
<outlet property="delegate" destination="bbz-4X-AYv" id="4pZ-gB-Uf0"/>
|
||||||
|
</connections>
|
||||||
|
</customObject>
|
||||||
|
<customObject id="-1" userLabel="First Responder" customClass="FirstResponder"/>
|
||||||
|
<customObject id="-3" userLabel="Application" customClass="NSObject"/>
|
||||||
|
<customObject id="bbz-4X-AYv" userLabel="AppDelegate" customClass="AppDelegate" customModule="Ghostty" customModuleProvider="target"/>
|
||||||
|
<customObject id="YLy-65-1bz" customClass="NSFontManager"/>
|
||||||
|
<menu title="Main Menu" systemMenu="main" id="AYu-sK-qS6">
|
||||||
|
<items>
|
||||||
|
<menuItem title="Ghostty" id="1Xt-HY-uBw">
|
||||||
|
<modifierMask key="keyEquivalentModifierMask"/>
|
||||||
|
<menu key="submenu" title="Ghostty" systemMenu="apple" id="uQy-DD-JDr">
|
||||||
|
<items>
|
||||||
|
<menuItem title="About Ghostty" id="5kV-Vb-QxS">
|
||||||
|
<modifierMask key="keyEquivalentModifierMask"/>
|
||||||
|
<connections>
|
||||||
|
<action selector="orderFrontStandardAboutPanel:" target="-1" id="Exp-CZ-Vem"/>
|
||||||
|
</connections>
|
||||||
|
</menuItem>
|
||||||
|
<menuItem isSeparatorItem="YES" id="VOq-y0-SEH"/>
|
||||||
|
<menuItem title="Preferences…" keyEquivalent="," id="BOF-NM-1cW"/>
|
||||||
|
<menuItem isSeparatorItem="YES" id="4je-JR-u6R"/>
|
||||||
|
<menuItem title="Hide Ghostty" keyEquivalent="h" id="Olw-nP-bQN">
|
||||||
|
<connections>
|
||||||
|
<action selector="hide:" target="-1" id="PnN-Uc-m68"/>
|
||||||
|
</connections>
|
||||||
|
</menuItem>
|
||||||
|
<menuItem title="Hide Others" keyEquivalent="h" id="Vdr-fp-XzO">
|
||||||
|
<modifierMask key="keyEquivalentModifierMask" option="YES" command="YES"/>
|
||||||
|
<connections>
|
||||||
|
<action selector="hideOtherApplications:" target="-1" id="VT4-aY-XCT"/>
|
||||||
|
</connections>
|
||||||
|
</menuItem>
|
||||||
|
<menuItem title="Show All" id="Kd2-mp-pUS">
|
||||||
|
<modifierMask key="keyEquivalentModifierMask"/>
|
||||||
|
<connections>
|
||||||
|
<action selector="unhideAllApplications:" target="-1" id="Dhg-Le-xox"/>
|
||||||
|
</connections>
|
||||||
|
</menuItem>
|
||||||
|
<menuItem isSeparatorItem="YES" id="kCx-OE-vgT"/>
|
||||||
|
<menuItem title="Quit NewApplication" keyEquivalent="q" id="4sb-4s-VLi">
|
||||||
|
<connections>
|
||||||
|
<action selector="terminate:" target="-1" id="Te7-pn-YzF"/>
|
||||||
|
</connections>
|
||||||
|
</menuItem>
|
||||||
|
</items>
|
||||||
|
</menu>
|
||||||
|
</menuItem>
|
||||||
|
<menuItem title="File" id="dMs-cI-mzQ">
|
||||||
|
<modifierMask key="keyEquivalentModifierMask"/>
|
||||||
|
<menu key="submenu" title="File" id="bib-Uj-vzu">
|
||||||
|
<items>
|
||||||
|
<menuItem title="New Window" keyEquivalent="n" id="Was-JA-tGl">
|
||||||
|
<connections>
|
||||||
|
<action selector="newWindow:" target="bbz-4X-AYv" id="NnC-l5-DUY"/>
|
||||||
|
</connections>
|
||||||
|
</menuItem>
|
||||||
|
<menuItem title="New Tab" keyEquivalent="t" id="uTG-Vz-hJU">
|
||||||
|
<connections>
|
||||||
|
<action selector="newTab:" target="bbz-4X-AYv" id="cxO-CS-TJq"/>
|
||||||
|
</connections>
|
||||||
|
</menuItem>
|
||||||
|
<menuItem isSeparatorItem="YES" id="m54-Is-iLE"/>
|
||||||
|
<menuItem title="Split Horizontally" keyEquivalent="d" id="VUR-Ld-nLx">
|
||||||
|
<connections>
|
||||||
|
<action selector="splitHorizontally:" target="bbz-4X-AYv" id="QT1-Yt-gYJ"/>
|
||||||
|
</connections>
|
||||||
|
</menuItem>
|
||||||
|
<menuItem title="Split Vertically" keyEquivalent="D" id="UDZ-4y-6xL">
|
||||||
|
<connections>
|
||||||
|
<action selector="splitVertically:" target="bbz-4X-AYv" id="ZZF-3f-OwW"/>
|
||||||
|
</connections>
|
||||||
|
</menuItem>
|
||||||
|
<menuItem isSeparatorItem="YES" id="sjq-M1-UGS"/>
|
||||||
|
<menuItem title="Close" keyEquivalent="w" id="DVo-aG-piG">
|
||||||
|
<connections>
|
||||||
|
<action selector="close:" target="bbz-4X-AYv" id="Szc-Fu-9yk"/>
|
||||||
|
</connections>
|
||||||
|
</menuItem>
|
||||||
|
<menuItem title="Close Window" keyEquivalent="W" id="W5w-UZ-crk">
|
||||||
|
<connections>
|
||||||
|
<action selector="closeWindow:" target="bbz-4X-AYv" id="j4w-Nd-9bO"/>
|
||||||
|
</connections>
|
||||||
|
</menuItem>
|
||||||
|
</items>
|
||||||
|
</menu>
|
||||||
|
</menuItem>
|
||||||
|
<menuItem title="Window" id="aUF-d1-5bR">
|
||||||
|
<modifierMask key="keyEquivalentModifierMask"/>
|
||||||
|
<menu key="submenu" title="Window" systemMenu="window" id="Td7-aD-5lo">
|
||||||
|
<items>
|
||||||
|
<menuItem title="Minimize" keyEquivalent="m" id="OY7-WF-poV">
|
||||||
|
<connections>
|
||||||
|
<action selector="performMiniaturize:" target="-1" id="VwT-WD-YPe"/>
|
||||||
|
</connections>
|
||||||
|
</menuItem>
|
||||||
|
<menuItem title="Zoom" id="R4o-n2-Eq4">
|
||||||
|
<modifierMask key="keyEquivalentModifierMask"/>
|
||||||
|
<connections>
|
||||||
|
<action selector="performZoom:" target="-1" id="DIl-cC-cCs"/>
|
||||||
|
</connections>
|
||||||
|
</menuItem>
|
||||||
|
<menuItem isSeparatorItem="YES" id="eu3-7i-yIM"/>
|
||||||
|
<menuItem title="Bring All to Front" id="LE2-aR-0XJ">
|
||||||
|
<modifierMask key="keyEquivalentModifierMask"/>
|
||||||
|
<connections>
|
||||||
|
<action selector="arrangeInFront:" target="-1" id="DRN-fu-gQh"/>
|
||||||
|
</connections>
|
||||||
|
</menuItem>
|
||||||
|
<menuItem isSeparatorItem="YES" id="rlu-tP-x0P"/>
|
||||||
|
<menuItem title="Select Previous Split" keyEquivalent="[" id="Lic-px-1wg">
|
||||||
|
<connections>
|
||||||
|
<action selector="splitMoveFocusPrevious:" target="bbz-4X-AYv" id="mOs-gG-dAC"/>
|
||||||
|
</connections>
|
||||||
|
</menuItem>
|
||||||
|
<menuItem title="Select Next Split" keyEquivalent="]" id="bD7-ei-wKU">
|
||||||
|
<connections>
|
||||||
|
<action selector="splitMoveFocusNext:" target="bbz-4X-AYv" id="rU6-Vw-DoW"/>
|
||||||
|
</connections>
|
||||||
|
</menuItem>
|
||||||
|
<menuItem title="Select Split" id="dos-9S-LXC">
|
||||||
|
<modifierMask key="keyEquivalentModifierMask"/>
|
||||||
|
<menu key="submenu" title="Select Split" id="8tg-60-ZSU">
|
||||||
|
<items>
|
||||||
|
<menuItem title="Select Split Above" keyEquivalent="" id="0yU-hC-8xF">
|
||||||
|
<modifierMask key="keyEquivalentModifierMask" option="YES" command="YES"/>
|
||||||
|
<connections>
|
||||||
|
<action selector="splitMoveFocusAbove:" target="bbz-4X-AYv" id="HDw-f2-RJY"/>
|
||||||
|
</connections>
|
||||||
|
</menuItem>
|
||||||
|
<menuItem title="Select Split Below" keyEquivalent="" id="QDz-d9-CBr">
|
||||||
|
<modifierMask key="keyEquivalentModifierMask" option="YES" command="YES"/>
|
||||||
|
<connections>
|
||||||
|
<action selector="splitMoveFocusBelow:" target="bbz-4X-AYv" id="fmW-hZ-uOA"/>
|
||||||
|
</connections>
|
||||||
|
</menuItem>
|
||||||
|
<menuItem title="Select Split Left" keyEquivalent="" id="cTK-oy-KuV">
|
||||||
|
<modifierMask key="keyEquivalentModifierMask" option="YES" command="YES"/>
|
||||||
|
<connections>
|
||||||
|
<action selector="splitMoveFocusLeft:" target="bbz-4X-AYv" id="N1i-a2-7N5"/>
|
||||||
|
</connections>
|
||||||
|
</menuItem>
|
||||||
|
<menuItem title="Select Split Right" keyEquivalent="" id="upj-mc-L7X">
|
||||||
|
<modifierMask key="keyEquivalentModifierMask" option="YES" command="YES"/>
|
||||||
|
<connections>
|
||||||
|
<action selector="splitMoveFocusRight:" target="bbz-4X-AYv" id="Pgi-df-84r"/>
|
||||||
|
</connections>
|
||||||
|
</menuItem>
|
||||||
|
</items>
|
||||||
|
</menu>
|
||||||
|
</menuItem>
|
||||||
|
</items>
|
||||||
|
</menu>
|
||||||
|
</menuItem>
|
||||||
|
<menuItem title="Help" id="wpr-3q-Mcd">
|
||||||
|
<modifierMask key="keyEquivalentModifierMask"/>
|
||||||
|
<menu key="submenu" title="Help" systemMenu="help" id="F2S-fz-NVQ">
|
||||||
|
<items>
|
||||||
|
<menuItem title="Ghostty Help" keyEquivalent="?" id="FKE-Sm-Kum">
|
||||||
|
<connections>
|
||||||
|
<action selector="showHelp:" target="-1" id="y7X-2Q-9no"/>
|
||||||
|
</connections>
|
||||||
|
</menuItem>
|
||||||
|
</items>
|
||||||
|
</menu>
|
||||||
|
</menuItem>
|
||||||
|
</items>
|
||||||
|
<point key="canvasLocation" x="139" y="154"/>
|
||||||
|
</menu>
|
||||||
|
</objects>
|
||||||
|
</document>
|
@ -146,6 +146,7 @@ const DerivedConfig = struct {
|
|||||||
clipboard_trim_trailing_spaces: bool,
|
clipboard_trim_trailing_spaces: bool,
|
||||||
confirm_close_surface: bool,
|
confirm_close_surface: bool,
|
||||||
mouse_interval: u64,
|
mouse_interval: u64,
|
||||||
|
macos_non_native_fullscreen: bool,
|
||||||
|
|
||||||
pub fn init(alloc_gpa: Allocator, config: *const configpkg.Config) !DerivedConfig {
|
pub fn init(alloc_gpa: Allocator, config: *const configpkg.Config) !DerivedConfig {
|
||||||
var arena = ArenaAllocator.init(alloc_gpa);
|
var arena = ArenaAllocator.init(alloc_gpa);
|
||||||
@ -160,6 +161,7 @@ const DerivedConfig = struct {
|
|||||||
.clipboard_trim_trailing_spaces = config.@"clipboard-trim-trailing-spaces",
|
.clipboard_trim_trailing_spaces = config.@"clipboard-trim-trailing-spaces",
|
||||||
.confirm_close_surface = config.@"confirm-close-surface",
|
.confirm_close_surface = config.@"confirm-close-surface",
|
||||||
.mouse_interval = config.@"click-repeat-interval" * 1_000_000, // 500ms
|
.mouse_interval = config.@"click-repeat-interval" * 1_000_000, // 500ms
|
||||||
|
.macos_non_native_fullscreen = config.@"macos-non-native-fullscreen",
|
||||||
|
|
||||||
// Assignments happen sequentially so we have to do this last
|
// Assignments happen sequentially so we have to do this last
|
||||||
// so that the memory is captured from allocs above.
|
// so that the memory is captured from allocs above.
|
||||||
@ -1213,7 +1215,7 @@ pub fn keyCallback(
|
|||||||
|
|
||||||
.toggle_fullscreen => {
|
.toggle_fullscreen => {
|
||||||
if (@hasDecl(apprt.Surface, "toggleFullscreen")) {
|
if (@hasDecl(apprt.Surface, "toggleFullscreen")) {
|
||||||
self.rt_surface.toggleFullscreen();
|
self.rt_surface.toggleFullscreen(self.config.macos_non_native_fullscreen);
|
||||||
} else log.warn("runtime doesn't implement toggleFullscreen", .{});
|
} else log.warn("runtime doesn't implement toggleFullscreen", .{});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -66,7 +66,7 @@ pub const App = struct {
|
|||||||
goto_tab: ?*const fn (SurfaceUD, usize) callconv(.C) void = null,
|
goto_tab: ?*const fn (SurfaceUD, usize) callconv(.C) void = null,
|
||||||
|
|
||||||
/// Toggle fullscreen for current window.
|
/// Toggle fullscreen for current window.
|
||||||
toggle_fullscreen: ?*const fn (SurfaceUD) callconv(.C) void = null,
|
toggle_fullscreen: ?*const fn (SurfaceUD, bool) callconv(.C) void = null,
|
||||||
};
|
};
|
||||||
|
|
||||||
core_app: *CoreApp,
|
core_app: *CoreApp,
|
||||||
@ -374,13 +374,13 @@ pub const Surface = struct {
|
|||||||
func(self.opts.userdata, n);
|
func(self.opts.userdata, n);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn toggleFullscreen(self: *Surface) void {
|
pub fn toggleFullscreen(self: *Surface, nonNativeFullscreen: bool) void {
|
||||||
const func = self.app.opts.toggle_fullscreen orelse {
|
const func = self.app.opts.toggle_fullscreen orelse {
|
||||||
log.info("runtime embedder does not toggle_fullscreen", .{});
|
log.info("runtime embedder does not toggle_fullscreen", .{});
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
|
|
||||||
func(self.opts.userdata);
|
func(self.opts.userdata, nonNativeFullscreen);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The cursor position from the host directly is in screen coordinates but
|
/// The cursor position from the host directly is in screen coordinates but
|
||||||
|
@ -483,7 +483,7 @@ const Window = struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Toggle fullscreen for this window.
|
/// Toggle fullscreen for this window.
|
||||||
fn toggleFullscreen(self: *Window) void {
|
fn toggleFullscreen(self: *Window, _: bool) void {
|
||||||
const is_fullscreen = c.gtk_window_is_fullscreen(self.window);
|
const is_fullscreen = c.gtk_window_is_fullscreen(self.window);
|
||||||
if (is_fullscreen == 0) {
|
if (is_fullscreen == 0) {
|
||||||
c.gtk_window_fullscreen(self.window);
|
c.gtk_window_fullscreen(self.window);
|
||||||
|
@ -221,6 +221,12 @@ pub const Config = struct {
|
|||||||
/// The default value is "detect".
|
/// The default value is "detect".
|
||||||
@"shell-integration": ShellIntegration = .detect,
|
@"shell-integration": ShellIntegration = .detect,
|
||||||
|
|
||||||
|
/// If true, fullscreen mode on macOS will not use the native fullscreen,
|
||||||
|
/// but make the window fullscreen without animations and using a new space.
|
||||||
|
/// That's faster than the native fullscreen mode since it doesn't use
|
||||||
|
/// animations.
|
||||||
|
@"macos-non-native-fullscreen": bool = false,
|
||||||
|
|
||||||
/// This is set by the CLI parser for deinit.
|
/// This is set by the CLI parser for deinit.
|
||||||
_arena: ?ArenaAllocator = null,
|
_arena: ?ArenaAllocator = null,
|
||||||
|
|
||||||
|
Reference in New Issue
Block a user