7 Commits

25 changed files with 759 additions and 34 deletions

3
.gitignore vendored
View File

@@ -81,3 +81,6 @@ build/
# Mac... files # Mac... files
**/.DS_Store **/.DS_Store
# Releases
releases/

View File

@@ -15,6 +15,7 @@
187F4B521BFC3BD29ADA79E3 /* HotkeyManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = EEB7FE2BDE5C25B7E599F340 /* HotkeyManager.swift */; }; 187F4B521BFC3BD29ADA79E3 /* HotkeyManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = EEB7FE2BDE5C25B7E599F340 /* HotkeyManager.swift */; };
1AB4A0F1BE668D3130EFBA93 /* TerminalTheme.swift in Sources */ = {isa = PBXBuildFile; fileRef = CFE9E5BADB0C427903A0D874 /* TerminalTheme.swift */; }; 1AB4A0F1BE668D3130EFBA93 /* TerminalTheme.swift in Sources */ = {isa = PBXBuildFile; fileRef = CFE9E5BADB0C427903A0D874 /* TerminalTheme.swift */; };
2089566A2BBAA65EA82119B3 /* NotchOrchestratorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF6D136A0B3FC79DDE12A826 /* NotchOrchestratorTests.swift */; }; 2089566A2BBAA65EA82119B3 /* NotchOrchestratorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF6D136A0B3FC79DDE12A826 /* NotchOrchestratorTests.swift */; };
21373D6E9C2F34FD63CEC6A5 /* TerminalScrollCoordinatorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 40D9E323185F6D722B360C3C /* TerminalScrollCoordinatorTests.swift */; };
2375B9DA559A0777FE558A8B /* WorkspaceStoreTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4FA62DFE9AD003F4C5B55F14 /* WorkspaceStoreTests.swift */; }; 2375B9DA559A0777FE558A8B /* WorkspaceStoreTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4FA62DFE9AD003F4C5B55F14 /* WorkspaceStoreTests.swift */; };
23E2DDCF36D0DAB2EA72C39C /* NotchState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B352301BC9CAD7C9D8B7AA9 /* NotchState.swift */; }; 23E2DDCF36D0DAB2EA72C39C /* NotchState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B352301BC9CAD7C9D8B7AA9 /* NotchState.swift */; };
26AE379040149CBE05B314BB /* WorkspacesSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F70A31EFACF23DD9262A040E /* WorkspacesSettingsView.swift */; }; 26AE379040149CBE05B314BB /* WorkspacesSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F70A31EFACF23DD9262A040E /* WorkspacesSettingsView.swift */; };
@@ -22,6 +23,8 @@
2DF22798D3A7514E2A9183FC /* WorkspaceSummary.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B2BCA543CE54DAB1DB80E43 /* WorkspaceSummary.swift */; }; 2DF22798D3A7514E2A9183FC /* WorkspaceSummary.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B2BCA543CE54DAB1DB80E43 /* WorkspaceSummary.swift */; };
34AF69BF7AF8DC78ADE3774A /* GeneralSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27E7EEEDFBD8D9CEB8FD86A5 /* GeneralSettingsView.swift */; }; 34AF69BF7AF8DC78ADE3774A /* GeneralSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27E7EEEDFBD8D9CEB8FD86A5 /* GeneralSettingsView.swift */; };
3B69CB3CDEC2E5F2DCE600F9 /* NotchSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 297BA3E5B837FBDDEED9DE66 /* NotchSettings.swift */; }; 3B69CB3CDEC2E5F2DCE600F9 /* NotchSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 297BA3E5B837FBDDEED9DE66 /* NotchSettings.swift */; };
40183737D8C237022D0882FD /* TerminalScrollbackEstimatorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D77DC2AED643512C786AD70A /* TerminalScrollbackEstimatorTests.swift */; };
4BA9D6274CFD6A87DC397B8E /* MouseAwareTerminalView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 982BA8965FE92A59D66F378B /* MouseAwareTerminalView.swift */; };
4CFA0C79095ACE0A327A2469 /* WorkspaceRegistry.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E97758F68FACCFFACA895B7 /* WorkspaceRegistry.swift */; }; 4CFA0C79095ACE0A327A2469 /* WorkspaceRegistry.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E97758F68FACCFFACA895B7 /* WorkspaceRegistry.swift */; };
4D335F67B71F7DD977B6AEF9 /* AppSettingsStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E6C98C406899F4B242075AF /* AppSettingsStore.swift */; }; 4D335F67B71F7DD977B6AEF9 /* AppSettingsStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E6C98C406899F4B242075AF /* AppSettingsStore.swift */; };
4E0AD14B7427532271E485AA /* NotchWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = EFAC70814C72BAF76D90B9DF /* NotchWindow.swift */; }; 4E0AD14B7427532271E485AA /* NotchWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = EFAC70814C72BAF76D90B9DF /* NotchWindow.swift */; };
@@ -54,8 +57,10 @@
D088BC850F37E717844761C6 /* CommandNotchApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5159CB9DBE2BAA0D2E201C39 /* CommandNotchApp.swift */; }; D088BC850F37E717844761C6 /* CommandNotchApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5159CB9DBE2BAA0D2E201C39 /* CommandNotchApp.swift */; };
D2468B19D6F0A2C1DFDFE2B7 /* ScreenContextTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A770A63582CF9834F4E7F058 /* ScreenContextTests.swift */; }; D2468B19D6F0A2C1DFDFE2B7 /* ScreenContextTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A770A63582CF9834F4E7F058 /* ScreenContextTests.swift */; };
D4CEB4B895F75D91FA892A06 /* PopoutWindowController.swift in Sources */ = {isa = PBXBuildFile; fileRef = CB593A2546BF2C0BE8E40387 /* PopoutWindowController.swift */; }; D4CEB4B895F75D91FA892A06 /* PopoutWindowController.swift in Sources */ = {isa = PBXBuildFile; fileRef = CB593A2546BF2C0BE8E40387 /* PopoutWindowController.swift */; };
D6BBC4DE23DEA39D9713469A /* TerminalScrollWheelRouterTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7315EA485A52A8DC42DF925E /* TerminalScrollWheelRouterTests.swift */; };
DAD3AB4A0DAADA32C02D959E /* SettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3125FD3DC55420122CF85D80 /* SettingsView.swift */; }; DAD3AB4A0DAADA32C02D959E /* SettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3125FD3DC55420122CF85D80 /* SettingsView.swift */; };
DCFD5B03E64A46783F46726B /* TerminalCommandArrowBehavior.swift in Sources */ = {isa = PBXBuildFile; fileRef = F22AA47452CF798A977A6F47 /* TerminalCommandArrowBehavior.swift */; }; DCFD5B03E64A46783F46726B /* TerminalCommandArrowBehavior.swift in Sources */ = {isa = PBXBuildFile; fileRef = F22AA47452CF798A977A6F47 /* TerminalCommandArrowBehavior.swift */; };
DD116B0FCC341D66F1534EC4 /* TerminalScrollbackEstimator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 165DCCD7BB164A6470D49BBF /* TerminalScrollbackEstimator.swift */; };
E63FD5862C0EC54E284F6A0F /* ScreenRegistry.swift in Sources */ = {isa = PBXBuildFile; fileRef = AAF23753B5A0CAF04D7566A3 /* ScreenRegistry.swift */; }; E63FD5862C0EC54E284F6A0F /* ScreenRegistry.swift in Sources */ = {isa = PBXBuildFile; fileRef = AAF23753B5A0CAF04D7566A3 /* ScreenRegistry.swift */; };
E792411FA82E79E810F4B4C3 /* HotkeyRecorderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2B81432CECBDB61D21EE4DC3 /* HotkeyRecorderView.swift */; }; E792411FA82E79E810F4B4C3 /* HotkeyRecorderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2B81432CECBDB61D21EE4DC3 /* HotkeyRecorderView.swift */; };
EE72479BA5A25FF31BACCC50 /* NotchOrchestrator.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC7912AF01E1600B8619AF31 /* NotchOrchestrator.swift */; }; EE72479BA5A25FF31BACCC50 /* NotchOrchestrator.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC7912AF01E1600B8619AF31 /* NotchOrchestrator.swift */; };
@@ -82,6 +87,7 @@
/* Begin PBXFileReference section */ /* Begin PBXFileReference section */
0E97758F68FACCFFACA895B7 /* WorkspaceRegistry.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WorkspaceRegistry.swift; sourceTree = "<group>"; }; 0E97758F68FACCFFACA895B7 /* WorkspaceRegistry.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WorkspaceRegistry.swift; sourceTree = "<group>"; };
165DCCD7BB164A6470D49BBF /* TerminalScrollbackEstimator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TerminalScrollbackEstimator.swift; sourceTree = "<group>"; };
27E7EEEDFBD8D9CEB8FD86A5 /* GeneralSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GeneralSettingsView.swift; sourceTree = "<group>"; }; 27E7EEEDFBD8D9CEB8FD86A5 /* GeneralSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GeneralSettingsView.swift; sourceTree = "<group>"; };
297BA3E5B837FBDDEED9DE66 /* NotchSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotchSettings.swift; sourceTree = "<group>"; }; 297BA3E5B837FBDDEED9DE66 /* NotchSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotchSettings.swift; sourceTree = "<group>"; };
2B81432CECBDB61D21EE4DC3 /* HotkeyRecorderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HotkeyRecorderView.swift; sourceTree = "<group>"; }; 2B81432CECBDB61D21EE4DC3 /* HotkeyRecorderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HotkeyRecorderView.swift; sourceTree = "<group>"; };
@@ -91,6 +97,7 @@
3CB1DFD6FCDF64B4DF24230A /* WorkspaceController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WorkspaceController.swift; sourceTree = "<group>"; }; 3CB1DFD6FCDF64B4DF24230A /* WorkspaceController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WorkspaceController.swift; sourceTree = "<group>"; };
3F57837A7115DEEE11E14B40 /* NotchShape.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotchShape.swift; sourceTree = "<group>"; }; 3F57837A7115DEEE11E14B40 /* NotchShape.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotchShape.swift; sourceTree = "<group>"; };
3F5FF5623898FA150C3B70D4 /* AppSettingsControllerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppSettingsControllerTests.swift; sourceTree = "<group>"; }; 3F5FF5623898FA150C3B70D4 /* AppSettingsControllerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppSettingsControllerTests.swift; sourceTree = "<group>"; };
40D9E323185F6D722B360C3C /* TerminalScrollCoordinatorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TerminalScrollCoordinatorTests.swift; sourceTree = "<group>"; };
48198AFE5473B0F7AECAB3FB /* AboutSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AboutSettingsView.swift; sourceTree = "<group>"; }; 48198AFE5473B0F7AECAB3FB /* AboutSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AboutSettingsView.swift; sourceTree = "<group>"; };
496267F03E261FEC9EBD5A9D /* CommandNotchUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = CommandNotchUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 496267F03E261FEC9EBD5A9D /* CommandNotchUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = CommandNotchUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
49E1791BB45E1505500ACC67 /* TerminalSession.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TerminalSession.swift; sourceTree = "<group>"; }; 49E1791BB45E1505500ACC67 /* TerminalSession.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TerminalSession.swift; sourceTree = "<group>"; };
@@ -105,6 +112,7 @@
726B935606FD961FD7E8C2BE /* CommandNotchUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CommandNotchUITests.swift; sourceTree = "<group>"; }; 726B935606FD961FD7E8C2BE /* CommandNotchUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CommandNotchUITests.swift; sourceTree = "<group>"; };
728B3125F7F7FDB7313D2DC6 /* SettingsWindowController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsWindowController.swift; sourceTree = "<group>"; }; 728B3125F7F7FDB7313D2DC6 /* SettingsWindowController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsWindowController.swift; sourceTree = "<group>"; };
72A1D3D12BAC593838B3125C /* TabBar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TabBar.swift; sourceTree = "<group>"; }; 72A1D3D12BAC593838B3125C /* TabBar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TabBar.swift; sourceTree = "<group>"; };
7315EA485A52A8DC42DF925E /* TerminalScrollWheelRouterTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TerminalScrollWheelRouterTests.swift; sourceTree = "<group>"; };
74463E4EAB78F56345360CD5 /* AppSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppSettings.swift; sourceTree = "<group>"; }; 74463E4EAB78F56345360CD5 /* AppSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppSettings.swift; sourceTree = "<group>"; };
7B2BCA543CE54DAB1DB80E43 /* WorkspaceSummary.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WorkspaceSummary.swift; sourceTree = "<group>"; }; 7B2BCA543CE54DAB1DB80E43 /* WorkspaceSummary.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WorkspaceSummary.swift; sourceTree = "<group>"; };
8210D7783A614FD7190F5DDD /* LaunchAtLoginHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LaunchAtLoginHelper.swift; sourceTree = "<group>"; }; 8210D7783A614FD7190F5DDD /* LaunchAtLoginHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LaunchAtLoginHelper.swift; sourceTree = "<group>"; };
@@ -113,6 +121,7 @@
8BB1C403BC2157756F572ACF /* HotkeyBinding.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HotkeyBinding.swift; sourceTree = "<group>"; }; 8BB1C403BC2157756F572ACF /* HotkeyBinding.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HotkeyBinding.swift; sourceTree = "<group>"; };
8CB8AF76C0F728897A26D7EF /* ScreenManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScreenManager.swift; sourceTree = "<group>"; }; 8CB8AF76C0F728897A26D7EF /* ScreenManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScreenManager.swift; sourceTree = "<group>"; };
900F0476BE9E3600FBD371BB /* SettingsBindings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsBindings.swift; sourceTree = "<group>"; }; 900F0476BE9E3600FBD371BB /* SettingsBindings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsBindings.swift; sourceTree = "<group>"; };
982BA8965FE92A59D66F378B /* MouseAwareTerminalView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MouseAwareTerminalView.swift; sourceTree = "<group>"; };
9C55F29B779DA0E5C5FC8627 /* SwiftTermView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwiftTermView.swift; sourceTree = "<group>"; }; 9C55F29B779DA0E5C5FC8627 /* SwiftTermView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwiftTermView.swift; sourceTree = "<group>"; };
9E6C98C406899F4B242075AF /* AppSettingsStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppSettingsStore.swift; sourceTree = "<group>"; }; 9E6C98C406899F4B242075AF /* AppSettingsStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppSettingsStore.swift; sourceTree = "<group>"; };
A64A11F27E65B342B991629A /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = "<group>"; }; A64A11F27E65B342B991629A /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = "<group>"; };
@@ -124,6 +133,7 @@
CFE9E5BADB0C427903A0D874 /* TerminalTheme.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TerminalTheme.swift; sourceTree = "<group>"; }; CFE9E5BADB0C427903A0D874 /* TerminalTheme.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TerminalTheme.swift; sourceTree = "<group>"; };
D03D042117E59DCA9D553844 /* HotkeySettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HotkeySettingsView.swift; sourceTree = "<group>"; }; D03D042117E59DCA9D553844 /* HotkeySettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HotkeySettingsView.swift; sourceTree = "<group>"; };
D288132700770C4A625A15F6 /* WindowFrameCalculatorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WindowFrameCalculatorTests.swift; sourceTree = "<group>"; }; D288132700770C4A625A15F6 /* WindowFrameCalculatorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WindowFrameCalculatorTests.swift; sourceTree = "<group>"; };
D77DC2AED643512C786AD70A /* TerminalScrollbackEstimatorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TerminalScrollbackEstimatorTests.swift; sourceTree = "<group>"; };
DC7912AF01E1600B8619AF31 /* NotchOrchestrator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotchOrchestrator.swift; sourceTree = "<group>"; }; DC7912AF01E1600B8619AF31 /* NotchOrchestrator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotchOrchestrator.swift; sourceTree = "<group>"; };
DF0FFBC96F2446687D6474F4 /* WorkspaceSwitcherView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WorkspaceSwitcherView.swift; sourceTree = "<group>"; }; DF0FFBC96F2446687D6474F4 /* WorkspaceSwitcherView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WorkspaceSwitcherView.swift; sourceTree = "<group>"; };
E37A6DCD9C5DE1FE11C4C1CD /* TerminalSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TerminalSettingsView.swift; sourceTree = "<group>"; }; E37A6DCD9C5DE1FE11C4C1CD /* TerminalSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TerminalSettingsView.swift; sourceTree = "<group>"; };
@@ -165,6 +175,7 @@
7181BB1F3926B457445105E5 /* ScreenContext.swift */, 7181BB1F3926B457445105E5 /* ScreenContext.swift */,
AAF23753B5A0CAF04D7566A3 /* ScreenRegistry.swift */, AAF23753B5A0CAF04D7566A3 /* ScreenRegistry.swift */,
567E85A2ED628460CEC760DB /* TerminalManager.swift */, 567E85A2ED628460CEC760DB /* TerminalManager.swift */,
165DCCD7BB164A6470D49BBF /* TerminalScrollbackEstimator.swift */,
49E1791BB45E1505500ACC67 /* TerminalSession.swift */, 49E1791BB45E1505500ACC67 /* TerminalSession.swift */,
CFE9E5BADB0C427903A0D874 /* TerminalTheme.swift */, CFE9E5BADB0C427903A0D874 /* TerminalTheme.swift */,
3CB1DFD6FCDF64B4DF24230A /* WorkspaceController.swift */, 3CB1DFD6FCDF64B4DF24230A /* WorkspaceController.swift */,
@@ -218,6 +229,9 @@
A770A63582CF9834F4E7F058 /* ScreenContextTests.swift */, A770A63582CF9834F4E7F058 /* ScreenContextTests.swift */,
EEC7F7D8D15A1BC4EE43DDDB /* ScreenRegistryTests.swift */, EEC7F7D8D15A1BC4EE43DDDB /* ScreenRegistryTests.swift */,
C0D19729317029008D81F361 /* TerminalCommandArrowBehaviorTests.swift */, C0D19729317029008D81F361 /* TerminalCommandArrowBehaviorTests.swift */,
D77DC2AED643512C786AD70A /* TerminalScrollbackEstimatorTests.swift */,
40D9E323185F6D722B360C3C /* TerminalScrollCoordinatorTests.swift */,
7315EA485A52A8DC42DF925E /* TerminalScrollWheelRouterTests.swift */,
D288132700770C4A625A15F6 /* WindowFrameCalculatorTests.swift */, D288132700770C4A625A15F6 /* WindowFrameCalculatorTests.swift */,
591FCE91AF83A8A8E44E1625 /* WorkspaceRegistryTests.swift */, 591FCE91AF83A8A8E44E1625 /* WorkspaceRegistryTests.swift */,
4FA62DFE9AD003F4C5B55F14 /* WorkspaceStoreTests.swift */, 4FA62DFE9AD003F4C5B55F14 /* WorkspaceStoreTests.swift */,
@@ -248,6 +262,7 @@
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
2B81432CECBDB61D21EE4DC3 /* HotkeyRecorderView.swift */, 2B81432CECBDB61D21EE4DC3 /* HotkeyRecorderView.swift */,
982BA8965FE92A59D66F378B /* MouseAwareTerminalView.swift */,
3F57837A7115DEEE11E14B40 /* NotchShape.swift */, 3F57837A7115DEEE11E14B40 /* NotchShape.swift */,
EFAC70814C72BAF76D90B9DF /* NotchWindow.swift */, EFAC70814C72BAF76D90B9DF /* NotchWindow.swift */,
9C55F29B779DA0E5C5FC8627 /* SwiftTermView.swift */, 9C55F29B779DA0E5C5FC8627 /* SwiftTermView.swift */,
@@ -370,7 +385,6 @@
}; };
}; };
buildConfigurationList = C47A3896770C98F2A3E62B7A /* Build configuration list for PBXProject "CommandNotch" */; buildConfigurationList = C47A3896770C98F2A3E62B7A /* Build configuration list for PBXProject "CommandNotch" */;
compatibilityVersion = "Xcode 14.0";
developmentRegion = en; developmentRegion = en;
hasScannedForEncodings = 0; hasScannedForEncodings = 0;
knownRegions = ( knownRegions = (
@@ -383,6 +397,7 @@
28377BE3F9997892D4929B6E /* XCRemoteSwiftPackageReference "SwiftTerm" */, 28377BE3F9997892D4929B6E /* XCRemoteSwiftPackageReference "SwiftTerm" */,
); );
preferredProjectObjectVersion = 77; preferredProjectObjectVersion = 77;
productRefGroup = B269158E04E8E603B61448F0 /* Products */;
projectDirPath = ""; projectDirPath = "";
projectRoot = ""; projectRoot = "";
targets = ( targets = (
@@ -423,6 +438,9 @@
D2468B19D6F0A2C1DFDFE2B7 /* ScreenContextTests.swift in Sources */, D2468B19D6F0A2C1DFDFE2B7 /* ScreenContextTests.swift in Sources */,
8A1B2A4CD61B0D9A4BAB075B /* ScreenRegistryTests.swift in Sources */, 8A1B2A4CD61B0D9A4BAB075B /* ScreenRegistryTests.swift in Sources */,
CFBBE994BA6BAD4658AAB9CB /* TerminalCommandArrowBehaviorTests.swift in Sources */, CFBBE994BA6BAD4658AAB9CB /* TerminalCommandArrowBehaviorTests.swift in Sources */,
21373D6E9C2F34FD63CEC6A5 /* TerminalScrollCoordinatorTests.swift in Sources */,
D6BBC4DE23DEA39D9713469A /* TerminalScrollWheelRouterTests.swift in Sources */,
40183737D8C237022D0882FD /* TerminalScrollbackEstimatorTests.swift in Sources */,
0F133E8A88D2E313D90C32AD /* WindowFrameCalculatorTests.swift in Sources */, 0F133E8A88D2E313D90C32AD /* WindowFrameCalculatorTests.swift in Sources */,
154F363D434A26105C5999B5 /* WorkspaceRegistryTests.swift in Sources */, 154F363D434A26105C5999B5 /* WorkspaceRegistryTests.swift in Sources */,
2375B9DA559A0777FE558A8B /* WorkspaceStoreTests.swift in Sources */, 2375B9DA559A0777FE558A8B /* WorkspaceStoreTests.swift in Sources */,
@@ -448,6 +466,7 @@
E792411FA82E79E810F4B4C3 /* HotkeyRecorderView.swift in Sources */, E792411FA82E79E810F4B4C3 /* HotkeyRecorderView.swift in Sources */,
6A18F6635B509FF58669F505 /* HotkeySettingsView.swift in Sources */, 6A18F6635B509FF58669F505 /* HotkeySettingsView.swift in Sources */,
12F68EDA880030DAB644FF5F /* LaunchAtLoginHelper.swift in Sources */, 12F68EDA880030DAB644FF5F /* LaunchAtLoginHelper.swift in Sources */,
4BA9D6274CFD6A87DC397B8E /* MouseAwareTerminalView.swift in Sources */,
A74E14B8AD19C53820853D8E /* NSScreen+Extensions.swift in Sources */, A74E14B8AD19C53820853D8E /* NSScreen+Extensions.swift in Sources */,
EE72479BA5A25FF31BACCC50 /* NotchOrchestrator.swift in Sources */, EE72479BA5A25FF31BACCC50 /* NotchOrchestrator.swift in Sources */,
3B69CB3CDEC2E5F2DCE600F9 /* NotchSettings.swift in Sources */, 3B69CB3CDEC2E5F2DCE600F9 /* NotchSettings.swift in Sources */,
@@ -465,6 +484,7 @@
88113BA9B217DA579C36BEBE /* TabBar.swift in Sources */, 88113BA9B217DA579C36BEBE /* TabBar.swift in Sources */,
DCFD5B03E64A46783F46726B /* TerminalCommandArrowBehavior.swift in Sources */, DCFD5B03E64A46783F46726B /* TerminalCommandArrowBehavior.swift in Sources */,
6F249EDFA2D654457DF385F1 /* TerminalManager.swift in Sources */, 6F249EDFA2D654457DF385F1 /* TerminalManager.swift in Sources */,
DD116B0FCC341D66F1534EC4 /* TerminalScrollbackEstimator.swift in Sources */,
7A69B5AAC686174BCA54D0F0 /* TerminalSession.swift in Sources */, 7A69B5AAC686174BCA54D0F0 /* TerminalSession.swift in Sources */,
65C7DB7296C6C6A77598A1F4 /* TerminalSettingsView.swift in Sources */, 65C7DB7296C6C6A77598A1F4 /* TerminalSettingsView.swift in Sources */,
1AB4A0F1BE668D3130EFBA93 /* TerminalTheme.swift in Sources */, 1AB4A0F1BE668D3130EFBA93 /* TerminalTheme.swift in Sources */,

View File

@@ -5,33 +5,9 @@
<key>SchemeUserState</key> <key>SchemeUserState</key>
<dict> <dict>
<key>CommandNotch.xcscheme_^#shared#^_</key> <key>CommandNotch.xcscheme_^#shared#^_</key>
<dict> <dict/>
<key>orderHint</key>
<integer>0</integer>
</dict>
<key>CommandNotchTests.xcscheme_^#shared#^_</key>
<dict>
<key>orderHint</key>
<integer>2</integer>
</dict>
<key>CommandNotchUITests.xcscheme_^#shared#^_</key>
<dict>
<key>orderHint</key>
<integer>2</integer>
</dict>
<key>Release-CommandNotch.xcscheme_^#shared#^_</key> <key>Release-CommandNotch.xcscheme_^#shared#^_</key>
<dict> <dict/>
<key>orderHint</key>
<integer>1</integer>
</dict>
</dict>
<key>SuppressBuildableAutocreation</key>
<dict>
<key>1485207FA11756EC2DF4F08B</key>
<dict>
<key>primary</key>
<true/>
</dict>
</dict> </dict>
</dict> </dict>
</plist> </plist>

View File

@@ -33,6 +33,7 @@ class AppDelegate: NSObject, NSApplicationDelegate {
observeSizePreferences() observeSizePreferences()
observeFontSizeChanges() observeFontSizeChanges()
observeTerminalThemeChanges() observeTerminalThemeChanges()
observeTerminalScrollbackChanges()
applyUITestLaunchBehaviorIfNeeded() applyUITestLaunchBehaviorIfNeeded()
} }
@@ -90,6 +91,16 @@ class AppDelegate: NSObject, NSApplicationDelegate {
.store(in: &cancellables) .store(in: &cancellables)
} }
private func observeTerminalScrollbackChanges() {
settingsController.$settings
.map(\.terminal.scrollbackLines)
.removeDuplicates()
.sink { scrollbackLines in
WorkspaceRegistry.shared.updateAllWorkspacesScrollbackLines(scrollbackLines)
}
.store(in: &cancellables)
}
private var launchArguments: [String] { private var launchArguments: [String] {
ProcessInfo.processInfo.arguments ProcessInfo.processInfo.arguments
} }

View File

@@ -0,0 +1,49 @@
import AppKit
import SwiftTerm
struct TerminalScrollWheelRouter {
static func shouldSendMouseWheel(
allowMouseReporting: Bool,
mouseMode: Terminal.MouseMode,
deltaY: Double
) -> Bool {
allowMouseReporting && mouseMode != .off && deltaY != 0
}
static func velocity(for deltaY: Double) -> Int {
let magnitude = Int(abs(deltaY))
if magnitude > 9 {
return 20
}
if magnitude > 5 {
return 10
}
if magnitude > 1 {
return 3
}
return 1
}
static func gridPosition(
point: CGPoint,
bounds: CGRect,
cols: Int,
rows: Int
) -> (x: Int, y: Int, pixelX: Int, pixelY: Int) {
let safeCols = max(cols, 1)
let safeRows = max(rows, 1)
let width = max(bounds.width, 1)
let height = max(bounds.height, 1)
let clampedX = min(max(point.x, 0), width)
let clampedY = min(max(point.y, 0), height)
let cellWidth = width / CGFloat(safeCols)
let cellHeight = height / CGFloat(safeRows)
let column = min(max(Int(clampedX / cellWidth), 0), safeCols - 1)
let row = min(max(Int((height - clampedY) / cellHeight), 0), safeRows - 1)
let pixelX = min(max(Int(clampedX), 0), Int(width))
let pixelY = min(max(Int(height - clampedY), 0), Int(height))
return (column, row, pixelX, pixelY)
}
}

View File

@@ -48,6 +48,7 @@ struct AppSettings: Equatable, Codable {
fontSize: NotchSettings.Defaults.terminalFontSize, fontSize: NotchSettings.Defaults.terminalFontSize,
shellPath: NotchSettings.Defaults.terminalShell, shellPath: NotchSettings.Defaults.terminalShell,
themeRawValue: NotchSettings.Defaults.terminalTheme, themeRawValue: NotchSettings.Defaults.terminalTheme,
scrollbackLines: NotchSettings.Defaults.terminalScrollbackLines,
sizePresetsJSON: NotchSettings.Defaults.terminalSizePresets sizePresetsJSON: NotchSettings.Defaults.terminalSizePresets
), ),
hotkeys: HotkeySettings( hotkeys: HotkeySettings(
@@ -106,6 +107,7 @@ extension AppSettings {
var fontSize: Double var fontSize: Double
var shellPath: String var shellPath: String
var themeRawValue: String var themeRawValue: String
var scrollbackLines: Int
var sizePresetsJSON: String var sizePresetsJSON: String
var theme: TerminalTheme { var theme: TerminalTheme {
@@ -155,6 +157,7 @@ struct TerminalSessionConfiguration: Equatable {
var fontSize: CGFloat var fontSize: CGFloat
var theme: TerminalTheme var theme: TerminalTheme
var shellPath: String var shellPath: String
var scrollbackLines: Int
} }
@MainActor @MainActor

View File

@@ -46,7 +46,8 @@ final class AppSettingsController: ObservableObject, TerminalSessionConfiguratio
TerminalSessionConfiguration( TerminalSessionConfiguration(
fontSize: CGFloat(settings.terminal.fontSize), fontSize: CGFloat(settings.terminal.fontSize),
theme: settings.terminal.theme, theme: settings.terminal.theme,
shellPath: settings.terminal.shellPath shellPath: settings.terminal.shellPath,
scrollbackLines: settings.terminal.scrollbackLines
) )
} }

View File

@@ -52,6 +52,7 @@ struct UserDefaultsAppSettingsStore: AppSettingsStoreType {
fontSize: double(NotchSettings.Keys.terminalFontSize, default: NotchSettings.Defaults.terminalFontSize), fontSize: double(NotchSettings.Keys.terminalFontSize, default: NotchSettings.Defaults.terminalFontSize),
shellPath: string(NotchSettings.Keys.terminalShell, default: NotchSettings.Defaults.terminalShell), shellPath: string(NotchSettings.Keys.terminalShell, default: NotchSettings.Defaults.terminalShell),
themeRawValue: string(NotchSettings.Keys.terminalTheme, default: NotchSettings.Defaults.terminalTheme), themeRawValue: string(NotchSettings.Keys.terminalTheme, default: NotchSettings.Defaults.terminalTheme),
scrollbackLines: integer(NotchSettings.Keys.terminalScrollbackLines, default: NotchSettings.Defaults.terminalScrollbackLines),
sizePresetsJSON: string(NotchSettings.Keys.terminalSizePresets, default: NotchSettings.Defaults.terminalSizePresets) sizePresetsJSON: string(NotchSettings.Keys.terminalSizePresets, default: NotchSettings.Defaults.terminalSizePresets)
), ),
hotkeys: .init( hotkeys: .init(
@@ -101,6 +102,7 @@ struct UserDefaultsAppSettingsStore: AppSettingsStoreType {
defaults.set(settings.terminal.fontSize, forKey: NotchSettings.Keys.terminalFontSize) defaults.set(settings.terminal.fontSize, forKey: NotchSettings.Keys.terminalFontSize)
defaults.set(settings.terminal.shellPath, forKey: NotchSettings.Keys.terminalShell) defaults.set(settings.terminal.shellPath, forKey: NotchSettings.Keys.terminalShell)
defaults.set(settings.terminal.themeRawValue, forKey: NotchSettings.Keys.terminalTheme) defaults.set(settings.terminal.themeRawValue, forKey: NotchSettings.Keys.terminalTheme)
defaults.set(settings.terminal.scrollbackLines, forKey: NotchSettings.Keys.terminalScrollbackLines)
defaults.set(settings.terminal.sizePresetsJSON, forKey: NotchSettings.Keys.terminalSizePresets) defaults.set(settings.terminal.sizePresetsJSON, forKey: NotchSettings.Keys.terminalSizePresets)
defaults.set(settings.hotkeys.toggle.toJSON(), forKey: NotchSettings.Keys.hotkeyToggle) defaults.set(settings.hotkeys.toggle.toJSON(), forKey: NotchSettings.Keys.hotkeyToggle)

View File

@@ -47,6 +47,7 @@ enum NotchSettings {
static let terminalFontSize = "terminalFontSize" static let terminalFontSize = "terminalFontSize"
static let terminalShell = "terminalShell" static let terminalShell = "terminalShell"
static let terminalTheme = "terminalTheme" static let terminalTheme = "terminalTheme"
static let terminalScrollbackLines = "terminalScrollbackLines"
static let terminalSizePresets = "terminalSizePresets" static let terminalSizePresets = "terminalSizePresets"
static let workspaceSummaries = "workspaceSummaries" static let workspaceSummaries = "workspaceSummaries"
static let screenAssignments = "screenAssignments" static let screenAssignments = "screenAssignments"
@@ -98,6 +99,7 @@ enum NotchSettings {
static let terminalFontSize: Double = 13 static let terminalFontSize: Double = 13
static let terminalShell: String = "" static let terminalShell: String = ""
static let terminalTheme: String = TerminalTheme.terminalApp.rawValue static let terminalTheme: String = TerminalTheme.terminalApp.rawValue
static let terminalScrollbackLines: Int = 500
static let terminalSizePresets: String = TerminalSizePresetStore.defaultPresetsJSON() static let terminalSizePresets: String = TerminalSizePresetStore.defaultPresetsJSON()
// Default hotkey bindings as JSON // Default hotkey bindings as JSON
@@ -148,6 +150,7 @@ enum NotchSettings {
Keys.terminalFontSize: Defaults.terminalFontSize, Keys.terminalFontSize: Defaults.terminalFontSize,
Keys.terminalShell: Defaults.terminalShell, Keys.terminalShell: Defaults.terminalShell,
Keys.terminalTheme: Defaults.terminalTheme, Keys.terminalTheme: Defaults.terminalTheme,
Keys.terminalScrollbackLines: Defaults.terminalScrollbackLines,
Keys.terminalSizePresets: Defaults.terminalSizePresets, Keys.terminalSizePresets: Defaults.terminalSizePresets,
Keys.hotkeyToggle: Defaults.hotkeyToggle, Keys.hotkeyToggle: Defaults.hotkeyToggle,

View File

@@ -0,0 +1,54 @@
import AppKit
import CoreText
import SwiftTerm
enum TerminalScrollbackEstimator {
private static let minimumColumns = 1
private static let minimumRows = 1
private static let defaultBytesPerLineOverhead = 256
struct Estimate: Equatable {
let bytes: Int
let columns: Int
let rows: Int
var formattedBytes: String {
ByteCountFormatter.string(fromByteCount: Int64(bytes), countStyle: .memory)
}
}
static func estimate(
scrollbackLines: Int,
fontSize: Double,
openWidth: Double,
openHeight: Double
) -> Estimate {
let safeScrollbackLines = max(0, scrollbackLines)
let dimensions = terminalGridDimensions(
fontSize: fontSize,
openWidth: openWidth,
openHeight: openHeight
)
let totalLines = safeScrollbackLines + dimensions.rows
let bytesPerCell = MemoryLayout<CharData>.stride
let bytesPerLine = (dimensions.columns * bytesPerCell) + defaultBytesPerLineOverhead
let totalBytes = max(0, totalLines * bytesPerLine)
return Estimate(bytes: totalBytes, columns: dimensions.columns, rows: dimensions.rows)
}
private static func terminalGridDimensions(
fontSize: Double,
openWidth: Double,
openHeight: Double
) -> (columns: Int, rows: Int) {
let font = NSFont.monospacedSystemFont(ofSize: CGFloat(max(1, fontSize)), weight: .regular)
let cellWidth = max(1, font.advancement(forGlyph: font.glyph(withName: "W")).width)
let cellHeight = max(1, ceil(CTFontGetAscent(font) + CTFontGetDescent(font) + CTFontGetLeading(font)))
let columns = max(minimumColumns, Int(CGFloat(max(1, openWidth)) / cellWidth))
let rows = max(minimumRows, Int(CGFloat(max(1, openHeight)) / cellHeight))
return (columns, rows)
}
}

View File

@@ -2,6 +2,58 @@ import AppKit
import SwiftTerm import SwiftTerm
import Combine import Combine
/// Tracks whether the terminal viewport should follow live output or preserve
/// the user's current scrollback position.
final class TerminalScrollCoordinator {
private let bottomThreshold: Double
private var suppressScrollTracking = false
private(set) var followsOutput = true
private(set) var preservedScrollPosition: Double = 1
init(bottomThreshold: Double = 0.999) {
self.bottomThreshold = bottomThreshold
}
func terminalDidScroll(to position: Double, canScroll: Bool) {
guard !suppressScrollTracking else { return }
guard canScroll else {
followsOutput = true
preservedScrollPosition = 1
return
}
let clampedPosition = min(max(position, 0), 1)
if clampedPosition >= bottomThreshold {
followsOutput = true
preservedScrollPosition = 1
} else {
followsOutput = false
preservedScrollPosition = clampedPosition
}
}
func outputRestorePosition(canScroll: Bool) -> Double? {
guard canScroll, !followsOutput else { return nil }
return preservedScrollPosition
}
@discardableResult
func userDidStartTyping() -> Bool {
let shouldJumpToBottom = !followsOutput
followsOutput = true
preservedScrollPosition = 1
return shouldJumpToBottom
}
func suppressTracking<T>(_ body: () -> T) -> T {
suppressScrollTracking = true
defer { suppressScrollTracking = false }
return body()
}
}
/// Wraps a single SwiftTerm TerminalView + LocalProcess pair. /// Wraps a single SwiftTerm TerminalView + LocalProcess pair.
@MainActor @MainActor
class TerminalSession: NSObject, ObservableObject, LocalProcessDelegate, @preconcurrency TerminalViewDelegate { class TerminalSession: NSObject, ObservableObject, LocalProcessDelegate, @preconcurrency TerminalViewDelegate {
@@ -10,9 +62,12 @@ class TerminalSession: NSObject, ObservableObject, LocalProcessDelegate, @precon
let terminalView: TerminalView let terminalView: TerminalView
private var process: LocalProcess? private var process: LocalProcess?
private var keyEventMonitor: Any? private var keyEventMonitor: Any?
private var scrollEventMonitor: Any?
private let backgroundColor = NSColor.black private let backgroundColor = NSColor.black
private let configuredShellPath: String private let configuredShellPath: String
private var scrollbackLines: Int
private let launchDirectory: String private let launchDirectory: String
private let scrollCoordinator = TerminalScrollCoordinator()
@Published var title: String = "shell" @Published var title: String = "shell"
@Published var isRunning: Bool = true @Published var isRunning: Bool = true
@@ -22,21 +77,26 @@ class TerminalSession: NSObject, ObservableObject, LocalProcessDelegate, @precon
fontSize: CGFloat, fontSize: CGFloat,
theme: TerminalTheme, theme: TerminalTheme,
shellPath: String, shellPath: String,
scrollbackLines: Int,
initialDirectory: String? = nil, initialDirectory: String? = nil,
startImmediately: Bool = true startImmediately: Bool = true
) { ) {
terminalView = TerminalView(frame: NSRect(x: 0, y: 0, width: 600, height: 300)) terminalView = TerminalView(frame: NSRect(x: 0, y: 0, width: 600, height: 300))
configuredShellPath = shellPath configuredShellPath = shellPath
self.scrollbackLines = max(0, scrollbackLines)
launchDirectory = Self.resolveInitialDirectory(initialDirectory) launchDirectory = Self.resolveInitialDirectory(initialDirectory)
currentDirectory = launchDirectory currentDirectory = launchDirectory
super.init() super.init()
terminalView.terminalDelegate = self terminalView.terminalDelegate = self
installOsc52ClipboardHandler()
let font = NSFont.monospacedSystemFont(ofSize: fontSize, weight: .regular) let font = NSFont.monospacedSystemFont(ofSize: fontSize, weight: .regular)
terminalView.font = font terminalView.font = font
applyTheme(theme) applyTheme(theme)
updateScrollbackLines(self.scrollbackLines)
installCommandArrowMonitor() installCommandArrowMonitor()
installScrollWheelMonitor()
if startImmediately { if startImmediately {
startShell() startShell()
@@ -49,6 +109,9 @@ class TerminalSession: NSObject, ObservableObject, LocalProcessDelegate, @precon
if let keyEventMonitor { if let keyEventMonitor {
NSEvent.removeMonitor(keyEventMonitor) NSEvent.removeMonitor(keyEventMonitor)
} }
if let scrollEventMonitor {
NSEvent.removeMonitor(scrollEventMonitor)
}
} }
// MARK: - Shell management // MARK: - Shell management
@@ -96,6 +159,23 @@ class TerminalSession: NSObject, ObservableObject, LocalProcessDelegate, @precon
return ProcessInfo.processInfo.environment["SHELL"] ?? "/bin/zsh" return ProcessInfo.processInfo.environment["SHELL"] ?? "/bin/zsh"
} }
private func installOsc52ClipboardHandler() {
let maxPayloadSize = 1_048_576 // 1 MB
terminalView.getTerminal().registerOscHandler(code: 52) { [weak self] data in
guard data.count >= 2,
data[data.startIndex] == UInt8(ascii: "c"),
data[data.startIndex + 1] == UInt8(ascii: ";") else { return }
let base64 = Data(data[(data.startIndex + 2)...])
guard let content = Data(base64Encoded: base64),
content.count <= maxPayloadSize,
let string = String(data: content, encoding: .utf8) else { return }
NSPasteboard.general.clearContents()
NSPasteboard.general.setString(string, forType: .string)
}
}
private func installCommandArrowMonitor() { private func installCommandArrowMonitor() {
keyEventMonitor = NSEvent.addLocalMonitorForEvents(matching: .keyDown) { [weak self] event in keyEventMonitor = NSEvent.addLocalMonitorForEvents(matching: .keyDown) { [weak self] event in
guard let self else { return event } guard let self else { return event }
@@ -116,6 +196,53 @@ class TerminalSession: NSObject, ObservableObject, LocalProcessDelegate, @precon
} }
} }
private func installScrollWheelMonitor() {
scrollEventMonitor = NSEvent.addLocalMonitorForEvents(matching: .scrollWheel) { [weak self] event in
guard let self else { return event }
guard let window = self.terminalView.window else { return event }
guard event.window === window else { return event }
guard window.firstResponder === self.terminalView else { return event }
let terminal = self.terminalView.getTerminal()
guard TerminalScrollWheelRouter.shouldSendMouseWheel(
allowMouseReporting: self.terminalView.allowMouseReporting,
mouseMode: terminal.mouseMode,
deltaY: event.deltaY
) else {
return event
}
let localPoint = self.terminalView.convert(event.locationInWindow, from: nil)
let dims = terminal.getDims()
let hit = TerminalScrollWheelRouter.gridPosition(
point: localPoint,
bounds: self.terminalView.bounds,
cols: dims.cols,
rows: dims.rows
)
let button = event.deltaY > 0 ? 4 : 5
let flags = terminal.encodeButton(
button: button,
release: false,
shift: event.modifierFlags.contains(.shift),
meta: event.modifierFlags.contains(.option),
control: event.modifierFlags.contains(.control)
)
for _ in 0..<TerminalScrollWheelRouter.velocity(for: event.deltaY) {
terminal.sendEvent(
buttonFlags: flags,
x: hit.x,
y: hit.y,
pixelX: hit.pixelX,
pixelY: hit.pixelY
)
}
return nil
}
}
func updateFontSize(_ size: CGFloat) { func updateFontSize(_ size: CGFloat) {
terminalView.font = NSFont.monospacedSystemFont(ofSize: size, weight: .regular) terminalView.font = NSFont.monospacedSystemFont(ofSize: size, weight: .regular)
} }
@@ -128,6 +255,12 @@ class TerminalSession: NSObject, ObservableObject, LocalProcessDelegate, @precon
terminalView.installColors(theme.ansiColors) terminalView.installColors(theme.ansiColors)
} }
func updateScrollbackLines(_ scrollbackLines: Int) {
let sanitizedScrollbackLines = max(0, scrollbackLines)
self.scrollbackLines = sanitizedScrollbackLines
terminalView.getTerminal().changeHistorySize(sanitizedScrollbackLines)
}
func terminate() { func terminate() {
process?.terminate() process?.terminate()
process = nil process = nil
@@ -137,12 +270,43 @@ class TerminalSession: NSObject, ObservableObject, LocalProcessDelegate, @precon
// MARK: - LocalProcessDelegate // MARK: - LocalProcessDelegate
nonisolated func processTerminated(_ source: LocalProcess, exitCode: Int32?) { nonisolated func processTerminated(_ source: LocalProcess, exitCode: Int32?) {
Task { @MainActor in self.isRunning = false } Task { @MainActor in
self.isRunning = false
self.resetTerminalModes()
}
}
private func resetTerminalModes() {
let resetSequences: [[UInt8]] = [
Array("\u{1b}[?9l".utf8),
Array("\u{1b}[?1000l".utf8),
Array("\u{1b}[?1002l".utf8),
Array("\u{1b}[?1003l".utf8),
Array("\u{1b}[?1006l".utf8),
Array("\u{1b}[?1015l".utf8),
Array("\u{1b}[?2004l".utf8),
Array("\u{1b}[?1l".utf8),
Array("\u{1b}[?1049l".utf8),
Array("\u{1b}[?25h".utf8),
]
for seq in resetSequences {
terminalView.feed(byteArray: seq[...])
}
} }
nonisolated func dataReceived(slice: ArraySlice<UInt8>) { nonisolated func dataReceived(slice: ArraySlice<UInt8>) {
let data = slice let data = slice
Task { @MainActor in self.terminalView.feed(byteArray: data) } Task { @MainActor in
if let restorePosition = self.scrollCoordinator.outputRestorePosition(canScroll: self.terminalView.canScroll) {
self.scrollCoordinator.suppressTracking {
self.terminalView.feed(byteArray: data)
self.terminalView.scroll(toPosition: restorePosition)
}
} else {
self.terminalView.feed(byteArray: data)
}
}
} }
nonisolated func getWindowSize() -> winsize { nonisolated func getWindowSize() -> winsize {
@@ -155,6 +319,9 @@ class TerminalSession: NSObject, ObservableObject, LocalProcessDelegate, @precon
// MARK: - TerminalViewDelegate // MARK: - TerminalViewDelegate
func send(source: TerminalView, data: ArraySlice<UInt8>) { func send(source: TerminalView, data: ArraySlice<UInt8>) {
if scrollCoordinator.userDidStartTyping() {
terminalView.scroll(toPosition: 1)
}
process?.send(data: data) process?.send(data: data)
} }
@@ -179,7 +346,9 @@ class TerminalSession: NSObject, ObservableObject, LocalProcessDelegate, @precon
currentDirectory = normalizedDirectory currentDirectory = normalizedDirectory
} }
func scrolled(source: TerminalView, position: Double) {} func scrolled(source: TerminalView, position: Double) {
scrollCoordinator.terminalDidScroll(to: position, canScroll: source.canScroll)
}
func rangeChanged(source: TerminalView, startY: Int, endY: Int) {} func rangeChanged(source: TerminalView, startY: Int, endY: Int) {}
func clipboardCopy(source: TerminalView, content: Data) { func clipboardCopy(source: TerminalView, content: Data) {

View File

@@ -7,6 +7,7 @@ protocol TerminalSessionFactoryType {
fontSize: CGFloat, fontSize: CGFloat,
theme: TerminalTheme, theme: TerminalTheme,
shellPath: String, shellPath: String,
scrollbackLines: Int,
initialDirectory: String? initialDirectory: String?
) -> TerminalSession ) -> TerminalSession
} }
@@ -16,12 +17,14 @@ struct LiveTerminalSessionFactory: TerminalSessionFactoryType {
fontSize: CGFloat, fontSize: CGFloat,
theme: TerminalTheme, theme: TerminalTheme,
shellPath: String, shellPath: String,
scrollbackLines: Int,
initialDirectory: String? initialDirectory: String?
) -> TerminalSession { ) -> TerminalSession {
TerminalSession( TerminalSession(
fontSize: fontSize, fontSize: fontSize,
theme: theme, theme: theme,
shellPath: shellPath, shellPath: shellPath,
scrollbackLines: scrollbackLines,
initialDirectory: initialDirectory initialDirectory: initialDirectory
) )
} }
@@ -106,6 +109,7 @@ final class WorkspaceController: ObservableObject {
fontSize: config.fontSize, fontSize: config.fontSize,
theme: config.theme, theme: config.theme,
shellPath: config.shellPath, shellPath: config.shellPath,
scrollbackLines: config.scrollbackLines,
initialDirectory: activeTab?.currentDirectory initialDirectory: activeTab?.currentDirectory
) )
@@ -187,4 +191,10 @@ final class WorkspaceController: ObservableObject {
tab.applyTheme(theme) tab.applyTheme(theme)
} }
} }
func updateAllScrollbackLines(_ scrollbackLines: Int) {
for tab in tabs {
tab.updateScrollbackLines(scrollbackLines)
}
}
} }

View File

@@ -157,6 +157,12 @@ final class WorkspaceRegistry: ObservableObject {
} }
} }
func updateAllWorkspacesScrollbackLines(_ scrollbackLines: Int) {
for controller in controllers.values {
controller.updateAllScrollbackLines(scrollbackLines)
}
}
private func resolvedWorkspaceName(from proposedName: String?) -> String { private func resolvedWorkspaceName(from proposedName: String?) -> String {
let trimmed = proposedName?.trimmingCharacters(in: .whitespacesAndNewlines) ?? "" let trimmed = proposedName?.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
if !trimmed.isEmpty { if !trimmed.isEmpty {

View File

@@ -1,6 +1,8 @@
import SwiftUI import SwiftUI
struct TerminalSettingsView: View { struct TerminalSettingsView: View {
private static let scrollbackRange = 0...1_000_000
@ObservedObject private var settingsController = AppSettingsController.shared @ObservedObject private var settingsController = AppSettingsController.shared
@State private var sizePresets: [TerminalSizePreset] = [] @State private var sizePresets: [TerminalSizePreset] = []
@@ -40,6 +42,34 @@ struct TerminalSettingsView: View {
.foregroundStyle(.secondary) .foregroundStyle(.secondary)
} }
Section("Scrollback") {
HStack {
Text("Lines")
Spacer()
TextField(
"Scrollback lines",
value: scrollbackBinding,
format: .number.grouping(.automatic)
)
.textFieldStyle(.roundedBorder)
.frame(width: 140)
}
Stepper(
value: scrollbackBinding,
in: Self.scrollbackRange,
step: scrollbackStepSize
) {
Text("\(settingsController.settings.terminal.scrollbackLines.formatted()) lines")
.monospacedDigit()
}
let estimate = scrollbackEstimate
Text("Based on the current terminal size, this may use ~\(estimate.formattedBytes) of RAM (\(estimate.columns) columns x \(estimate.rows) rows visible).")
.font(.caption)
.foregroundStyle(.secondary)
}
Section("Size Presets") { Section("Size Presets") {
ForEach($sizePresets) { $preset in ForEach($sizePresets) { $preset in
TerminalSizePresetEditor( TerminalSizePresetEditor(
@@ -94,6 +124,39 @@ struct TerminalSettingsView: View {
sizePresets.removeAll { $0.id == id } sizePresets.removeAll { $0.id == id }
} }
private var scrollbackBinding: Binding<Int> {
Binding(
get: { settingsController.settings.terminal.scrollbackLines },
set: { newValue in
let clampedValue = min(max(Self.scrollbackRange.lowerBound, newValue), Self.scrollbackRange.upperBound)
settingsController.update {
$0.terminal.scrollbackLines = clampedValue
}
}
)
}
private var scrollbackEstimate: TerminalScrollbackEstimator.Estimate {
TerminalScrollbackEstimator.estimate(
scrollbackLines: settingsController.settings.terminal.scrollbackLines,
fontSize: settingsController.settings.terminal.fontSize,
openWidth: settingsController.settings.display.openWidth,
openHeight: settingsController.settings.display.openHeight
)
}
private var scrollbackStepSize: Int {
let lines = settingsController.settings.terminal.scrollbackLines
switch lines {
case ..<10_000:
return 500
case ..<100_000:
return 5_000
default:
return 10_000
}
}
private func applyPreset(_ preset: TerminalSizePreset) { private func applyPreset(_ preset: TerminalSizePreset) {
settingsController.update { settingsController.update {
$0.display.openWidth = preset.width $0.display.openWidth = preset.width

View File

@@ -7,11 +7,13 @@ final class AppSettingsControllerTests: XCTestCase {
let store = InMemoryAppSettingsStore() let store = InMemoryAppSettingsStore()
var settings = AppSettings.default var settings = AppSettings.default
settings.terminal.shellPath = "/opt/homebrew/bin/fish" settings.terminal.shellPath = "/opt/homebrew/bin/fish"
settings.terminal.scrollbackLines = 12_000
store.storedSettings = settings store.storedSettings = settings
let controller = AppSettingsController(store: store) let controller = AppSettingsController(store: store)
XCTAssertEqual(controller.terminalSessionConfiguration.shellPath, "/opt/homebrew/bin/fish") XCTAssertEqual(controller.terminalSessionConfiguration.shellPath, "/opt/homebrew/bin/fish")
XCTAssertEqual(controller.terminalSessionConfiguration.scrollbackLines, 12_000)
} }
func testTerminalSizePresetsDecodeFromTypedSettings() { func testTerminalSizePresetsDecodeFromTypedSettings() {

View File

@@ -26,6 +26,7 @@ final class AppSettingsStoreTests: XCTestCase {
settings.appearance.blurRadius = 4.5 settings.appearance.blurRadius = 4.5
settings.terminal.fontSize = 16 settings.terminal.fontSize = 16
settings.terminal.themeRawValue = TerminalTheme.dracula.rawValue settings.terminal.themeRawValue = TerminalTheme.dracula.rawValue
settings.terminal.scrollbackLines = 25_000
settings.terminal.sizePresetsJSON = TerminalSizePresetStore.encodePresets([ settings.terminal.sizePresetsJSON = TerminalSizePresetStore.encodePresets([
TerminalSizePreset(name: "Wide", width: 960, height: 420, hotkey: .cmdShiftDigit(4)) TerminalSizePreset(name: "Wide", width: 960, height: 420, hotkey: .cmdShiftDigit(4))
]) ])

View File

@@ -306,7 +306,7 @@ private final class TestAppSettingsStore: AppSettingsStoreType {
} }
private final class ScreenRegistryTestSettingsProvider: TerminalSessionConfigurationProviding { private final class ScreenRegistryTestSettingsProvider: TerminalSessionConfigurationProviding {
let terminalSessionConfiguration = TerminalSessionConfiguration(fontSize: 13, theme: .terminalApp, shellPath: "") let terminalSessionConfiguration = TerminalSessionConfiguration(fontSize: 13, theme: .terminalApp, shellPath: "", scrollbackLines: 500)
let hotkeySettings = AppSettings.default.hotkeys let hotkeySettings = AppSettings.default.hotkeys
let terminalSizePresets = TerminalSizePresetStore.loadDefaults() let terminalSizePresets = TerminalSizePresetStore.loadDefaults()
} }
@@ -317,6 +317,7 @@ private struct ScreenRegistryUnusedTerminalSessionFactory: TerminalSessionFactor
fontSize: CGFloat, fontSize: CGFloat,
theme: TerminalTheme, theme: TerminalTheme,
shellPath: String, shellPath: String,
scrollbackLines: Int,
initialDirectory: String? initialDirectory: String?
) -> TerminalSession { ) -> TerminalSession {
fatalError("ScreenRegistryTests should not create live terminal sessions.") fatalError("ScreenRegistryTests should not create live terminal sessions.")

View File

@@ -0,0 +1,46 @@
import XCTest
@testable import CommandNotch
final class TerminalScrollCoordinatorTests: XCTestCase {
func testScrollAwayFromBottomDisablesOutputFollow() {
let coordinator = TerminalScrollCoordinator()
coordinator.terminalDidScroll(to: 0.42, canScroll: true)
XCTAssertFalse(coordinator.followsOutput)
XCTAssertEqual(coordinator.outputRestorePosition(canScroll: true) ?? .nan, 0.42, accuracy: 0.0001)
}
func testTypingReEnablesFollowAndRequestsJumpToBottom() {
let coordinator = TerminalScrollCoordinator()
coordinator.terminalDidScroll(to: 0.42, canScroll: true)
let shouldJump = coordinator.userDidStartTyping()
XCTAssertTrue(shouldJump)
XCTAssertTrue(coordinator.followsOutput)
XCTAssertNil(coordinator.outputRestorePosition(canScroll: true))
}
func testScrollingBackToBottomReEnablesOutputFollow() {
let coordinator = TerminalScrollCoordinator()
coordinator.terminalDidScroll(to: 0.42, canScroll: true)
coordinator.terminalDidScroll(to: 1, canScroll: true)
XCTAssertTrue(coordinator.followsOutput)
XCTAssertNil(coordinator.outputRestorePosition(canScroll: true))
}
func testSuppressedTrackingIgnoresProgrammaticScrollUpdates() {
let coordinator = TerminalScrollCoordinator()
coordinator.terminalDidScroll(to: 0.42, canScroll: true)
coordinator.suppressTracking {
coordinator.terminalDidScroll(to: 1, canScroll: true)
}
XCTAssertFalse(coordinator.followsOutput)
XCTAssertEqual(coordinator.outputRestorePosition(canScroll: true) ?? .nan, 0.42, accuracy: 0.0001)
}
}

View File

@@ -0,0 +1,49 @@
import XCTest
@testable import CommandNotch
import SwiftTerm
final class TerminalScrollWheelRouterTests: XCTestCase {
func testMouseWheelForwardingRequiresMouseReportingAndActiveMouseMode() {
XCTAssertFalse(TerminalScrollWheelRouter.shouldSendMouseWheel(
allowMouseReporting: false,
mouseMode: .vt200,
deltaY: 1
))
XCTAssertFalse(TerminalScrollWheelRouter.shouldSendMouseWheel(
allowMouseReporting: true,
mouseMode: .off,
deltaY: 1
))
XCTAssertFalse(TerminalScrollWheelRouter.shouldSendMouseWheel(
allowMouseReporting: true,
mouseMode: .vt200,
deltaY: 0
))
XCTAssertTrue(TerminalScrollWheelRouter.shouldSendMouseWheel(
allowMouseReporting: true,
mouseMode: .vt200,
deltaY: -1
))
}
func testVelocityMatchesExpectedThresholds() {
XCTAssertEqual(TerminalScrollWheelRouter.velocity(for: 1), 1)
XCTAssertEqual(TerminalScrollWheelRouter.velocity(for: 2), 3)
XCTAssertEqual(TerminalScrollWheelRouter.velocity(for: 6), 10)
XCTAssertEqual(TerminalScrollWheelRouter.velocity(for: 10), 20)
}
func testGridPositionClampsToTerminalBounds() {
let hit = TerminalScrollWheelRouter.gridPosition(
point: CGPoint(x: 210, y: -10),
bounds: CGRect(x: 0, y: 0, width: 200, height: 100),
cols: 10,
rows: 5
)
XCTAssertEqual(hit.x, 9)
XCTAssertEqual(hit.y, 4)
XCTAssertEqual(hit.pixelX, 200)
XCTAssertEqual(hit.pixelY, 100)
}
}

View File

@@ -0,0 +1,34 @@
import XCTest
@testable import CommandNotch
final class TerminalScrollbackEstimatorTests: XCTestCase {
func testEstimateIncreasesAsScrollbackGrows() {
let small = TerminalScrollbackEstimator.estimate(
scrollbackLines: 5_000,
fontSize: 13,
openWidth: 640,
openHeight: 350
)
let large = TerminalScrollbackEstimator.estimate(
scrollbackLines: 100_000,
fontSize: 13,
openWidth: 640,
openHeight: 350
)
XCTAssertGreaterThan(large.bytes, small.bytes)
XCTAssertGreaterThan(large.columns, 0)
XCTAssertGreaterThan(large.rows, 0)
}
func testEstimateClampsNegativeScrollbackToZero() {
let estimate = TerminalScrollbackEstimator.estimate(
scrollbackLines: -1_000,
fontSize: 13,
openWidth: 640,
openHeight: 350
)
XCTAssertGreaterThan(estimate.bytes, 0)
}
}

View File

@@ -191,7 +191,7 @@ private final class InMemoryWorkspaceStore: WorkspaceStoreType {
} }
private final class TestSettingsProvider: TerminalSessionConfigurationProviding { private final class TestSettingsProvider: TerminalSessionConfigurationProviding {
let terminalSessionConfiguration = TerminalSessionConfiguration(fontSize: 13, theme: .terminalApp, shellPath: "") let terminalSessionConfiguration = TerminalSessionConfiguration(fontSize: 13, theme: .terminalApp, shellPath: "", scrollbackLines: 500)
let hotkeySettings = AppSettings.default.hotkeys let hotkeySettings = AppSettings.default.hotkeys
let terminalSizePresets = TerminalSizePresetStore.loadDefaults() let terminalSizePresets = TerminalSizePresetStore.loadDefaults()
} }
@@ -204,6 +204,7 @@ private final class RecordingTerminalSessionFactory: TerminalSessionFactoryType
fontSize: CGFloat, fontSize: CGFloat,
theme: TerminalTheme, theme: TerminalTheme,
shellPath: String, shellPath: String,
scrollbackLines: Int,
initialDirectory: String? initialDirectory: String?
) -> TerminalSession { ) -> TerminalSession {
requestedDirectories.append(initialDirectory) requestedDirectories.append(initialDirectory)
@@ -211,6 +212,7 @@ private final class RecordingTerminalSessionFactory: TerminalSessionFactoryType
fontSize: fontSize, fontSize: fontSize,
theme: theme, theme: theme,
shellPath: shellPath, shellPath: shellPath,
scrollbackLines: scrollbackLines,
initialDirectory: initialDirectory, initialDirectory: initialDirectory,
startImmediately: false startImmediately: false
) )
@@ -223,6 +225,7 @@ private struct UnusedTerminalSessionFactory: TerminalSessionFactoryType {
fontSize: CGFloat, fontSize: CGFloat,
theme: TerminalTheme, theme: TerminalTheme,
shellPath: String, shellPath: String,
scrollbackLines: Int,
initialDirectory: String? initialDirectory: String?
) -> TerminalSession { ) -> TerminalSession {
fatalError("WorkspaceRegistryTests should not create live terminal sessions.") fatalError("WorkspaceRegistryTests should not create live terminal sessions.")

View File

@@ -63,6 +63,7 @@ Click the preview above to watch the demo recording.
- macOS 14 or later - macOS 14 or later
- Xcode 16 or later - Xcode 16 or later
- Homebrew `xcodegen` - Homebrew `xcodegen`
- Homebrew `create-dmg` for release `.dmg` packaging
### Build ### Build
@@ -81,6 +82,33 @@ DEVELOPER_DIR=/Applications/Xcode.app/Contents/Developer \
xcodebuild build -project CommandNotch.xcodeproj -scheme CommandNotch -destination 'platform=macOS' xcodebuild build -project CommandNotch.xcodeproj -scheme CommandNotch -destination 'platform=macOS'
``` ```
### Build a release `.dmg`
Use `create-dmg` to build the styled Finder installer window with the usual drag-to-`Applications` layout.
Install the packaging dependency once:
```bash
brew install create-dmg
```
Then build from the `app/` directory:
```bash
./scripts/build-release-dmg.sh
```
That produces:
- `releases/CommandNotch YYYY-MM-DD HH-MM-SS/CommandNotch.app`
- `releases/CommandNotch YYYY-MM-DD HH-MM-SS/CommandNotch.dmg`
Notes:
- The script regenerates the Xcode project, archives the Release build, then packages the archived app into a styled `.dmg`.
- The archive is written to `/tmp` and is only used as the source for the exported `.app`.
- If you want a distributable build signed with a specific identity, make sure your Xcode signing settings are configured before running the archive step.
## Project Layout ## Project Layout
```text ```text

129
scripts/build-dmg.sh Executable file
View File

@@ -0,0 +1,129 @@
#!/usr/bin/env bash
set -euo pipefail
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
PROJECT_DIR="$ROOT_DIR/CommandNotch"
PROJECT_SPEC="$PROJECT_DIR/project.yml"
XCODEPROJ="$PROJECT_DIR/CommandNotch.xcodeproj"
SCHEME="Release-CommandNotch"
CONFIGURATION="Release"
APP_NAME="CommandNotch"
DERIVED_DATA_DIR="$ROOT_DIR/build/release"
DIST_DIR="$ROOT_DIR/dist"
STAGING_DIR="$DIST_DIR/dmg"
APP_BUNDLE="$DERIVED_DATA_DIR/Build/Products/$CONFIGURATION/$APP_NAME.app"
DMG_PATH="$DIST_DIR/$APP_NAME.dmg"
usage() {
cat <<EOF
Usage: $(basename "$0") [--skip-generate]
Builds $APP_NAME.app and packages it into a drag-to-Applications DMG.
Options:
--skip-generate Reuse the existing Xcode project without running xcodegen.
-h, --help Show this help text.
EOF
}
require_command() {
if ! command -v "$1" >/dev/null 2>&1; then
echo "error: required command not found: $1" >&2
exit 1
fi
}
configure_developer_dir() {
local current_dir
current_dir="$(xcode-select -p 2>/dev/null || true)"
if [[ "$current_dir" == "/Library/Developer/CommandLineTools" ]]; then
local xcode_dir="/Applications/Xcode.app/Contents/Developer"
if [[ -d "$xcode_dir" ]]; then
export DEVELOPER_DIR="$xcode_dir"
echo "Using Xcode developer directory: $DEVELOPER_DIR"
return
fi
echo "error: xcode-select is pointing at Command Line Tools, and /Applications/Xcode.app was not found." >&2
echo "Install Xcode or run: sudo xcode-select -s /Applications/Xcode.app/Contents/Developer" >&2
exit 1
fi
}
skip_generate=0
while (($# > 0)); do
case "$1" in
--skip-generate)
skip_generate=1
shift
;;
-h|--help)
usage
exit 0
;;
*)
echo "error: unknown argument: $1" >&2
usage >&2
exit 1
;;
esac
done
require_command xcodebuild
require_command hdiutil
if [[ $skip_generate -eq 0 ]]; then
require_command xcodegen
fi
configure_developer_dir
if [[ ! -f "$PROJECT_SPEC" ]]; then
echo "error: project spec not found at $PROJECT_SPEC" >&2
exit 1
fi
if [[ $skip_generate -eq 0 ]]; then
echo "Generating Xcode project..."
xcodegen generate --spec "$PROJECT_SPEC"
fi
if [[ ! -d "$XCODEPROJ" ]]; then
echo "error: Xcode project not found at $XCODEPROJ" >&2
exit 1
fi
echo "Building $APP_NAME.app..."
rm -rf "$DERIVED_DATA_DIR"
xcodebuild \
-project "$XCODEPROJ" \
-scheme "$SCHEME" \
-configuration "$CONFIGURATION" \
-derivedDataPath "$DERIVED_DATA_DIR" \
build
if [[ ! -d "$APP_BUNDLE" ]]; then
echo "error: built app bundle not found at $APP_BUNDLE" >&2
exit 1
fi
echo "Preparing DMG staging folder..."
rm -rf "$STAGING_DIR" "$DMG_PATH"
mkdir -p "$STAGING_DIR"
ditto "$APP_BUNDLE" "$STAGING_DIR/$APP_NAME.app"
ln -s /Applications "$STAGING_DIR/Applications"
echo "Creating DMG..."
hdiutil create \
-volname "$APP_NAME" \
-srcfolder "$STAGING_DIR" \
-ov \
-format UDZO \
"$DMG_PATH"
echo
echo "Done:"
echo " App: $APP_BUNDLE"
echo " DMG: $DMG_PATH"

62
scripts/build-release-dmg.sh Executable file
View File

@@ -0,0 +1,62 @@
#!/usr/bin/env bash
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
APP_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
PROJECT_DIR="$APP_ROOT/CommandNotch"
if ! command -v xcodegen >/dev/null 2>&1; then
echo "error: xcodegen is required. Install it with: brew install xcodegen" >&2
exit 1
fi
if ! command -v create-dmg >/dev/null 2>&1; then
echo "error: create-dmg is required. Install it with: brew install create-dmg" >&2
exit 1
fi
timestamp="$(date '+%Y-%m-%d %H-%M-%S')"
release_dir="$APP_ROOT/releases/CommandNotch $timestamp"
archive_path="/tmp/CommandNotch-$timestamp.xcarchive"
staging_dir="$(mktemp -d)"
app_path="$release_dir/CommandNotch.app"
dmg_path="$release_dir/CommandNotch.dmg"
cleanup() {
rm -rf "$staging_dir"
}
trap cleanup EXIT
mkdir -p "$release_dir"
cd "$PROJECT_DIR"
xcodegen generate --spec project.yml
DEVELOPER_DIR="${DEVELOPER_DIR:-/Applications/Xcode.app/Contents/Developer}" \
xcodebuild archive \
-project CommandNotch.xcodeproj \
-scheme CommandNotch \
-configuration Release \
-destination 'generic/platform=macOS' \
-archivePath "$archive_path"
ditto "$archive_path/Products/Applications/CommandNotch.app" "$app_path"
ditto "$app_path" "$staging_dir/CommandNotch.app"
create-dmg \
--volname "CommandNotch" \
--window-pos 200 120 \
--window-size 720 420 \
--icon-size 128 \
--icon "CommandNotch.app" 180 210 \
--icon "Applications" 540 210 \
--hide-extension "CommandNotch.app" \
--app-drop-link 540 210 \
"$dmg_path" \
"$staging_dir"
echo "Created:"
echo " $app_path"
echo " $dmg_path"