Compare commits
9 Commits
1777eac0d6
...
better-res
| Author | SHA1 | Date | |
|---|---|---|---|
|
e4719cb9f4
|
|||
|
256998eb9f
|
|||
|
9d05bc586a
|
|||
|
a6c8218bab
|
|||
|
ce20a46ccc
|
|||
|
23dc8d0be3
|
|||
|
81a296609a
|
|||
|
5d161bb214
|
|||
|
2bf1cbad2a
|
5
.gitignore
vendored
@@ -78,3 +78,8 @@ jspm_packages/
|
|||||||
# Build output
|
# Build output
|
||||||
dist/
|
dist/
|
||||||
build/
|
build/
|
||||||
|
|
||||||
|
**/Release*
|
||||||
|
CommandNotch 20*
|
||||||
|
|
||||||
|
**/.DS_Store
|
||||||
@@ -7,11 +7,13 @@
|
|||||||
objects = {
|
objects = {
|
||||||
|
|
||||||
/* Begin PBXBuildFile section */
|
/* Begin PBXBuildFile section */
|
||||||
|
0F4A88A33D93B6E100A1C001 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 0F4A88A33D93B6E100A1C002 /* Assets.xcassets */; };
|
||||||
2213F430F3D8A88033607CD2 /* NotchSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA6359CF9DDF89413440300D /* NotchSettings.swift */; };
|
2213F430F3D8A88033607CD2 /* NotchSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA6359CF9DDF89413440300D /* NotchSettings.swift */; };
|
||||||
247C6F84E7ADE7AED43381E2 /* DowntermApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B671125208055E5334CB85E /* DowntermApp.swift */; };
|
247C6F84E7ADE7AED43381E2 /* CommandNotchApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B671125208055E5334CB85E /* CommandNotchApp.swift */; };
|
||||||
26A767A10DDA77A690CC3C37 /* NotchViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 589421631401C819FE1A7BA9 /* NotchViewModel.swift */; };
|
26A767A10DDA77A690CC3C37 /* NotchViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 589421631401C819FE1A7BA9 /* NotchViewModel.swift */; };
|
||||||
295653929D5B9C0E6C90D6D7 /* SwiftTerm in Frameworks */ = {isa = PBXBuildFile; productRef = 032AECA58EA4C274BE9F3320 /* SwiftTerm */; };
|
295653929D5B9C0E6C90D6D7 /* SwiftTerm in Frameworks */ = {isa = PBXBuildFile; productRef = 032AECA58EA4C274BE9F3320 /* SwiftTerm */; };
|
||||||
37FC0A7CEEA37C9DCC6A8351 /* TerminalSession.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B598809B19C892470DE7268 /* TerminalSession.swift */; };
|
37FC0A7CEEA37C9DCC6A8351 /* TerminalSession.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B598809B19C892470DE7268 /* TerminalSession.swift */; };
|
||||||
|
3A1F0C4BE9D84A5C8E2B7101 /* TerminalTheme.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A1F0C4AE9D84A5C8E2B7101 /* TerminalTheme.swift */; };
|
||||||
4566E6B87CB62AF5C8D4B9D8 /* NotchShape.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1FC09C538CBE7C2D072008B2 /* NotchShape.swift */; };
|
4566E6B87CB62AF5C8D4B9D8 /* NotchShape.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1FC09C538CBE7C2D072008B2 /* NotchShape.swift */; };
|
||||||
4D5125E11B4DDBDB3DFACBAF /* HotkeyManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3B72743F178231E0B06DD3DE /* HotkeyManager.swift */; };
|
4D5125E11B4DDBDB3DFACBAF /* HotkeyManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3B72743F178231E0B06DD3DE /* HotkeyManager.swift */; };
|
||||||
5B14FC23928E817FEB8D2A74 /* NotchWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02FEFF9074A85F02C43D9408 /* NotchWindow.swift */; };
|
5B14FC23928E817FEB8D2A74 /* NotchWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02FEFF9074A85F02C43D9408 /* NotchWindow.swift */; };
|
||||||
@@ -35,6 +37,7 @@
|
|||||||
02FEFF9074A85F02C43D9408 /* NotchWindow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotchWindow.swift; sourceTree = "<group>"; };
|
02FEFF9074A85F02C43D9408 /* NotchWindow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotchWindow.swift; sourceTree = "<group>"; };
|
||||||
0A973877BCE6084D0EBBBDBD /* SettingsWindowController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsWindowController.swift; sourceTree = "<group>"; };
|
0A973877BCE6084D0EBBBDBD /* SettingsWindowController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsWindowController.swift; sourceTree = "<group>"; };
|
||||||
0B567F3B5D006D2B35630CFF /* LaunchAtLoginHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LaunchAtLoginHelper.swift; sourceTree = "<group>"; };
|
0B567F3B5D006D2B35630CFF /* LaunchAtLoginHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LaunchAtLoginHelper.swift; sourceTree = "<group>"; };
|
||||||
|
0F4A88A33D93B6E100A1C002 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
|
||||||
15A290D4D21D6C01A583A372 /* ScreenManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScreenManager.swift; sourceTree = "<group>"; };
|
15A290D4D21D6C01A583A372 /* ScreenManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScreenManager.swift; sourceTree = "<group>"; };
|
||||||
1E47000112562615C7E59489 /* SwiftTermView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwiftTermView.swift; sourceTree = "<group>"; };
|
1E47000112562615C7E59489 /* SwiftTermView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwiftTermView.swift; sourceTree = "<group>"; };
|
||||||
1FC09C538CBE7C2D072008B2 /* NotchShape.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotchShape.swift; sourceTree = "<group>"; };
|
1FC09C538CBE7C2D072008B2 /* NotchShape.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotchShape.swift; sourceTree = "<group>"; };
|
||||||
@@ -42,13 +45,14 @@
|
|||||||
2C5C99B7CD7F60E55844E40C /* NotchState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotchState.swift; sourceTree = "<group>"; };
|
2C5C99B7CD7F60E55844E40C /* NotchState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotchState.swift; sourceTree = "<group>"; };
|
||||||
3B72743F178231E0B06DD3DE /* HotkeyManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HotkeyManager.swift; sourceTree = "<group>"; };
|
3B72743F178231E0B06DD3DE /* HotkeyManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HotkeyManager.swift; sourceTree = "<group>"; };
|
||||||
490C53139360D970099D8F3D /* HotkeyRecorderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HotkeyRecorderView.swift; sourceTree = "<group>"; };
|
490C53139360D970099D8F3D /* HotkeyRecorderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HotkeyRecorderView.swift; sourceTree = "<group>"; };
|
||||||
4B671125208055E5334CB85E /* DowntermApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DowntermApp.swift; sourceTree = "<group>"; };
|
4B671125208055E5334CB85E /* CommandNotchApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CommandNotchApp.swift; sourceTree = "<group>"; };
|
||||||
4BB81B6DA7126E1F5FBCC8B8 /* HotkeyBinding.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HotkeyBinding.swift; sourceTree = "<group>"; };
|
4BB81B6DA7126E1F5FBCC8B8 /* HotkeyBinding.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HotkeyBinding.swift; sourceTree = "<group>"; };
|
||||||
589421631401C819FE1A7BA9 /* NotchViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotchViewModel.swift; sourceTree = "<group>"; };
|
589421631401C819FE1A7BA9 /* NotchViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotchViewModel.swift; sourceTree = "<group>"; };
|
||||||
5C0779490DE9020FBBC464BE /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
|
5C0779490DE9020FBBC464BE /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
|
||||||
665CFC051CF185B71199608D /* Downterm.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Downterm.app; sourceTree = BUILT_PRODUCTS_DIR; };
|
665CFC051CF185B71199608D /* CommandNotch.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = CommandNotch.app; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||||
7B598809B19C892470DE7268 /* TerminalSession.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TerminalSession.swift; sourceTree = "<group>"; };
|
7B598809B19C892470DE7268 /* TerminalSession.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TerminalSession.swift; sourceTree = "<group>"; };
|
||||||
9547A79F60E46F4521A70674 /* Downterm.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Downterm.entitlements; sourceTree = "<group>"; };
|
3A1F0C4AE9D84A5C8E2B7101 /* TerminalTheme.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TerminalTheme.swift; sourceTree = "<group>"; };
|
||||||
|
9547A79F60E46F4521A70674 /* CommandNotch.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = CommandNotch.entitlements; sourceTree = "<group>"; };
|
||||||
AA6359CF9DDF89413440300D /* NotchSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotchSettings.swift; sourceTree = "<group>"; };
|
AA6359CF9DDF89413440300D /* NotchSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotchSettings.swift; sourceTree = "<group>"; };
|
||||||
BA6843B571B41986DE386F5F /* TerminalManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TerminalManager.swift; sourceTree = "<group>"; };
|
BA6843B571B41986DE386F5F /* TerminalManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TerminalManager.swift; sourceTree = "<group>"; };
|
||||||
C5CB3313B230019D0E988AFE /* SettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsView.swift; sourceTree = "<group>"; };
|
C5CB3313B230019D0E988AFE /* SettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsView.swift; sourceTree = "<group>"; };
|
||||||
@@ -72,7 +76,8 @@
|
|||||||
0EF94ED46B4860C241540F0A /* Resources */ = {
|
0EF94ED46B4860C241540F0A /* Resources */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
9547A79F60E46F4521A70674 /* Downterm.entitlements */,
|
0F4A88A33D93B6E100A1C002 /* Assets.xcassets */,
|
||||||
|
9547A79F60E46F4521A70674 /* CommandNotch.entitlements */,
|
||||||
);
|
);
|
||||||
path = Resources;
|
path = Resources;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
@@ -92,7 +97,7 @@
|
|||||||
792DD4F8C079680683D8FF7A /* Products */ = {
|
792DD4F8C079680683D8FF7A /* Products */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
665CFC051CF185B71199608D /* Downterm.app */,
|
665CFC051CF185B71199608D /* CommandNotch.app */,
|
||||||
);
|
);
|
||||||
name = Products;
|
name = Products;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
@@ -106,6 +111,7 @@
|
|||||||
589421631401C819FE1A7BA9 /* NotchViewModel.swift */,
|
589421631401C819FE1A7BA9 /* NotchViewModel.swift */,
|
||||||
BA6843B571B41986DE386F5F /* TerminalManager.swift */,
|
BA6843B571B41986DE386F5F /* TerminalManager.swift */,
|
||||||
7B598809B19C892470DE7268 /* TerminalSession.swift */,
|
7B598809B19C892470DE7268 /* TerminalSession.swift */,
|
||||||
|
3A1F0C4AE9D84A5C8E2B7101 /* TerminalTheme.swift */,
|
||||||
);
|
);
|
||||||
path = Models;
|
path = Models;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
@@ -118,12 +124,12 @@
|
|||||||
path = Extensions;
|
path = Extensions;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
};
|
};
|
||||||
9E1CA4816F67033BBD52D8A3 /* Downterm */ = {
|
9E1CA4816F67033BBD52D8A3 /* CommandNotch */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
5C0779490DE9020FBBC464BE /* AppDelegate.swift */,
|
5C0779490DE9020FBBC464BE /* AppDelegate.swift */,
|
||||||
20BA7F4716DA3909DA8BC381 /* ContentView.swift */,
|
20BA7F4716DA3909DA8BC381 /* ContentView.swift */,
|
||||||
4B671125208055E5334CB85E /* DowntermApp.swift */,
|
4B671125208055E5334CB85E /* CommandNotchApp.swift */,
|
||||||
F32F526005A2589010E63C76 /* Components */,
|
F32F526005A2589010E63C76 /* Components */,
|
||||||
8D95E0324E6AFC9E4DC0C087 /* Extensions */,
|
8D95E0324E6AFC9E4DC0C087 /* Extensions */,
|
||||||
27C90448ECAC906F0DA429C0 /* Managers */,
|
27C90448ECAC906F0DA429C0 /* Managers */,
|
||||||
@@ -131,7 +137,7 @@
|
|||||||
0EF94ED46B4860C241540F0A /* Resources */,
|
0EF94ED46B4860C241540F0A /* Resources */,
|
||||||
C2B8955F4D0A1DAA7E60326A /* Views */,
|
C2B8955F4D0A1DAA7E60326A /* Views */,
|
||||||
);
|
);
|
||||||
path = Downterm;
|
path = CommandNotch;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
};
|
};
|
||||||
C2B8955F4D0A1DAA7E60326A /* Views */ = {
|
C2B8955F4D0A1DAA7E60326A /* Views */ = {
|
||||||
@@ -157,7 +163,7 @@
|
|||||||
FC6F23514BFE2235BD4154E8 = {
|
FC6F23514BFE2235BD4154E8 = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
9E1CA4816F67033BBD52D8A3 /* Downterm */,
|
9E1CA4816F67033BBD52D8A3 /* CommandNotch */,
|
||||||
792DD4F8C079680683D8FF7A /* Products */,
|
792DD4F8C079680683D8FF7A /* Products */,
|
||||||
);
|
);
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
@@ -165,23 +171,24 @@
|
|||||||
/* End PBXGroup section */
|
/* End PBXGroup section */
|
||||||
|
|
||||||
/* Begin PBXNativeTarget section */
|
/* Begin PBXNativeTarget section */
|
||||||
1485207FA11756EC2DF4F08B /* Downterm */ = {
|
1485207FA11756EC2DF4F08B /* CommandNotch */ = {
|
||||||
isa = PBXNativeTarget;
|
isa = PBXNativeTarget;
|
||||||
buildConfigurationList = 74CB98309F5464CDCB00C63A /* Build configuration list for PBXNativeTarget "Downterm" */;
|
buildConfigurationList = 74CB98309F5464CDCB00C63A /* Build configuration list for PBXNativeTarget "CommandNotch" */;
|
||||||
buildPhases = (
|
buildPhases = (
|
||||||
F3C6D5CD1247D246A3F6F7AB /* Sources */,
|
F3C6D5CD1247D246A3F6F7AB /* Sources */,
|
||||||
6085DF2BDFFB2A99C4ABD514 /* Frameworks */,
|
6085DF2BDFFB2A99C4ABD514 /* Frameworks */,
|
||||||
|
0F4A88A33D93B6E100A1C003 /* Resources */,
|
||||||
);
|
);
|
||||||
buildRules = (
|
buildRules = (
|
||||||
);
|
);
|
||||||
dependencies = (
|
dependencies = (
|
||||||
);
|
);
|
||||||
name = Downterm;
|
name = CommandNotch;
|
||||||
packageProductDependencies = (
|
packageProductDependencies = (
|
||||||
032AECA58EA4C274BE9F3320 /* SwiftTerm */,
|
032AECA58EA4C274BE9F3320 /* SwiftTerm */,
|
||||||
);
|
);
|
||||||
productName = Downterm;
|
productName = CommandNotch;
|
||||||
productReference = 665CFC051CF185B71199608D /* Downterm.app */;
|
productReference = 665CFC051CF185B71199608D /* CommandNotch.app */;
|
||||||
productType = "com.apple.product-type.application";
|
productType = "com.apple.product-type.application";
|
||||||
};
|
};
|
||||||
/* End PBXNativeTarget section */
|
/* End PBXNativeTarget section */
|
||||||
@@ -193,7 +200,7 @@
|
|||||||
BuildIndependentTargetsInParallel = YES;
|
BuildIndependentTargetsInParallel = YES;
|
||||||
LastUpgradeCheck = 1600;
|
LastUpgradeCheck = 1600;
|
||||||
};
|
};
|
||||||
buildConfigurationList = D1C4019FEAFC83BB053C9E6E /* Build configuration list for PBXProject "Downterm" */;
|
buildConfigurationList = D1C4019FEAFC83BB053C9E6E /* Build configuration list for PBXProject "CommandNotch" */;
|
||||||
compatibilityVersion = "Xcode 14.0";
|
compatibilityVersion = "Xcode 14.0";
|
||||||
developmentRegion = en;
|
developmentRegion = en;
|
||||||
hasScannedForEncodings = 0;
|
hasScannedForEncodings = 0;
|
||||||
@@ -209,11 +216,22 @@
|
|||||||
projectDirPath = "";
|
projectDirPath = "";
|
||||||
projectRoot = "";
|
projectRoot = "";
|
||||||
targets = (
|
targets = (
|
||||||
1485207FA11756EC2DF4F08B /* Downterm */,
|
1485207FA11756EC2DF4F08B /* CommandNotch */,
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
/* End PBXProject section */
|
/* End PBXProject section */
|
||||||
|
|
||||||
|
/* Begin PBXResourcesBuildPhase section */
|
||||||
|
0F4A88A33D93B6E100A1C003 /* Resources */ = {
|
||||||
|
isa = PBXResourcesBuildPhase;
|
||||||
|
buildActionMask = 2147483647;
|
||||||
|
files = (
|
||||||
|
0F4A88A33D93B6E100A1C001 /* Assets.xcassets in Resources */,
|
||||||
|
);
|
||||||
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
};
|
||||||
|
/* End PBXResourcesBuildPhase section */
|
||||||
|
|
||||||
/* Begin PBXSourcesBuildPhase section */
|
/* Begin PBXSourcesBuildPhase section */
|
||||||
F3C6D5CD1247D246A3F6F7AB /* Sources */ = {
|
F3C6D5CD1247D246A3F6F7AB /* Sources */ = {
|
||||||
isa = PBXSourcesBuildPhase;
|
isa = PBXSourcesBuildPhase;
|
||||||
@@ -221,7 +239,7 @@
|
|||||||
files = (
|
files = (
|
||||||
81A912E3E16165D999882078 /* AppDelegate.swift in Sources */,
|
81A912E3E16165D999882078 /* AppDelegate.swift in Sources */,
|
||||||
888C45C650327089EBD39B2E /* ContentView.swift in Sources */,
|
888C45C650327089EBD39B2E /* ContentView.swift in Sources */,
|
||||||
247C6F84E7ADE7AED43381E2 /* DowntermApp.swift in Sources */,
|
247C6F84E7ADE7AED43381E2 /* CommandNotchApp.swift in Sources */,
|
||||||
88EBFBB2292659EA7C42A8F9 /* HotkeyBinding.swift in Sources */,
|
88EBFBB2292659EA7C42A8F9 /* HotkeyBinding.swift in Sources */,
|
||||||
4D5125E11B4DDBDB3DFACBAF /* HotkeyManager.swift in Sources */,
|
4D5125E11B4DDBDB3DFACBAF /* HotkeyManager.swift in Sources */,
|
||||||
7BD705CA6A34117929B362EC /* HotkeyRecorderView.swift in Sources */,
|
7BD705CA6A34117929B362EC /* HotkeyRecorderView.swift in Sources */,
|
||||||
@@ -240,6 +258,7 @@
|
|||||||
7EA51C3720BED7E6189E057D /* TabBar.swift in Sources */,
|
7EA51C3720BED7E6189E057D /* TabBar.swift in Sources */,
|
||||||
E9A064422790735E033E534F /* TerminalManager.swift in Sources */,
|
E9A064422790735E033E534F /* TerminalManager.swift in Sources */,
|
||||||
37FC0A7CEEA37C9DCC6A8351 /* TerminalSession.swift in Sources */,
|
37FC0A7CEEA37C9DCC6A8351 /* TerminalSession.swift in Sources */,
|
||||||
|
3A1F0C4BE9D84A5C8E2B7101 /* TerminalTheme.swift in Sources */,
|
||||||
);
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
};
|
};
|
||||||
@@ -250,18 +269,21 @@
|
|||||||
isa = XCBuildConfiguration;
|
isa = XCBuildConfiguration;
|
||||||
buildSettings = {
|
buildSettings = {
|
||||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||||
|
ASSETCATALOG_COMPILER_INCLUDE_ALL_APPICON_ASSETS = YES;
|
||||||
CLANG_USE_OPTIMIZATION_PROFILE = YES;
|
CLANG_USE_OPTIMIZATION_PROFILE = YES;
|
||||||
CODE_SIGN_ENTITLEMENTS = Downterm/Resources/Downterm.entitlements;
|
CODE_SIGN_ENTITLEMENTS = CommandNotch/Resources/CommandNotch.entitlements;
|
||||||
"CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development";
|
"CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development";
|
||||||
COMBINE_HIDPI_IMAGES = YES;
|
COMBINE_HIDPI_IMAGES = YES;
|
||||||
"DEVELOPMENT_TEAM[sdk=macosx*]" = G698BP272N;
|
"DEVELOPMENT_TEAM[sdk=macosx*]" = G698BP272N;
|
||||||
INFOPLIST_FILE = Downterm/Resources/Info.plist;
|
INFOPLIST_FILE = CommandNotch/Resources/Info.plist;
|
||||||
|
INFOPLIST_KEY_CFBundleDisplayName = CommandNotch;
|
||||||
LD_RUNPATH_SEARCH_PATHS = (
|
LD_RUNPATH_SEARCH_PATHS = (
|
||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
"@executable_path/../Frameworks",
|
"@executable_path/../Frameworks",
|
||||||
);
|
);
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = com.downterm.app;
|
MARKETING_VERSION = 0.0.3;
|
||||||
PRODUCT_NAME = Downterm;
|
PRODUCT_BUNDLE_IDENTIFIER = com.commandnotch.app;
|
||||||
|
PRODUCT_NAME = CommandNotch;
|
||||||
SDKROOT = macosx;
|
SDKROOT = macosx;
|
||||||
};
|
};
|
||||||
name = Release;
|
name = Release;
|
||||||
@@ -334,18 +356,21 @@
|
|||||||
isa = XCBuildConfiguration;
|
isa = XCBuildConfiguration;
|
||||||
buildSettings = {
|
buildSettings = {
|
||||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||||
|
ASSETCATALOG_COMPILER_INCLUDE_ALL_APPICON_ASSETS = YES;
|
||||||
CLANG_USE_OPTIMIZATION_PROFILE = YES;
|
CLANG_USE_OPTIMIZATION_PROFILE = YES;
|
||||||
CODE_SIGN_ENTITLEMENTS = Downterm/Resources/Downterm.entitlements;
|
CODE_SIGN_ENTITLEMENTS = CommandNotch/Resources/CommandNotch.entitlements;
|
||||||
"CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development";
|
"CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development";
|
||||||
COMBINE_HIDPI_IMAGES = YES;
|
COMBINE_HIDPI_IMAGES = YES;
|
||||||
"DEVELOPMENT_TEAM[sdk=macosx*]" = G698BP272N;
|
"DEVELOPMENT_TEAM[sdk=macosx*]" = G698BP272N;
|
||||||
INFOPLIST_FILE = Downterm/Resources/Info.plist;
|
INFOPLIST_FILE = CommandNotch/Resources/Info.plist;
|
||||||
|
INFOPLIST_KEY_CFBundleDisplayName = CommandNotch;
|
||||||
LD_RUNPATH_SEARCH_PATHS = (
|
LD_RUNPATH_SEARCH_PATHS = (
|
||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
"@executable_path/../Frameworks",
|
"@executable_path/../Frameworks",
|
||||||
);
|
);
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = com.downterm.app;
|
MARKETING_VERSION = 0.0.3;
|
||||||
PRODUCT_NAME = Downterm;
|
PRODUCT_BUNDLE_IDENTIFIER = com.commandnotch.app;
|
||||||
|
PRODUCT_NAME = CommandNotch;
|
||||||
SDKROOT = macosx;
|
SDKROOT = macosx;
|
||||||
};
|
};
|
||||||
name = Debug;
|
name = Debug;
|
||||||
@@ -410,7 +435,7 @@
|
|||||||
/* End XCBuildConfiguration section */
|
/* End XCBuildConfiguration section */
|
||||||
|
|
||||||
/* Begin XCConfigurationList section */
|
/* Begin XCConfigurationList section */
|
||||||
74CB98309F5464CDCB00C63A /* Build configuration list for PBXNativeTarget "Downterm" */ = {
|
74CB98309F5464CDCB00C63A /* Build configuration list for PBXNativeTarget "CommandNotch" */ = {
|
||||||
isa = XCConfigurationList;
|
isa = XCConfigurationList;
|
||||||
buildConfigurations = (
|
buildConfigurations = (
|
||||||
7020C02C1BDF63690CC9A3AC /* Debug */,
|
7020C02C1BDF63690CC9A3AC /* Debug */,
|
||||||
@@ -419,7 +444,7 @@
|
|||||||
defaultConfigurationIsVisible = 0;
|
defaultConfigurationIsVisible = 0;
|
||||||
defaultConfigurationName = Debug;
|
defaultConfigurationName = Debug;
|
||||||
};
|
};
|
||||||
D1C4019FEAFC83BB053C9E6E /* Build configuration list for PBXProject "Downterm" */ = {
|
D1C4019FEAFC83BB053C9E6E /* Build configuration list for PBXProject "CommandNotch" */ = {
|
||||||
isa = XCConfigurationList;
|
isa = XCConfigurationList;
|
||||||
buildConfigurations = (
|
buildConfigurations = (
|
||||||
3595A9212275B9AEC4448C64 /* Debug */,
|
3595A9212275B9AEC4448C64 /* Debug */,
|
||||||
7
Downterm/CommandNotch.xcodeproj/project.xcworkspace/contents.xcworkspacedata
generated
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<Workspace
|
||||||
|
version = "1.0">
|
||||||
|
<FileRef
|
||||||
|
location = "self:">
|
||||||
|
</FileRef>
|
||||||
|
</Workspace>
|
||||||
@@ -16,9 +16,9 @@
|
|||||||
<BuildableReference
|
<BuildableReference
|
||||||
BuildableIdentifier = "primary"
|
BuildableIdentifier = "primary"
|
||||||
BlueprintIdentifier = "1485207FA11756EC2DF4F08B"
|
BlueprintIdentifier = "1485207FA11756EC2DF4F08B"
|
||||||
BuildableName = "Downterm.app"
|
BuildableName = "CommandNotch.app"
|
||||||
BlueprintName = "Downterm"
|
BlueprintName = "CommandNotch"
|
||||||
ReferencedContainer = "container:Downterm.xcodeproj">
|
ReferencedContainer = "container:CommandNotch.xcodeproj">
|
||||||
</BuildableReference>
|
</BuildableReference>
|
||||||
</BuildActionEntry>
|
</BuildActionEntry>
|
||||||
</BuildActionEntries>
|
</BuildActionEntries>
|
||||||
@@ -45,9 +45,9 @@
|
|||||||
<BuildableReference
|
<BuildableReference
|
||||||
BuildableIdentifier = "primary"
|
BuildableIdentifier = "primary"
|
||||||
BlueprintIdentifier = "1485207FA11756EC2DF4F08B"
|
BlueprintIdentifier = "1485207FA11756EC2DF4F08B"
|
||||||
BuildableName = "Downterm.app"
|
BuildableName = "CommandNotch.app"
|
||||||
BlueprintName = "Downterm"
|
BlueprintName = "CommandNotch"
|
||||||
ReferencedContainer = "container:Downterm.xcodeproj">
|
ReferencedContainer = "container:CommandNotch.xcodeproj">
|
||||||
</BuildableReference>
|
</BuildableReference>
|
||||||
</BuildableProductRunnable>
|
</BuildableProductRunnable>
|
||||||
</LaunchAction>
|
</LaunchAction>
|
||||||
@@ -62,9 +62,9 @@
|
|||||||
<BuildableReference
|
<BuildableReference
|
||||||
BuildableIdentifier = "primary"
|
BuildableIdentifier = "primary"
|
||||||
BlueprintIdentifier = "1485207FA11756EC2DF4F08B"
|
BlueprintIdentifier = "1485207FA11756EC2DF4F08B"
|
||||||
BuildableName = "Downterm.app"
|
BuildableName = "CommandNotch.app"
|
||||||
BlueprintName = "Downterm"
|
BlueprintName = "CommandNotch"
|
||||||
ReferencedContainer = "container:Downterm.xcodeproj">
|
ReferencedContainer = "container:CommandNotch.xcodeproj">
|
||||||
</BuildableReference>
|
</BuildableReference>
|
||||||
</BuildableProductRunnable>
|
</BuildableProductRunnable>
|
||||||
</ProfileAction>
|
</ProfileAction>
|
||||||
@@ -16,9 +16,9 @@
|
|||||||
<BuildableReference
|
<BuildableReference
|
||||||
BuildableIdentifier = "primary"
|
BuildableIdentifier = "primary"
|
||||||
BlueprintIdentifier = "1485207FA11756EC2DF4F08B"
|
BlueprintIdentifier = "1485207FA11756EC2DF4F08B"
|
||||||
BuildableName = "Downterm.app"
|
BuildableName = "CommandNotch.app"
|
||||||
BlueprintName = "Downterm"
|
BlueprintName = "CommandNotch"
|
||||||
ReferencedContainer = "container:Downterm.xcodeproj">
|
ReferencedContainer = "container:CommandNotch.xcodeproj">
|
||||||
</BuildableReference>
|
</BuildableReference>
|
||||||
</BuildActionEntry>
|
</BuildActionEntry>
|
||||||
</BuildActionEntries>
|
</BuildActionEntries>
|
||||||
@@ -45,9 +45,9 @@
|
|||||||
<BuildableReference
|
<BuildableReference
|
||||||
BuildableIdentifier = "primary"
|
BuildableIdentifier = "primary"
|
||||||
BlueprintIdentifier = "1485207FA11756EC2DF4F08B"
|
BlueprintIdentifier = "1485207FA11756EC2DF4F08B"
|
||||||
BuildableName = "Downterm.app"
|
BuildableName = "CommandNotch.app"
|
||||||
BlueprintName = "Downterm"
|
BlueprintName = "CommandNotch"
|
||||||
ReferencedContainer = "container:Downterm.xcodeproj">
|
ReferencedContainer = "container:CommandNotch.xcodeproj">
|
||||||
</BuildableReference>
|
</BuildableReference>
|
||||||
</BuildableProductRunnable>
|
</BuildableProductRunnable>
|
||||||
</LaunchAction>
|
</LaunchAction>
|
||||||
@@ -62,9 +62,9 @@
|
|||||||
<BuildableReference
|
<BuildableReference
|
||||||
BuildableIdentifier = "primary"
|
BuildableIdentifier = "primary"
|
||||||
BlueprintIdentifier = "1485207FA11756EC2DF4F08B"
|
BlueprintIdentifier = "1485207FA11756EC2DF4F08B"
|
||||||
BuildableName = "Downterm.app"
|
BuildableName = "CommandNotch.app"
|
||||||
BlueprintName = "Downterm"
|
BlueprintName = "CommandNotch"
|
||||||
ReferencedContainer = "container:Downterm.xcodeproj">
|
ReferencedContainer = "container:CommandNotch.xcodeproj">
|
||||||
</BuildableReference>
|
</BuildableReference>
|
||||||
</BuildableProductRunnable>
|
</BuildableProductRunnable>
|
||||||
</ProfileAction>
|
</ProfileAction>
|
||||||
@@ -4,12 +4,12 @@
|
|||||||
<dict>
|
<dict>
|
||||||
<key>SchemeUserState</key>
|
<key>SchemeUserState</key>
|
||||||
<dict>
|
<dict>
|
||||||
<key>Downterm.xcscheme_^#shared#^_</key>
|
<key>CommandNotch.xcscheme_^#shared#^_</key>
|
||||||
<dict>
|
<dict>
|
||||||
<key>orderHint</key>
|
<key>orderHint</key>
|
||||||
<integer>0</integer>
|
<integer>0</integer>
|
||||||
</dict>
|
</dict>
|
||||||
<key>Release-Downterm.xcscheme_^#shared#^_</key>
|
<key>Release-CommandNotch.xcscheme_^#shared#^_</key>
|
||||||
<dict>
|
<dict>
|
||||||
<key>orderHint</key>
|
<key>orderHint</key>
|
||||||
<integer>1</integer>
|
<integer>1</integer>
|
||||||
@@ -19,6 +19,7 @@ class AppDelegate: NSObject, NSApplicationDelegate {
|
|||||||
observeDisplayPreference()
|
observeDisplayPreference()
|
||||||
observeSizePreferences()
|
observeSizePreferences()
|
||||||
observeFontSizeChanges()
|
observeFontSizeChanges()
|
||||||
|
observeTerminalThemeChanges()
|
||||||
}
|
}
|
||||||
|
|
||||||
func applicationWillTerminate(_ notification: Notification) {
|
func applicationWillTerminate(_ notification: Notification) {
|
||||||
@@ -58,6 +59,16 @@ class AppDelegate: NSObject, NSApplicationDelegate {
|
|||||||
}
|
}
|
||||||
.store(in: &cancellables)
|
.store(in: &cancellables)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Live-update terminal colors across all sessions.
|
||||||
|
private func observeTerminalThemeChanges() {
|
||||||
|
UserDefaults.standard.publisher(for: \.terminalTheme)
|
||||||
|
.removeDuplicates()
|
||||||
|
.sink { newTheme in
|
||||||
|
TerminalManager.shared.updateAllThemes(TerminalTheme.resolve(newTheme))
|
||||||
|
}
|
||||||
|
.store(in: &cancellables)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - KVO key paths
|
// MARK: - KVO key paths
|
||||||
@@ -67,6 +78,10 @@ private extension UserDefaults {
|
|||||||
double(forKey: NotchSettings.Keys.terminalFontSize)
|
double(forKey: NotchSettings.Keys.terminalFontSize)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@objc var terminalTheme: String {
|
||||||
|
string(forKey: NotchSettings.Keys.terminalTheme) ?? NotchSettings.Defaults.terminalTheme
|
||||||
|
}
|
||||||
|
|
||||||
@objc var showOnAllDisplays: Bool {
|
@objc var showOnAllDisplays: Bool {
|
||||||
bool(forKey: NotchSettings.Keys.showOnAllDisplays)
|
bool(forKey: NotchSettings.Keys.showOnAllDisplays)
|
||||||
}
|
}
|
||||||
@@ -1,10 +1,10 @@
|
|||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
/// Main entry point for the Downterm application.
|
/// Main entry point for the CommandNotch application.
|
||||||
/// Provides a MenuBarExtra for quick access to settings and app controls.
|
/// Provides a MenuBarExtra for quick access to settings and app controls.
|
||||||
/// The notch windows and terminal sessions are managed by AppDelegate + ScreenManager.
|
/// The notch windows and terminal sessions are managed by AppDelegate + ScreenManager.
|
||||||
@main
|
@main
|
||||||
struct DowntermApp: App {
|
struct CommandNotchApp: App {
|
||||||
|
|
||||||
@NSApplicationDelegateAdaptor(AppDelegate.self) var appDelegate
|
@NSApplicationDelegateAdaptor(AppDelegate.self) var appDelegate
|
||||||
|
|
||||||
@@ -12,7 +12,7 @@ struct DowntermApp: App {
|
|||||||
private var showMenuBarIcon = NotchSettings.Defaults.showMenuBarIcon
|
private var showMenuBarIcon = NotchSettings.Defaults.showMenuBarIcon
|
||||||
|
|
||||||
var body: some Scene {
|
var body: some Scene {
|
||||||
MenuBarExtra("Downterm", systemImage: "terminal", isInserted: $showMenuBarIcon) {
|
MenuBarExtra("CommandNotch", systemImage: "terminal", isInserted: $showMenuBarIcon) {
|
||||||
Button("Toggle Notch") {
|
Button("Toggle Notch") {
|
||||||
ScreenManager.shared.toggleNotchOnActiveScreen()
|
ScreenManager.shared.toggleNotchOnActiveScreen()
|
||||||
}
|
}
|
||||||
@@ -27,7 +27,7 @@ struct DowntermApp: App {
|
|||||||
|
|
||||||
Divider()
|
Divider()
|
||||||
|
|
||||||
Button("Quit Downterm") {
|
Button("Quit CommandNotch") {
|
||||||
NSApplication.shared.terminate(nil)
|
NSApplication.shared.terminate(nil)
|
||||||
}
|
}
|
||||||
.keyboardShortcut("Q", modifiers: .command)
|
.keyboardShortcut("Q", modifiers: .command)
|
||||||
@@ -28,6 +28,36 @@ struct HotkeyRecorderView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
struct OptionalHotkeyRecorderView: View {
|
||||||
|
let label: String
|
||||||
|
@Binding var binding: HotkeyBinding?
|
||||||
|
|
||||||
|
@State private var isRecording = false
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
HStack {
|
||||||
|
Text(label)
|
||||||
|
.frame(width: 140, alignment: .leading)
|
||||||
|
|
||||||
|
OptionalHotkeyRecorderField(binding: $binding, isRecording: $isRecording)
|
||||||
|
.frame(width: 120, height: 24)
|
||||||
|
.background(
|
||||||
|
RoundedRectangle(cornerRadius: 6)
|
||||||
|
.fill(isRecording ? Color.accentColor.opacity(0.15) : Color.secondary.opacity(0.1))
|
||||||
|
)
|
||||||
|
.overlay(
|
||||||
|
RoundedRectangle(cornerRadius: 6)
|
||||||
|
.stroke(isRecording ? Color.accentColor : Color.secondary.opacity(0.3), lineWidth: 1)
|
||||||
|
)
|
||||||
|
|
||||||
|
Button("Clear") {
|
||||||
|
binding = nil
|
||||||
|
}
|
||||||
|
.disabled(binding == nil)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// NSViewRepresentable that captures key events when focused.
|
/// NSViewRepresentable that captures key events when focused.
|
||||||
struct HotkeyRecorderField: NSViewRepresentable {
|
struct HotkeyRecorderField: NSViewRepresentable {
|
||||||
@Binding var binding: HotkeyBinding
|
@Binding var binding: HotkeyBinding
|
||||||
@@ -52,6 +82,29 @@ struct HotkeyRecorderField: NSViewRepresentable {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
struct OptionalHotkeyRecorderField: NSViewRepresentable {
|
||||||
|
@Binding var binding: HotkeyBinding?
|
||||||
|
@Binding var isRecording: Bool
|
||||||
|
|
||||||
|
func makeNSView(context: Context) -> HotkeyNSView {
|
||||||
|
let view = HotkeyNSView()
|
||||||
|
view.onKeyRecorded = { newBinding in
|
||||||
|
binding = newBinding
|
||||||
|
isRecording = false
|
||||||
|
}
|
||||||
|
view.onFocusChanged = { focused in
|
||||||
|
isRecording = focused
|
||||||
|
}
|
||||||
|
return view
|
||||||
|
}
|
||||||
|
|
||||||
|
func updateNSView(_ nsView: HotkeyNSView, context: Context) {
|
||||||
|
nsView.currentLabel = binding?.displayString ?? "Not set"
|
||||||
|
nsView.showRecording = isRecording
|
||||||
|
nsView.needsDisplay = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// The actual NSView that handles key capture.
|
/// The actual NSView that handles key capture.
|
||||||
class HotkeyNSView: NSView {
|
class HotkeyNSView: NSView {
|
||||||
var currentLabel: String = ""
|
var currentLabel: String = ""
|
||||||
@@ -59,21 +112,32 @@ class HotkeyNSView: NSView {
|
|||||||
var onKeyRecorded: ((HotkeyBinding) -> Void)?
|
var onKeyRecorded: ((HotkeyBinding) -> Void)?
|
||||||
var onFocusChanged: ((Bool) -> Void)?
|
var onFocusChanged: ((Bool) -> Void)?
|
||||||
|
|
||||||
|
private let label: NSTextField = {
|
||||||
|
let field = NSTextField(labelWithString: "")
|
||||||
|
field.alignment = .center
|
||||||
|
field.font = NSFont.monospacedSystemFont(ofSize: 12, weight: .medium)
|
||||||
|
field.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
field.backgroundColor = .clear
|
||||||
|
field.isBezeled = false
|
||||||
|
field.lineBreakMode = .byTruncatingTail
|
||||||
|
return field
|
||||||
|
}()
|
||||||
|
|
||||||
|
override init(frame frameRect: NSRect) {
|
||||||
|
super.init(frame: frameRect)
|
||||||
|
setupLabel()
|
||||||
|
}
|
||||||
|
|
||||||
|
required init?(coder: NSCoder) {
|
||||||
|
super.init(coder: coder)
|
||||||
|
setupLabel()
|
||||||
|
}
|
||||||
|
|
||||||
override var acceptsFirstResponder: Bool { true }
|
override var acceptsFirstResponder: Bool { true }
|
||||||
|
|
||||||
override func draw(_ dirtyRect: NSRect) {
|
override func layout() {
|
||||||
let text = showRecording ? "Press keys…" : currentLabel
|
super.layout()
|
||||||
let attrs: [NSAttributedString.Key: Any] = [
|
updateLabelAppearance()
|
||||||
.font: NSFont.monospacedSystemFont(ofSize: 12, weight: .medium),
|
|
||||||
.foregroundColor: showRecording ? NSColor.controlAccentColor : NSColor.labelColor
|
|
||||||
]
|
|
||||||
let str = NSAttributedString(string: text, attributes: attrs)
|
|
||||||
let size = str.size()
|
|
||||||
let point = NSPoint(
|
|
||||||
x: (bounds.width - size.width) / 2,
|
|
||||||
y: (bounds.height - size.height) / 2
|
|
||||||
)
|
|
||||||
str.draw(at: point)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override func mouseDown(with event: NSEvent) {
|
override func mouseDown(with event: NSEvent) {
|
||||||
@@ -108,4 +172,19 @@ class HotkeyNSView: NSView {
|
|||||||
// Resign first responder after recording
|
// Resign first responder after recording
|
||||||
window?.makeFirstResponder(nil)
|
window?.makeFirstResponder(nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private func setupLabel() {
|
||||||
|
addSubview(label)
|
||||||
|
NSLayoutConstraint.activate([
|
||||||
|
label.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 6),
|
||||||
|
label.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -6),
|
||||||
|
label.centerYAnchor.constraint(equalTo: centerYAnchor)
|
||||||
|
])
|
||||||
|
updateLabelAppearance()
|
||||||
|
}
|
||||||
|
|
||||||
|
private func updateLabelAppearance() {
|
||||||
|
label.stringValue = showRecording ? "Press keys..." : currentLabel
|
||||||
|
label.textColor = showRecording ? .controlAccentColor : .labelColor
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -5,13 +5,13 @@ import SwiftUI
|
|||||||
/// between the compact closed state and the expanded open state.
|
/// between the compact closed state and the expanded open state.
|
||||||
///
|
///
|
||||||
/// The shape uses quadratic Bezier curves to produce the distinctive
|
/// The shape uses quadratic Bezier curves to produce the distinctive
|
||||||
/// "ear" ramps on each side when closed, and a clean rounded-bottom
|
/// top-edge cut-ins of the closed notch, and a clean rounded-bottom
|
||||||
/// rectangle when open (topCornerRadius approaches 0).
|
/// rectangle when open (topCornerRadius approaches 0).
|
||||||
struct NotchShape: Shape {
|
struct NotchShape: Shape {
|
||||||
|
|
||||||
/// Radius applied to the top-left and top-right outer corners (the "ears").
|
/// Radius applied to the top-left and top-right transitions where the notch
|
||||||
/// When close to 0, the top corners become sharp and the shape is a
|
/// curves away from the screen edge. When close to 0, the top corners become
|
||||||
/// rectangle with rounded bottom corners — no visible ear ramps.
|
/// sharp and the shape is a rectangle with rounded bottom corners.
|
||||||
var topCornerRadius: CGFloat
|
var topCornerRadius: CGFloat
|
||||||
|
|
||||||
/// Radius applied to the bottom-left and bottom-right inner corners.
|
/// Radius applied to the bottom-left and bottom-right inner corners.
|
||||||
@@ -46,10 +46,10 @@ struct NotchShape: Shape {
|
|||||||
path.move(to: CGPoint(x: minX, y: minY))
|
path.move(to: CGPoint(x: minX, y: minY))
|
||||||
|
|
||||||
if topR > 0.5 {
|
if topR > 0.5 {
|
||||||
// Top-left ear: curve down from the top edge
|
// Leave the screen edge horizontally, then turn into the side wall.
|
||||||
path.addQuadCurve(
|
path.addQuadCurve(
|
||||||
to: CGPoint(x: minX + topR, y: minY + topR),
|
to: CGPoint(x: minX + topR, y: minY + topR),
|
||||||
control: CGPoint(x: minX, y: minY + topR)
|
control: CGPoint(x: minX + topR, y: minY)
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
path.addLine(to: CGPoint(x: minX, y: minY))
|
path.addLine(to: CGPoint(x: minX, y: minY))
|
||||||
@@ -73,14 +73,14 @@ struct NotchShape: Shape {
|
|||||||
control: CGPoint(x: maxX - topR, y: maxY)
|
control: CGPoint(x: maxX - topR, y: maxY)
|
||||||
)
|
)
|
||||||
|
|
||||||
// Right edge up to top-right ear area
|
// Right edge up to the top-right transition
|
||||||
path.addLine(to: CGPoint(x: maxX - topR, y: minY + topR))
|
path.addLine(to: CGPoint(x: maxX - topR, y: minY + topR))
|
||||||
|
|
||||||
if topR > 0.5 {
|
if topR > 0.5 {
|
||||||
// Top-right ear: curve back up to the top edge
|
// Mirror the top-left transition.
|
||||||
path.addQuadCurve(
|
path.addQuadCurve(
|
||||||
to: CGPoint(x: maxX, y: minY),
|
to: CGPoint(x: maxX, y: minY),
|
||||||
control: CGPoint(x: maxX, y: minY + topR)
|
control: CGPoint(x: maxX - topR, y: minY)
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
path.addLine(to: CGPoint(x: maxX, y: minY))
|
path.addLine(to: CGPoint(x: maxX, y: minY))
|
||||||
@@ -100,8 +100,8 @@ extension NotchShape {
|
|||||||
NotchShape(topCornerRadius: 6, bottomCornerRadius: 14)
|
NotchShape(topCornerRadius: 6, bottomCornerRadius: 14)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Open-state shape: no ear ramps, just rounded bottom corners.
|
/// Open-state shape: no top-edge cut-ins, just rounded bottom corners.
|
||||||
/// topCornerRadius is near-zero so the ears disappear and the panel
|
/// topCornerRadius is near-zero so the top becomes effectively flat and the panel
|
||||||
/// extends flush to the top edge of the screen.
|
/// extends flush to the top edge of the screen.
|
||||||
static var opened: NotchShape {
|
static var opened: NotchShape {
|
||||||
NotchShape(topCornerRadius: 0, bottomCornerRadius: 24)
|
NotchShape(topCornerRadius: 0, bottomCornerRadius: 24)
|
||||||
@@ -28,6 +28,8 @@ struct ContentView: View {
|
|||||||
@AppStorage(NotchSettings.Keys.hoverSpringDamping) private var hoverSpringDamping = NotchSettings.Defaults.hoverSpringDamping
|
@AppStorage(NotchSettings.Keys.hoverSpringDamping) private var hoverSpringDamping = NotchSettings.Defaults.hoverSpringDamping
|
||||||
|
|
||||||
@State private var hoverTask: Task<Void, Never>?
|
@State private var hoverTask: Task<Void, Never>?
|
||||||
|
@State private var resizeStartSize: CGSize?
|
||||||
|
@State private var resizeStartMouseLocation: CGPoint?
|
||||||
|
|
||||||
private var hoverAnimation: Animation {
|
private var hoverAnimation: Animation {
|
||||||
.interactiveSpring(response: hoverSpringResponse, dampingFraction: hoverSpringDamping)
|
.interactiveSpring(response: hoverSpringResponse, dampingFraction: hoverSpringDamping)
|
||||||
@@ -53,6 +55,11 @@ struct ContentView: View {
|
|||||||
.overlay(alignment: .top) {
|
.overlay(alignment: .top) {
|
||||||
Rectangle().fill(.black).frame(height: 1)
|
Rectangle().fill(.black).frame(height: 1)
|
||||||
}
|
}
|
||||||
|
.overlay(alignment: .bottomTrailing) {
|
||||||
|
if vm.notchState == .open {
|
||||||
|
resizeHandle
|
||||||
|
}
|
||||||
|
}
|
||||||
.shadow(
|
.shadow(
|
||||||
color: enableShadow ? Color.black.opacity(shadowOpacity) : .clear,
|
color: enableShadow ? Color.black.opacity(shadowOpacity) : .clear,
|
||||||
radius: enableShadow ? shadowRadius : 0
|
radius: enableShadow ? shadowRadius : 0
|
||||||
@@ -62,9 +69,22 @@ struct ContentView: View {
|
|||||||
.opacity(notchOpacity)
|
.opacity(notchOpacity)
|
||||||
.blur(radius: blurRadius)
|
.blur(radius: blurRadius)
|
||||||
.animation(vm.notchState == .open ? vm.openAnimation : vm.closeAnimation, value: vm.notchState)
|
.animation(vm.notchState == .open ? vm.openAnimation : vm.closeAnimation, value: vm.notchState)
|
||||||
.animation(vm.notchState == .open ? vm.openAnimation : vm.closeAnimation, value: vm.notchSize.width)
|
.animation(sizeAnimation, value: vm.notchSize.width)
|
||||||
.animation(vm.notchState == .open ? vm.openAnimation : vm.closeAnimation, value: vm.notchSize.height)
|
.animation(sizeAnimation, value: vm.notchSize.height)
|
||||||
.onHover { handleHover($0) }
|
.onHover { handleHover($0) }
|
||||||
|
.onChange(of: vm.isCloseTransitionActive) { _, isClosing in
|
||||||
|
if isClosing {
|
||||||
|
hoverTask?.cancel()
|
||||||
|
} else {
|
||||||
|
scheduleHoverOpenIfNeeded()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.onDisappear {
|
||||||
|
hoverTask?.cancel()
|
||||||
|
resizeStartSize = nil
|
||||||
|
resizeStartMouseLocation = nil
|
||||||
|
vm.endInteractiveResize()
|
||||||
|
}
|
||||||
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .top)
|
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .top)
|
||||||
.edgesIgnoringSafeArea(.all)
|
.edgesIgnoringSafeArea(.all)
|
||||||
}
|
}
|
||||||
@@ -94,6 +114,47 @@ struct ContentView: View {
|
|||||||
.background(.black)
|
.background(.black)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private var resizeHandle: some View {
|
||||||
|
ResizeHandleShape()
|
||||||
|
.stroke(.white.opacity(0.32), style: StrokeStyle(lineWidth: 1.2, lineCap: .round))
|
||||||
|
.frame(width: 16, height: 16)
|
||||||
|
.padding(.trailing, 8)
|
||||||
|
.padding(.bottom, 8)
|
||||||
|
.contentShape(Rectangle().inset(by: -8))
|
||||||
|
.gesture(resizeGesture)
|
||||||
|
}
|
||||||
|
|
||||||
|
private var resizeGesture: some Gesture {
|
||||||
|
DragGesture(minimumDistance: 0)
|
||||||
|
.onChanged { value in
|
||||||
|
if resizeStartSize == nil {
|
||||||
|
resizeStartSize = vm.notchSize
|
||||||
|
resizeStartMouseLocation = NSEvent.mouseLocation
|
||||||
|
vm.beginInteractiveResize()
|
||||||
|
}
|
||||||
|
|
||||||
|
guard let startSize = resizeStartSize,
|
||||||
|
let startMouseLocation = resizeStartMouseLocation else { return }
|
||||||
|
let currentMouseLocation = NSEvent.mouseLocation
|
||||||
|
vm.resizeOpenNotch(
|
||||||
|
to: CGSize(
|
||||||
|
width: startSize.width + ((currentMouseLocation.x - startMouseLocation.x) * 2),
|
||||||
|
height: startSize.height + (startMouseLocation.y - currentMouseLocation.y)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
.onEnded { _ in
|
||||||
|
resizeStartSize = nil
|
||||||
|
resizeStartMouseLocation = nil
|
||||||
|
vm.endInteractiveResize()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private var sizeAnimation: Animation? {
|
||||||
|
guard !vm.isUserResizing, !vm.isPresetResizing else { return nil }
|
||||||
|
return vm.notchState == .open ? vm.openAnimation : vm.closeAnimation
|
||||||
|
}
|
||||||
|
|
||||||
/// Open layout: VStack with toolbar row on top, terminal in the middle,
|
/// Open layout: VStack with toolbar row on top, terminal in the middle,
|
||||||
/// tab bar at the bottom. Every section has a black background.
|
/// tab bar at the bottom. Every section has a black background.
|
||||||
private var openContent: some View {
|
private var openContent: some View {
|
||||||
@@ -146,17 +207,30 @@ struct ContentView: View {
|
|||||||
private func handleHover(_ hovering: Bool) {
|
private func handleHover(_ hovering: Bool) {
|
||||||
if hovering {
|
if hovering {
|
||||||
withAnimation(hoverAnimation) { vm.isHovering = true }
|
withAnimation(hoverAnimation) { vm.isHovering = true }
|
||||||
guard openNotchOnHover, vm.notchState == .closed else { return }
|
scheduleHoverOpenIfNeeded()
|
||||||
|
|
||||||
hoverTask?.cancel()
|
|
||||||
hoverTask = Task { @MainActor in
|
|
||||||
try? await Task.sleep(nanoseconds: UInt64(minimumHoverDuration * 1_000_000_000))
|
|
||||||
guard !Task.isCancelled, vm.isHovering else { return }
|
|
||||||
vm.requestOpen?()
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
hoverTask?.cancel()
|
hoverTask?.cancel()
|
||||||
withAnimation(hoverAnimation) { vm.isHovering = false }
|
withAnimation(hoverAnimation) { vm.isHovering = false }
|
||||||
|
vm.clearHoverOpenSuppression()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func scheduleHoverOpenIfNeeded() {
|
||||||
|
hoverTask?.cancel()
|
||||||
|
guard openNotchOnHover,
|
||||||
|
vm.notchState == .closed,
|
||||||
|
!vm.isCloseTransitionActive,
|
||||||
|
!vm.suppressHoverOpenUntilHoverExit,
|
||||||
|
vm.isHovering else { return }
|
||||||
|
|
||||||
|
hoverTask = Task { @MainActor in
|
||||||
|
try? await Task.sleep(nanoseconds: UInt64(minimumHoverDuration * 1_000_000_000))
|
||||||
|
guard !Task.isCancelled,
|
||||||
|
vm.isHovering,
|
||||||
|
vm.notchState == .closed,
|
||||||
|
!vm.isCloseTransitionActive,
|
||||||
|
!vm.suppressHoverOpenUntilHoverExit else { return }
|
||||||
|
vm.requestOpen?()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -164,3 +238,16 @@ struct ContentView: View {
|
|||||||
title.count <= 30 ? title : String(title.prefix(28)) + "…"
|
title.count <= 30 ? title : String(title.prefix(28)) + "…"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private struct ResizeHandleShape: Shape {
|
||||||
|
func path(in rect: CGRect) -> Path {
|
||||||
|
var path = Path()
|
||||||
|
path.move(to: CGPoint(x: rect.maxX - 10, y: rect.maxY))
|
||||||
|
path.addLine(to: CGPoint(x: rect.maxX, y: rect.maxY - 10))
|
||||||
|
path.move(to: CGPoint(x: rect.maxX - 6, y: rect.maxY))
|
||||||
|
path.addLine(to: CGPoint(x: rect.maxX, y: rect.maxY - 6))
|
||||||
|
path.move(to: CGPoint(x: rect.maxX - 2, y: rect.maxY))
|
||||||
|
path.addLine(to: CGPoint(x: rect.maxX, y: rect.maxY - 2))
|
||||||
|
return path
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -18,6 +18,7 @@ class HotkeyManager {
|
|||||||
var onNextTab: (() -> Void)?
|
var onNextTab: (() -> Void)?
|
||||||
var onPreviousTab: (() -> Void)?
|
var onPreviousTab: (() -> Void)?
|
||||||
var onDetachTab: (() -> Void)?
|
var onDetachTab: (() -> Void)?
|
||||||
|
var onApplySizePreset: ((TerminalSizePreset) -> Void)?
|
||||||
var onSwitchToTab: ((Int) -> Void)?
|
var onSwitchToTab: ((Int) -> Void)?
|
||||||
|
|
||||||
/// Tab-level hotkeys only fire when the notch is open.
|
/// Tab-level hotkeys only fire when the notch is open.
|
||||||
@@ -50,6 +51,9 @@ class HotkeyManager {
|
|||||||
private var detachBinding: HotkeyBinding {
|
private var detachBinding: HotkeyBinding {
|
||||||
binding(for: NotchSettings.Keys.hotkeyDetachTab) ?? .cmdD
|
binding(for: NotchSettings.Keys.hotkeyDetachTab) ?? .cmdD
|
||||||
}
|
}
|
||||||
|
private var sizePresets: [TerminalSizePreset] {
|
||||||
|
TerminalSizePresetStore.load()
|
||||||
|
}
|
||||||
|
|
||||||
private func binding(for key: String) -> HotkeyBinding? {
|
private func binding(for key: String) -> HotkeyBinding? {
|
||||||
guard let json = UserDefaults.standard.string(forKey: key) else { return nil }
|
guard let json = UserDefaults.standard.string(forKey: key) else { return nil }
|
||||||
@@ -211,6 +215,13 @@ class HotkeyManager {
|
|||||||
onDetachTab?()
|
onDetachTab?()
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
for preset in sizePresets {
|
||||||
|
guard let binding = preset.hotkey else { continue }
|
||||||
|
if binding.matches(event) {
|
||||||
|
onApplySizePreset?(preset)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Cmd+1 through Cmd+9
|
// Cmd+1 through Cmd+9
|
||||||
if event.modifierFlags.contains(.command) {
|
if event.modifierFlags.contains(.command) {
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import AppKit
|
import AppKit
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
import SwiftTerm
|
import Combine
|
||||||
|
|
||||||
/// Manages standalone pop-out terminal windows for detached tabs.
|
/// Manages standalone pop-out terminal windows for detached tabs.
|
||||||
/// Each detached tab gets its own resizable window with the terminal view.
|
/// Each detached tab gets its own resizable window with the terminal view.
|
||||||
@@ -12,6 +12,7 @@ class PopoutWindowController: NSObject, NSWindowDelegate {
|
|||||||
/// Tracks open pop-out windows so they aren't released prematurely.
|
/// Tracks open pop-out windows so they aren't released prematurely.
|
||||||
private var windows: [UUID: NSWindow] = [:]
|
private var windows: [UUID: NSWindow] = [:]
|
||||||
private var sessions: [UUID: TerminalSession] = [:]
|
private var sessions: [UUID: TerminalSession] = [:]
|
||||||
|
private var titleObservers: [UUID: AnyCancellable] = [:]
|
||||||
|
|
||||||
private override init() {
|
private override init() {
|
||||||
super.init()
|
super.init()
|
||||||
@@ -21,6 +22,12 @@ class PopoutWindowController: NSObject, NSWindowDelegate {
|
|||||||
func popout(session: TerminalSession) {
|
func popout(session: TerminalSession) {
|
||||||
let windowID = session.id
|
let windowID = session.id
|
||||||
|
|
||||||
|
if let existingWindow = windows[windowID] {
|
||||||
|
existingWindow.makeKeyAndOrderFront(nil)
|
||||||
|
NSApp.activate(ignoringOtherApps: true)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
let win = NSWindow(
|
let win = NSWindow(
|
||||||
contentRect: NSRect(x: 0, y: 0, width: 720, height: 480),
|
contentRect: NSRect(x: 0, y: 0, width: 720, height: 480),
|
||||||
styleMask: [.titled, .closable, .resizable, .miniaturizable],
|
styleMask: [.titled, .closable, .resizable, .miniaturizable],
|
||||||
@@ -33,12 +40,14 @@ class PopoutWindowController: NSObject, NSWindowDelegate {
|
|||||||
win.delegate = self
|
win.delegate = self
|
||||||
win.isReleasedWhenClosed = false
|
win.isReleasedWhenClosed = false
|
||||||
|
|
||||||
// Embed the terminal view directly
|
let hostingView = NSHostingView(
|
||||||
let tv = session.terminalView
|
rootView: SwiftTermView(session: session)
|
||||||
tv.removeFromSuperview()
|
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
||||||
tv.frame = NSRect(origin: .zero, size: win.contentView!.bounds.size)
|
.background(Color.black)
|
||||||
tv.autoresizingMask = [.width, .height]
|
.preferredColorScheme(.dark)
|
||||||
win.contentView?.addSubview(tv)
|
)
|
||||||
|
hostingView.frame = NSRect(origin: .zero, size: win.contentRect(forFrameRect: win.frame).size)
|
||||||
|
win.contentView = hostingView
|
||||||
|
|
||||||
win.center()
|
win.center()
|
||||||
win.makeKeyAndOrderFront(nil)
|
win.makeKeyAndOrderFront(nil)
|
||||||
@@ -48,16 +57,22 @@ class PopoutWindowController: NSObject, NSWindowDelegate {
|
|||||||
sessions[windowID] = session
|
sessions[windowID] = session
|
||||||
|
|
||||||
// Update window title when the terminal title changes
|
// Update window title when the terminal title changes
|
||||||
session.$title
|
titleObservers[windowID] = session.$title
|
||||||
.receive(on: RunLoop.main)
|
.receive(on: RunLoop.main)
|
||||||
.sink { [weak win] title in win?.title = title }
|
.sink { [weak win] title in win?.title = title }
|
||||||
.store(in: &popoutCancellables)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private var popoutCancellables = Set<AnyCancellable>()
|
|
||||||
|
|
||||||
// MARK: - NSWindowDelegate
|
// MARK: - NSWindowDelegate
|
||||||
|
|
||||||
|
func windowDidBecomeKey(_ notification: Notification) {
|
||||||
|
guard let window = notification.object as? NSWindow,
|
||||||
|
let entry = windows.first(where: { $0.value === window }),
|
||||||
|
let terminalView = sessions[entry.key]?.terminalView,
|
||||||
|
terminalView.window === window else { return }
|
||||||
|
|
||||||
|
window.makeFirstResponder(terminalView)
|
||||||
|
}
|
||||||
|
|
||||||
func windowWillClose(_ notification: Notification) {
|
func windowWillClose(_ notification: Notification) {
|
||||||
guard let closingWindow = notification.object as? NSWindow else { return }
|
guard let closingWindow = notification.object as? NSWindow else { return }
|
||||||
|
|
||||||
@@ -66,8 +81,7 @@ class PopoutWindowController: NSObject, NSWindowDelegate {
|
|||||||
sessions[entry.key]?.terminate()
|
sessions[entry.key]?.terminate()
|
||||||
sessions.removeValue(forKey: entry.key)
|
sessions.removeValue(forKey: entry.key)
|
||||||
windows.removeValue(forKey: entry.key)
|
windows.removeValue(forKey: entry.key)
|
||||||
|
titleObservers.removeValue(forKey: entry.key)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
import Combine
|
|
||||||
404
Downterm/CommandNotch/Managers/ScreenManager.swift
Normal file
@@ -0,0 +1,404 @@
|
|||||||
|
import AppKit
|
||||||
|
import SwiftUI
|
||||||
|
import Combine
|
||||||
|
|
||||||
|
/// Manages one NotchWindow per connected display.
|
||||||
|
/// Routes all open/close through centralized methods that handle
|
||||||
|
/// window activation, key status, and first responder assignment
|
||||||
|
/// so the terminal can receive keyboard input.
|
||||||
|
@MainActor
|
||||||
|
class ScreenManager: ObservableObject {
|
||||||
|
|
||||||
|
static let shared = ScreenManager()
|
||||||
|
private let focusRetryDelay: TimeInterval = 0.01
|
||||||
|
private let presetResizeFrameInterval: TimeInterval = 1.0 / 60.0
|
||||||
|
|
||||||
|
private(set) var windows: [String: NotchWindow] = [:]
|
||||||
|
private(set) var viewModels: [String: NotchViewModel] = [:]
|
||||||
|
private var presetResizeTimers: [String: Timer] = [:]
|
||||||
|
|
||||||
|
@AppStorage(NotchSettings.Keys.showOnAllDisplays)
|
||||||
|
private var showOnAllDisplays = NotchSettings.Defaults.showOnAllDisplays
|
||||||
|
|
||||||
|
private var cancellables = Set<AnyCancellable>()
|
||||||
|
|
||||||
|
private init() {}
|
||||||
|
|
||||||
|
// MARK: - Lifecycle
|
||||||
|
|
||||||
|
func start() {
|
||||||
|
observeScreenChanges()
|
||||||
|
rebuildWindows()
|
||||||
|
setupHotkeys()
|
||||||
|
}
|
||||||
|
|
||||||
|
func stop() {
|
||||||
|
cleanupAllWindows()
|
||||||
|
cancellables.removeAll()
|
||||||
|
HotkeyManager.shared.stop()
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Hotkey wiring
|
||||||
|
|
||||||
|
private func setupHotkeys() {
|
||||||
|
let hk = HotkeyManager.shared
|
||||||
|
let tm = TerminalManager.shared
|
||||||
|
|
||||||
|
// Callbacks are invoked on the main thread by HotkeyManager.
|
||||||
|
// MainActor.assumeIsolated lets us safely call @MainActor methods.
|
||||||
|
hk.onToggle = { [weak self] in
|
||||||
|
MainActor.assumeIsolated { self?.toggleNotchOnActiveScreen() }
|
||||||
|
}
|
||||||
|
hk.onNewTab = { MainActor.assumeIsolated { tm.newTab() } }
|
||||||
|
hk.onCloseTab = { MainActor.assumeIsolated { tm.closeActiveTab() } }
|
||||||
|
hk.onNextTab = { MainActor.assumeIsolated { tm.nextTab() } }
|
||||||
|
hk.onPreviousTab = { MainActor.assumeIsolated { tm.previousTab() } }
|
||||||
|
hk.onDetachTab = { [weak self] in
|
||||||
|
MainActor.assumeIsolated { self?.detachActiveTab() }
|
||||||
|
}
|
||||||
|
hk.onApplySizePreset = { [weak self] preset in
|
||||||
|
MainActor.assumeIsolated { self?.applySizePreset(preset) }
|
||||||
|
}
|
||||||
|
hk.onSwitchToTab = { index in
|
||||||
|
MainActor.assumeIsolated { tm.switchToTab(at: index) }
|
||||||
|
}
|
||||||
|
|
||||||
|
hk.start()
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Toggle
|
||||||
|
|
||||||
|
func toggleNotchOnActiveScreen() {
|
||||||
|
let mouseLocation = NSEvent.mouseLocation
|
||||||
|
let targetScreen = NSScreen.screens.first { NSMouseInRect(mouseLocation, $0.frame, false) }
|
||||||
|
?? NSScreen.main
|
||||||
|
guard let screen = targetScreen else { return }
|
||||||
|
let uuid = screen.displayUUID
|
||||||
|
|
||||||
|
// Close any other open notch first
|
||||||
|
for (otherUUID, otherVM) in viewModels where otherUUID != uuid {
|
||||||
|
if otherVM.notchState == .open {
|
||||||
|
closeNotch(screenUUID: otherUUID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let vm = viewModels[uuid] {
|
||||||
|
if vm.notchState == .open {
|
||||||
|
closeNotch(screenUUID: uuid)
|
||||||
|
} else {
|
||||||
|
openNotch(screenUUID: uuid)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Open / Close
|
||||||
|
|
||||||
|
func openNotch(screenUUID: String) {
|
||||||
|
guard let vm = viewModels[screenUUID],
|
||||||
|
let window = windows[screenUUID] else { return }
|
||||||
|
|
||||||
|
vm.cancelCloseTransition()
|
||||||
|
|
||||||
|
withAnimation(vm.openAnimation) {
|
||||||
|
vm.open()
|
||||||
|
}
|
||||||
|
|
||||||
|
window.isNotchOpen = true
|
||||||
|
HotkeyManager.shared.isNotchOpen = true
|
||||||
|
|
||||||
|
// Activate the app so the window can become key.
|
||||||
|
NSApp.activate(ignoringOtherApps: true)
|
||||||
|
window.makeKeyAndOrderFront(nil)
|
||||||
|
|
||||||
|
focusActiveTerminal(in: screenUUID)
|
||||||
|
}
|
||||||
|
|
||||||
|
func closeNotch(screenUUID: String) {
|
||||||
|
guard let vm = viewModels[screenUUID],
|
||||||
|
let window = windows[screenUUID] else { return }
|
||||||
|
|
||||||
|
vm.beginCloseTransition()
|
||||||
|
|
||||||
|
withAnimation(vm.closeAnimation) {
|
||||||
|
vm.close()
|
||||||
|
}
|
||||||
|
|
||||||
|
window.isNotchOpen = false
|
||||||
|
HotkeyManager.shared.isNotchOpen = false
|
||||||
|
}
|
||||||
|
|
||||||
|
private func detachActiveTab() {
|
||||||
|
if let session = TerminalManager.shared.detachActiveTab() {
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
PopoutWindowController.shared.popout(session: session)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func applySizePreset(_ preset: TerminalSizePreset) {
|
||||||
|
guard let (screenUUID, vm) = viewModels.first(where: { $0.value.notchState == .open }) else {
|
||||||
|
UserDefaults.standard.set(preset.width, forKey: NotchSettings.Keys.openWidth)
|
||||||
|
UserDefaults.standard.set(preset.height, forKey: NotchSettings.Keys.openHeight)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let startSize = vm.notchSize
|
||||||
|
let targetSize = vm.setStoredOpenSize(preset.size)
|
||||||
|
animatePresetResize(for: screenUUID, from: startSize, to: targetSize, duration: vm.openAnimationDuration)
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Window creation
|
||||||
|
|
||||||
|
func rebuildWindows() {
|
||||||
|
cleanupAllWindows()
|
||||||
|
|
||||||
|
let screens: [NSScreen]
|
||||||
|
if showOnAllDisplays {
|
||||||
|
screens = NSScreen.screens
|
||||||
|
} else {
|
||||||
|
screens = [NSScreen.main].compactMap { $0 }
|
||||||
|
}
|
||||||
|
for screen in screens {
|
||||||
|
createWindow(for: screen)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func createWindow(for screen: NSScreen) {
|
||||||
|
let uuid = screen.displayUUID
|
||||||
|
let vm = NotchViewModel(screenUUID: uuid)
|
||||||
|
let initialContentSize = vm.openNotchSize
|
||||||
|
|
||||||
|
let window = NotchWindow(
|
||||||
|
contentRect: NSRect(origin: .zero, size: CGSize(width: initialContentSize.width + 40, height: initialContentSize.height + 20)),
|
||||||
|
styleMask: [.borderless, .nonactivatingPanel, .utilityWindow],
|
||||||
|
backing: .buffered,
|
||||||
|
defer: false
|
||||||
|
)
|
||||||
|
|
||||||
|
// Close the notch when the window loses focus
|
||||||
|
window.onResignKey = { [weak self] in
|
||||||
|
self?.closeNotch(screenUUID: uuid)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wire the ViewModel callbacks so ContentView routes through us
|
||||||
|
vm.requestOpen = { [weak self] in
|
||||||
|
self?.openNotch(screenUUID: uuid)
|
||||||
|
}
|
||||||
|
vm.requestClose = { [weak self] in
|
||||||
|
self?.closeNotch(screenUUID: uuid)
|
||||||
|
}
|
||||||
|
vm.requestWindowResize = { [weak self] in
|
||||||
|
self?.updateWindowFrame(for: uuid, centerHorizontally: true)
|
||||||
|
}
|
||||||
|
|
||||||
|
let hostingView = NSHostingView(
|
||||||
|
rootView: ContentView(vm: vm, terminalManager: TerminalManager.shared)
|
||||||
|
.preferredColorScheme(.dark)
|
||||||
|
)
|
||||||
|
let containerView = NSView(frame: NSRect(origin: .zero, size: window.frame.size))
|
||||||
|
containerView.autoresizesSubviews = true
|
||||||
|
containerView.wantsLayer = true
|
||||||
|
containerView.layer?.backgroundColor = NSColor.clear.cgColor
|
||||||
|
|
||||||
|
hostingView.frame = containerView.bounds
|
||||||
|
hostingView.autoresizingMask = [.width, .height]
|
||||||
|
containerView.addSubview(hostingView)
|
||||||
|
window.contentView = containerView
|
||||||
|
|
||||||
|
windows[uuid] = window
|
||||||
|
viewModels[uuid] = vm
|
||||||
|
|
||||||
|
updateWindowFrame(for: uuid, centerHorizontally: true)
|
||||||
|
window.orderFrontRegardless()
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Repositioning
|
||||||
|
|
||||||
|
func repositionWindows() {
|
||||||
|
for (uuid, window) in windows {
|
||||||
|
guard let screen = NSScreen.screens.first(where: { $0.displayUUID == uuid }) else { continue }
|
||||||
|
guard let vm = viewModels[uuid] else { continue }
|
||||||
|
|
||||||
|
vm.refreshClosedSize()
|
||||||
|
|
||||||
|
updateWindowFrame(for: uuid, on: screen, window: window, centerHorizontally: true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func updateWindowFrame(for screenUUID: String, centerHorizontally: Bool = false) {
|
||||||
|
guard let screen = NSScreen.screens.first(where: { $0.displayUUID == screenUUID }),
|
||||||
|
let window = windows[screenUUID] else { return }
|
||||||
|
updateWindowFrame(for: screenUUID, on: screen, window: window, centerHorizontally: centerHorizontally)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func updateWindowFrame(
|
||||||
|
for screenUUID: String,
|
||||||
|
on screen: NSScreen,
|
||||||
|
window: NotchWindow,
|
||||||
|
centerHorizontally: Bool = false
|
||||||
|
) {
|
||||||
|
let frame = targetWindowFrame(
|
||||||
|
for: screenUUID,
|
||||||
|
on: screen,
|
||||||
|
window: window,
|
||||||
|
centerHorizontally: centerHorizontally,
|
||||||
|
contentSize: nil
|
||||||
|
)
|
||||||
|
guard !window.frame.equalTo(frame) else { return }
|
||||||
|
window.setFrame(frame, display: false)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func targetWindowFrame(
|
||||||
|
for screenUUID: String,
|
||||||
|
on screen: NSScreen,
|
||||||
|
window: NotchWindow,
|
||||||
|
centerHorizontally: Bool,
|
||||||
|
contentSize: CGSize?
|
||||||
|
) -> NSRect {
|
||||||
|
guard let vm = viewModels[screenUUID] else { return window.frame }
|
||||||
|
|
||||||
|
let shadowPadding: CGFloat = 20
|
||||||
|
let openSize = contentSize ?? vm.openNotchSize
|
||||||
|
let windowWidth = openSize.width + 40
|
||||||
|
let windowHeight = openSize.height + shadowPadding
|
||||||
|
let centeredX = screen.frame.origin.x + (screen.frame.width - windowWidth) / 2
|
||||||
|
|
||||||
|
let x: CGFloat = centerHorizontally || vm.notchState == .closed
|
||||||
|
? centeredX
|
||||||
|
: min(max(window.frame.minX, screen.frame.minX), screen.frame.maxX - windowWidth)
|
||||||
|
|
||||||
|
return NSRect(
|
||||||
|
x: x,
|
||||||
|
y: screen.frame.origin.y + screen.frame.height - windowHeight,
|
||||||
|
width: windowWidth,
|
||||||
|
height: windowHeight
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func animatePresetResize(
|
||||||
|
for screenUUID: String,
|
||||||
|
from startSize: CGSize,
|
||||||
|
to targetSize: CGSize,
|
||||||
|
duration: TimeInterval
|
||||||
|
) {
|
||||||
|
cancelPresetResize(for: screenUUID)
|
||||||
|
|
||||||
|
guard let vm = viewModels[screenUUID] else { return }
|
||||||
|
guard startSize != targetSize else {
|
||||||
|
vm.notchSize = targetSize
|
||||||
|
updateWindowFrame(for: screenUUID, centerHorizontally: true)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
vm.isPresetResizing = true
|
||||||
|
let startTime = CACurrentMediaTime()
|
||||||
|
let duration = max(duration, presetResizeFrameInterval)
|
||||||
|
|
||||||
|
let timer = Timer(timeInterval: presetResizeFrameInterval, repeats: true) { [weak self] timer in
|
||||||
|
MainActor.assumeIsolated {
|
||||||
|
guard let self, let vm = self.viewModels[screenUUID] else {
|
||||||
|
timer.invalidate()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let elapsed = CACurrentMediaTime() - startTime
|
||||||
|
let progress = min(1, elapsed / duration)
|
||||||
|
let easedProgress = 0.5 - (cos(.pi * progress) / 2)
|
||||||
|
let size = CGSize(
|
||||||
|
width: startSize.width + ((targetSize.width - startSize.width) * easedProgress),
|
||||||
|
height: startSize.height + ((targetSize.height - startSize.height) * easedProgress)
|
||||||
|
)
|
||||||
|
|
||||||
|
vm.notchSize = size
|
||||||
|
self.updateWindowFrame(for: screenUUID, contentSize: size, centerHorizontally: true)
|
||||||
|
|
||||||
|
if progress >= 1 {
|
||||||
|
vm.notchSize = targetSize
|
||||||
|
vm.isPresetResizing = false
|
||||||
|
self.updateWindowFrame(for: screenUUID, contentSize: targetSize, centerHorizontally: true)
|
||||||
|
self.presetResizeTimers[screenUUID] = nil
|
||||||
|
timer.invalidate()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
presetResizeTimers[screenUUID] = timer
|
||||||
|
RunLoop.main.add(timer, forMode: .common)
|
||||||
|
timer.fire()
|
||||||
|
}
|
||||||
|
|
||||||
|
private func cancelPresetResize(for screenUUID: String) {
|
||||||
|
presetResizeTimers[screenUUID]?.invalidate()
|
||||||
|
presetResizeTimers[screenUUID] = nil
|
||||||
|
viewModels[screenUUID]?.isPresetResizing = false
|
||||||
|
}
|
||||||
|
|
||||||
|
private func updateWindowFrame(
|
||||||
|
for screenUUID: String,
|
||||||
|
contentSize: CGSize,
|
||||||
|
centerHorizontally: Bool = false
|
||||||
|
) {
|
||||||
|
guard let screen = NSScreen.screens.first(where: { $0.displayUUID == screenUUID }),
|
||||||
|
let window = windows[screenUUID] else { return }
|
||||||
|
|
||||||
|
let frame = targetWindowFrame(
|
||||||
|
for: screenUUID,
|
||||||
|
on: screen,
|
||||||
|
window: window,
|
||||||
|
centerHorizontally: centerHorizontally,
|
||||||
|
contentSize: contentSize
|
||||||
|
)
|
||||||
|
guard !window.frame.equalTo(frame) else { return }
|
||||||
|
window.setFrame(frame, display: false)
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Cleanup
|
||||||
|
|
||||||
|
private func cleanupAllWindows() {
|
||||||
|
for (_, timer) in presetResizeTimers {
|
||||||
|
timer.invalidate()
|
||||||
|
}
|
||||||
|
presetResizeTimers.removeAll()
|
||||||
|
for (_, window) in windows {
|
||||||
|
window.orderOut(nil)
|
||||||
|
window.close()
|
||||||
|
}
|
||||||
|
windows.removeAll()
|
||||||
|
viewModels.removeAll()
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Screen observation
|
||||||
|
|
||||||
|
private func observeScreenChanges() {
|
||||||
|
NotificationCenter.default.publisher(for: NSApplication.didChangeScreenParametersNotification)
|
||||||
|
.debounce(for: .milliseconds(500), scheduler: RunLoop.main)
|
||||||
|
.sink { [weak self] _ in self?.handleScreenConfigurationChange() }
|
||||||
|
.store(in: &cancellables)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func handleScreenConfigurationChange() {
|
||||||
|
let currentUUIDs = Set(NSScreen.screens.map { $0.displayUUID })
|
||||||
|
let knownUUIDs = Set(windows.keys)
|
||||||
|
if currentUUIDs != knownUUIDs {
|
||||||
|
rebuildWindows()
|
||||||
|
} else {
|
||||||
|
repositionWindows()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func focusActiveTerminal(in screenUUID: String, attemptsRemaining: Int = 12) {
|
||||||
|
guard let window = windows[screenUUID],
|
||||||
|
let terminalView = TerminalManager.shared.activeTab?.terminalView else { return }
|
||||||
|
|
||||||
|
if terminalView.window === window {
|
||||||
|
window.makeFirstResponder(terminalView)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
guard attemptsRemaining > 0 else { return }
|
||||||
|
|
||||||
|
DispatchQueue.main.asyncAfter(deadline: .now() + focusRetryDelay) { [weak self] in
|
||||||
|
self?.focusActiveTerminal(in: screenUUID, attemptsRemaining: attemptsRemaining - 1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -33,7 +33,7 @@ class SettingsWindowController: NSObject, NSWindowDelegate {
|
|||||||
backing: .buffered,
|
backing: .buffered,
|
||||||
defer: false
|
defer: false
|
||||||
)
|
)
|
||||||
win.title = "Downterm Settings"
|
win.title = "CommandNotch Settings"
|
||||||
win.contentView = hostingView
|
win.contentView = hostingView
|
||||||
win.center()
|
win.center()
|
||||||
win.delegate = self
|
win.delegate = self
|
||||||
@@ -3,7 +3,7 @@ import Carbon.HIToolbox
|
|||||||
|
|
||||||
/// Serializable representation of a keyboard shortcut (modifier flags + key code).
|
/// Serializable representation of a keyboard shortcut (modifier flags + key code).
|
||||||
/// Stored in UserDefaults as a JSON string.
|
/// Stored in UserDefaults as a JSON string.
|
||||||
struct HotkeyBinding: Codable, Equatable {
|
struct HotkeyBinding: Codable, Equatable, Hashable {
|
||||||
var modifiers: UInt // NSEvent.ModifierFlags.rawValue, masked to cmd/shift/ctrl/opt
|
var modifiers: UInt // NSEvent.ModifierFlags.rawValue, masked to cmd/shift/ctrl/opt
|
||||||
var keyCode: UInt16
|
var keyCode: UInt16
|
||||||
|
|
||||||
@@ -89,4 +89,25 @@ struct HotkeyBinding: Codable, Equatable {
|
|||||||
static let cmdShiftRB = HotkeyBinding(modifiers: NSEvent.ModifierFlags([.command, .shift]).rawValue, keyCode: 30) // ]
|
static let cmdShiftRB = HotkeyBinding(modifiers: NSEvent.ModifierFlags([.command, .shift]).rawValue, keyCode: 30) // ]
|
||||||
static let cmdShiftLB = HotkeyBinding(modifiers: NSEvent.ModifierFlags([.command, .shift]).rawValue, keyCode: 33) // [
|
static let cmdShiftLB = HotkeyBinding(modifiers: NSEvent.ModifierFlags([.command, .shift]).rawValue, keyCode: 33) // [
|
||||||
static let cmdD = HotkeyBinding(modifiers: NSEvent.ModifierFlags.command.rawValue, keyCode: 2)
|
static let cmdD = HotkeyBinding(modifiers: NSEvent.ModifierFlags.command.rawValue, keyCode: 2)
|
||||||
|
|
||||||
|
static func cmdShiftDigit(_ digit: Int) -> HotkeyBinding? {
|
||||||
|
guard let keyCode = keyCode(forDigit: digit) else { return nil }
|
||||||
|
return HotkeyBinding(modifiers: NSEvent.ModifierFlags([.command, .shift]).rawValue, keyCode: keyCode)
|
||||||
|
}
|
||||||
|
|
||||||
|
static func keyCode(forDigit digit: Int) -> UInt16? {
|
||||||
|
switch digit {
|
||||||
|
case 0: return 29
|
||||||
|
case 1: return 18
|
||||||
|
case 2: return 19
|
||||||
|
case 3: return 20
|
||||||
|
case 4: return 21
|
||||||
|
case 5: return 23
|
||||||
|
case 6: return 22
|
||||||
|
case 7: return 26
|
||||||
|
case 8: return 28
|
||||||
|
case 9: return 25
|
||||||
|
default: return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
import Foundation
|
import Foundation
|
||||||
|
import AppKit
|
||||||
|
|
||||||
/// Central registry of all user-configurable notch settings.
|
/// Central registry of all user-configurable notch settings.
|
||||||
enum NotchSettings {
|
enum NotchSettings {
|
||||||
@@ -36,6 +37,7 @@ enum NotchSettings {
|
|||||||
static let closeSpringDamping = "closeSpringDamping"
|
static let closeSpringDamping = "closeSpringDamping"
|
||||||
static let hoverSpringResponse = "hoverSpringResponse"
|
static let hoverSpringResponse = "hoverSpringResponse"
|
||||||
static let hoverSpringDamping = "hoverSpringDamping"
|
static let hoverSpringDamping = "hoverSpringDamping"
|
||||||
|
static let resizeAnimationDuration = "resizeAnimationDuration"
|
||||||
|
|
||||||
// Behavior
|
// Behavior
|
||||||
static let enableGestures = "enableGestures"
|
static let enableGestures = "enableGestures"
|
||||||
@@ -44,6 +46,8 @@ enum NotchSettings {
|
|||||||
// Terminal
|
// Terminal
|
||||||
static let terminalFontSize = "terminalFontSize"
|
static let terminalFontSize = "terminalFontSize"
|
||||||
static let terminalShell = "terminalShell"
|
static let terminalShell = "terminalShell"
|
||||||
|
static let terminalTheme = "terminalTheme"
|
||||||
|
static let terminalSizePresets = "terminalSizePresets"
|
||||||
|
|
||||||
// Hotkeys — each stores a HotkeyBinding JSON string
|
// Hotkeys — each stores a HotkeyBinding JSON string
|
||||||
static let hotkeyToggle = "hotkey_toggle"
|
static let hotkeyToggle = "hotkey_toggle"
|
||||||
@@ -82,12 +86,15 @@ enum NotchSettings {
|
|||||||
static let closeSpringDamping: Double = 1.0
|
static let closeSpringDamping: Double = 1.0
|
||||||
static let hoverSpringResponse: Double = 0.38
|
static let hoverSpringResponse: Double = 0.38
|
||||||
static let hoverSpringDamping: Double = 0.8
|
static let hoverSpringDamping: Double = 0.8
|
||||||
|
static let resizeAnimationDuration: Double = 0.42
|
||||||
|
|
||||||
static let enableGestures: Bool = true
|
static let enableGestures: Bool = true
|
||||||
static let gestureSensitivity: Double = 0.5
|
static let gestureSensitivity: Double = 0.5
|
||||||
|
|
||||||
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 terminalSizePresets: String = TerminalSizePresetStore.defaultPresetsJSON()
|
||||||
|
|
||||||
// Default hotkey bindings as JSON
|
// Default hotkey bindings as JSON
|
||||||
static let hotkeyToggle: String = HotkeyBinding.cmdReturn.toJSON()
|
static let hotkeyToggle: String = HotkeyBinding.cmdReturn.toJSON()
|
||||||
@@ -127,12 +134,15 @@ enum NotchSettings {
|
|||||||
Keys.closeSpringDamping: Defaults.closeSpringDamping,
|
Keys.closeSpringDamping: Defaults.closeSpringDamping,
|
||||||
Keys.hoverSpringResponse: Defaults.hoverSpringResponse,
|
Keys.hoverSpringResponse: Defaults.hoverSpringResponse,
|
||||||
Keys.hoverSpringDamping: Defaults.hoverSpringDamping,
|
Keys.hoverSpringDamping: Defaults.hoverSpringDamping,
|
||||||
|
Keys.resizeAnimationDuration: Defaults.resizeAnimationDuration,
|
||||||
|
|
||||||
Keys.enableGestures: Defaults.enableGestures,
|
Keys.enableGestures: Defaults.enableGestures,
|
||||||
Keys.gestureSensitivity: Defaults.gestureSensitivity,
|
Keys.gestureSensitivity: Defaults.gestureSensitivity,
|
||||||
|
|
||||||
Keys.terminalFontSize: Defaults.terminalFontSize,
|
Keys.terminalFontSize: Defaults.terminalFontSize,
|
||||||
Keys.terminalShell: Defaults.terminalShell,
|
Keys.terminalShell: Defaults.terminalShell,
|
||||||
|
Keys.terminalTheme: Defaults.terminalTheme,
|
||||||
|
Keys.terminalSizePresets: Defaults.terminalSizePresets,
|
||||||
|
|
||||||
Keys.hotkeyToggle: Defaults.hotkeyToggle,
|
Keys.hotkeyToggle: Defaults.hotkeyToggle,
|
||||||
Keys.hotkeyNewTab: Defaults.hotkeyNewTab,
|
Keys.hotkeyNewTab: Defaults.hotkeyNewTab,
|
||||||
@@ -171,3 +181,82 @@ enum NonNotchHeightMode: Int, CaseIterable, Identifiable {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
struct TerminalSizePreset: Codable, Equatable, Identifiable {
|
||||||
|
var id: UUID
|
||||||
|
var name: String
|
||||||
|
var width: Double
|
||||||
|
var height: Double
|
||||||
|
var hotkey: HotkeyBinding?
|
||||||
|
|
||||||
|
init(
|
||||||
|
id: UUID = UUID(),
|
||||||
|
name: String,
|
||||||
|
width: Double,
|
||||||
|
height: Double,
|
||||||
|
hotkey: HotkeyBinding? = nil
|
||||||
|
) {
|
||||||
|
self.id = id
|
||||||
|
self.name = name
|
||||||
|
self.width = width
|
||||||
|
self.height = height
|
||||||
|
self.hotkey = hotkey
|
||||||
|
}
|
||||||
|
|
||||||
|
var size: CGSize {
|
||||||
|
CGSize(width: width, height: height)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
enum TerminalSizePresetStore {
|
||||||
|
static func load() -> [TerminalSizePreset] {
|
||||||
|
let defaults = UserDefaults.standard
|
||||||
|
guard let json = defaults.string(forKey: NotchSettings.Keys.terminalSizePresets),
|
||||||
|
let data = json.data(using: .utf8),
|
||||||
|
let presets = try? JSONDecoder().decode([TerminalSizePreset].self, from: data) else {
|
||||||
|
return defaultPresets()
|
||||||
|
}
|
||||||
|
return presets
|
||||||
|
}
|
||||||
|
|
||||||
|
static func save(_ presets: [TerminalSizePreset]) {
|
||||||
|
guard let data = try? JSONEncoder().encode(presets),
|
||||||
|
let json = String(data: data, encoding: .utf8) else { return }
|
||||||
|
UserDefaults.standard.set(json, forKey: NotchSettings.Keys.terminalSizePresets)
|
||||||
|
}
|
||||||
|
|
||||||
|
static func reset() {
|
||||||
|
save(defaultPresets())
|
||||||
|
}
|
||||||
|
|
||||||
|
static func loadDefaults() -> [TerminalSizePreset] {
|
||||||
|
defaultPresets()
|
||||||
|
}
|
||||||
|
|
||||||
|
static func defaultPresetsJSON() -> String {
|
||||||
|
guard let data = try? JSONEncoder().encode(defaultPresets()),
|
||||||
|
let json = String(data: data, encoding: .utf8) else {
|
||||||
|
return "[]"
|
||||||
|
}
|
||||||
|
return json
|
||||||
|
}
|
||||||
|
|
||||||
|
static func suggestedHotkey(for presets: [TerminalSizePreset]) -> HotkeyBinding? {
|
||||||
|
let used = Set(presets.compactMap(\.hotkey))
|
||||||
|
for digit in 1...9 {
|
||||||
|
guard let candidate = HotkeyBinding.cmdShiftDigit(digit) else { continue }
|
||||||
|
if !used.contains(candidate) {
|
||||||
|
return candidate
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
private static func defaultPresets() -> [TerminalSizePreset] {
|
||||||
|
[
|
||||||
|
TerminalSizePreset(name: "Compact", width: 480, height: 220, hotkey: HotkeyBinding.cmdShiftDigit(1)),
|
||||||
|
TerminalSizePreset(name: "Default", width: 640, height: 350, hotkey: HotkeyBinding.cmdShiftDigit(2)),
|
||||||
|
TerminalSizePreset(name: "Large", width: 900, height: 500, hotkey: HotkeyBinding.cmdShiftDigit(3)),
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
181
Downterm/CommandNotch/Models/NotchViewModel.swift
Normal file
@@ -0,0 +1,181 @@
|
|||||||
|
import SwiftUI
|
||||||
|
import Combine
|
||||||
|
|
||||||
|
/// Per-screen observable state that drives the notch UI.
|
||||||
|
@MainActor
|
||||||
|
class NotchViewModel: ObservableObject {
|
||||||
|
private static let minimumOpenWidth: CGFloat = 320
|
||||||
|
private static let minimumOpenHeight: CGFloat = 140
|
||||||
|
private static let windowHorizontalPadding: CGFloat = 40
|
||||||
|
private static let windowVerticalPadding: CGFloat = 20
|
||||||
|
|
||||||
|
let screenUUID: String
|
||||||
|
|
||||||
|
@Published var notchState: NotchState = .closed
|
||||||
|
@Published var notchSize: CGSize
|
||||||
|
@Published var closedNotchSize: CGSize
|
||||||
|
@Published var isHovering: Bool = false
|
||||||
|
@Published var isCloseTransitionActive: Bool = false
|
||||||
|
@Published var suppressHoverOpenUntilHoverExit: Bool = false
|
||||||
|
@Published var isUserResizing: Bool = false
|
||||||
|
@Published var isPresetResizing: Bool = false
|
||||||
|
|
||||||
|
let terminalManager = TerminalManager.shared
|
||||||
|
|
||||||
|
/// Set by ScreenManager — routes open/close through proper
|
||||||
|
/// window activation so the terminal receives keyboard input.
|
||||||
|
var requestOpen: (() -> Void)?
|
||||||
|
var requestClose: (() -> Void)?
|
||||||
|
var requestWindowResize: (() -> Void)?
|
||||||
|
|
||||||
|
private var cancellables = Set<AnyCancellable>()
|
||||||
|
|
||||||
|
@AppStorage(NotchSettings.Keys.openWidth) private var openWidth = NotchSettings.Defaults.openWidth
|
||||||
|
@AppStorage(NotchSettings.Keys.openHeight) private var openHeight = NotchSettings.Defaults.openHeight
|
||||||
|
|
||||||
|
@AppStorage(NotchSettings.Keys.openSpringResponse) private var openSpringResponse = NotchSettings.Defaults.openSpringResponse
|
||||||
|
@AppStorage(NotchSettings.Keys.openSpringDamping) private var openSpringDamping = NotchSettings.Defaults.openSpringDamping
|
||||||
|
@AppStorage(NotchSettings.Keys.closeSpringResponse) private var closeSpringResponse = NotchSettings.Defaults.closeSpringResponse
|
||||||
|
@AppStorage(NotchSettings.Keys.closeSpringDamping) private var closeSpringDamping = NotchSettings.Defaults.closeSpringDamping
|
||||||
|
@AppStorage(NotchSettings.Keys.resizeAnimationDuration) private var resizeAnimationDurationSetting = NotchSettings.Defaults.resizeAnimationDuration
|
||||||
|
|
||||||
|
private var closeTransitionTask: Task<Void, Never>?
|
||||||
|
|
||||||
|
var openAnimation: Animation {
|
||||||
|
.spring(response: openSpringResponse, dampingFraction: openSpringDamping)
|
||||||
|
}
|
||||||
|
var closeAnimation: Animation {
|
||||||
|
.spring(response: closeSpringResponse, dampingFraction: closeSpringDamping)
|
||||||
|
}
|
||||||
|
var openAnimationDuration: TimeInterval {
|
||||||
|
max(0.05, resizeAnimationDurationSetting)
|
||||||
|
}
|
||||||
|
|
||||||
|
init(screenUUID: String) {
|
||||||
|
self.screenUUID = screenUUID
|
||||||
|
let screen = NSScreen.screens.first { $0.displayUUID == screenUUID } ?? NSScreen.main
|
||||||
|
let closed = screen?.closedNotchSize() ?? CGSize(width: 220, height: 32)
|
||||||
|
self.closedNotchSize = closed
|
||||||
|
self.notchSize = closed
|
||||||
|
}
|
||||||
|
|
||||||
|
func open() {
|
||||||
|
let size = openNotchSize
|
||||||
|
openWidth = size.width
|
||||||
|
openHeight = size.height
|
||||||
|
notchSize = size
|
||||||
|
notchState = .open
|
||||||
|
}
|
||||||
|
|
||||||
|
func close() {
|
||||||
|
refreshClosedSize()
|
||||||
|
notchSize = closedNotchSize
|
||||||
|
notchState = .closed
|
||||||
|
}
|
||||||
|
|
||||||
|
func refreshClosedSize() {
|
||||||
|
let screen = NSScreen.screens.first { $0.displayUUID == screenUUID } ?? NSScreen.main
|
||||||
|
closedNotchSize = screen?.closedNotchSize() ?? CGSize(width: 220, height: 32)
|
||||||
|
}
|
||||||
|
|
||||||
|
var openNotchSize: CGSize {
|
||||||
|
clampedOpenSize(CGSize(width: openWidth, height: openHeight))
|
||||||
|
}
|
||||||
|
|
||||||
|
func beginInteractiveResize() {
|
||||||
|
isUserResizing = true
|
||||||
|
}
|
||||||
|
|
||||||
|
func resizeOpenNotch(to proposedSize: CGSize) {
|
||||||
|
setOpenSize(proposedSize, notifyWindowResize: true)
|
||||||
|
}
|
||||||
|
|
||||||
|
func endInteractiveResize() {
|
||||||
|
isUserResizing = false
|
||||||
|
}
|
||||||
|
|
||||||
|
func applySizePreset(_ preset: TerminalSizePreset, notifyWindowResize: Bool = true) {
|
||||||
|
setOpenSize(preset.size, notifyWindowResize: notifyWindowResize)
|
||||||
|
}
|
||||||
|
|
||||||
|
@discardableResult
|
||||||
|
func setStoredOpenSize(_ proposedSize: CGSize) -> CGSize {
|
||||||
|
let clampedSize = clampedOpenSize(proposedSize)
|
||||||
|
openWidth = clampedSize.width
|
||||||
|
openHeight = clampedSize.height
|
||||||
|
return clampedSize
|
||||||
|
}
|
||||||
|
|
||||||
|
@discardableResult
|
||||||
|
func setOpenSize(_ proposedSize: CGSize, notifyWindowResize: Bool) -> CGSize {
|
||||||
|
let clampedSize = setStoredOpenSize(proposedSize)
|
||||||
|
if notchState == .open {
|
||||||
|
notchSize = clampedSize
|
||||||
|
}
|
||||||
|
if notifyWindowResize {
|
||||||
|
requestWindowResize?()
|
||||||
|
}
|
||||||
|
return clampedSize
|
||||||
|
}
|
||||||
|
|
||||||
|
private func clampedOpenSize(_ size: CGSize) -> CGSize {
|
||||||
|
CGSize(
|
||||||
|
width: size.width.clamped(to: Self.minimumOpenWidth...maximumAllowedWidth),
|
||||||
|
height: size.height.clamped(to: Self.minimumOpenHeight...maximumAllowedHeight)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private var maximumAllowedWidth: CGFloat {
|
||||||
|
guard let screen = NSScreen.screens.first(where: { $0.displayUUID == screenUUID }) ?? NSScreen.main else {
|
||||||
|
return Self.minimumOpenWidth
|
||||||
|
}
|
||||||
|
return max(Self.minimumOpenWidth, screen.frame.width - Self.windowHorizontalPadding)
|
||||||
|
}
|
||||||
|
|
||||||
|
private var maximumAllowedHeight: CGFloat {
|
||||||
|
guard let screen = NSScreen.screens.first(where: { $0.displayUUID == screenUUID }) ?? NSScreen.main else {
|
||||||
|
return Self.minimumOpenHeight
|
||||||
|
}
|
||||||
|
return max(Self.minimumOpenHeight, screen.frame.height - Self.windowVerticalPadding)
|
||||||
|
}
|
||||||
|
|
||||||
|
var closeInteractionLockDuration: TimeInterval {
|
||||||
|
max(closeSpringResponse + 0.2, 0.35)
|
||||||
|
}
|
||||||
|
|
||||||
|
func beginCloseTransition() {
|
||||||
|
closeTransitionTask?.cancel()
|
||||||
|
isCloseTransitionActive = true
|
||||||
|
if isHovering {
|
||||||
|
suppressHoverOpenUntilHoverExit = true
|
||||||
|
}
|
||||||
|
|
||||||
|
let delay = closeInteractionLockDuration
|
||||||
|
closeTransitionTask = Task { @MainActor [weak self] in
|
||||||
|
try? await Task.sleep(nanoseconds: UInt64(delay * 1_000_000_000))
|
||||||
|
guard let self, !Task.isCancelled else { return }
|
||||||
|
self.isCloseTransitionActive = false
|
||||||
|
self.closeTransitionTask = nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func cancelCloseTransition() {
|
||||||
|
closeTransitionTask?.cancel()
|
||||||
|
closeTransitionTask = nil
|
||||||
|
isCloseTransitionActive = false
|
||||||
|
}
|
||||||
|
|
||||||
|
func clearHoverOpenSuppression() {
|
||||||
|
suppressHoverOpenUntilHoverExit = false
|
||||||
|
}
|
||||||
|
|
||||||
|
deinit {
|
||||||
|
closeTransitionTask?.cancel()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private extension CGFloat {
|
||||||
|
func clamped(to range: ClosedRange<CGFloat>) -> CGFloat {
|
||||||
|
Swift.min(Swift.max(self, range.lowerBound), range.upperBound)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -13,6 +13,8 @@ class TerminalManager: ObservableObject {
|
|||||||
|
|
||||||
@AppStorage(NotchSettings.Keys.terminalFontSize)
|
@AppStorage(NotchSettings.Keys.terminalFontSize)
|
||||||
private var fontSize: Double = NotchSettings.Defaults.terminalFontSize
|
private var fontSize: Double = NotchSettings.Defaults.terminalFontSize
|
||||||
|
@AppStorage(NotchSettings.Keys.terminalTheme)
|
||||||
|
private var theme: String = NotchSettings.Defaults.terminalTheme
|
||||||
|
|
||||||
private var cancellables = Set<AnyCancellable>()
|
private var cancellables = Set<AnyCancellable>()
|
||||||
|
|
||||||
@@ -35,7 +37,10 @@ class TerminalManager: ObservableObject {
|
|||||||
// MARK: - Tab operations
|
// MARK: - Tab operations
|
||||||
|
|
||||||
func newTab() {
|
func newTab() {
|
||||||
let session = TerminalSession(fontSize: CGFloat(fontSize))
|
let session = TerminalSession(
|
||||||
|
fontSize: CGFloat(fontSize),
|
||||||
|
theme: TerminalTheme.resolve(theme)
|
||||||
|
)
|
||||||
|
|
||||||
// Forward title changes to trigger view updates in this manager
|
// Forward title changes to trigger view updates in this manager
|
||||||
session.$title
|
session.$title
|
||||||
@@ -104,4 +109,10 @@ class TerminalManager: ObservableObject {
|
|||||||
tab.updateFontSize(size)
|
tab.updateFontSize(size)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func updateAllThemes(_ theme: TerminalTheme) {
|
||||||
|
for tab in tabs {
|
||||||
|
tab.applyTheme(theme)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -9,25 +9,21 @@ class TerminalSession: NSObject, ObservableObject, LocalProcessDelegate, Termina
|
|||||||
let id = UUID()
|
let id = UUID()
|
||||||
let terminalView: TerminalView
|
let terminalView: TerminalView
|
||||||
private var process: LocalProcess?
|
private var process: LocalProcess?
|
||||||
|
private let backgroundColor = NSColor.black
|
||||||
|
|
||||||
@Published var title: String = "shell"
|
@Published var title: String = "shell"
|
||||||
@Published var isRunning: Bool = true
|
@Published var isRunning: Bool = true
|
||||||
@Published var currentDirectory: String?
|
@Published var currentDirectory: String?
|
||||||
|
|
||||||
init(fontSize: CGFloat) {
|
init(fontSize: CGFloat, theme: TerminalTheme) {
|
||||||
terminalView = TerminalView(frame: NSRect(x: 0, y: 0, width: 600, height: 300))
|
terminalView = TerminalView(frame: NSRect(x: 0, y: 0, width: 600, height: 300))
|
||||||
super.init()
|
super.init()
|
||||||
|
|
||||||
terminalView.terminalDelegate = self
|
terminalView.terminalDelegate = self
|
||||||
|
|
||||||
// Solid black — matches every other element in the notch.
|
|
||||||
// The single `.opacity(notchOpacity)` on ContentView makes
|
|
||||||
// everything uniformly transparent.
|
|
||||||
terminalView.nativeBackgroundColor = .black
|
|
||||||
terminalView.nativeForegroundColor = .init(white: 0.9, alpha: 1.0)
|
|
||||||
|
|
||||||
let font = NSFont.monospacedSystemFont(ofSize: fontSize, weight: .regular)
|
let font = NSFont.monospacedSystemFont(ofSize: fontSize, weight: .regular)
|
||||||
terminalView.font = font
|
terminalView.font = font
|
||||||
|
applyTheme(theme)
|
||||||
|
|
||||||
startShell()
|
startShell()
|
||||||
}
|
}
|
||||||
@@ -64,6 +60,14 @@ class TerminalSession: NSObject, ObservableObject, LocalProcessDelegate, Termina
|
|||||||
terminalView.font = NSFont.monospacedSystemFont(ofSize: size, weight: .regular)
|
terminalView.font = NSFont.monospacedSystemFont(ofSize: size, weight: .regular)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func applyTheme(_ theme: TerminalTheme) {
|
||||||
|
// Keep the notch visually consistent while swapping the terminal's
|
||||||
|
// default foreground color and ANSI palette for command output.
|
||||||
|
terminalView.nativeBackgroundColor = backgroundColor
|
||||||
|
terminalView.nativeForegroundColor = theme.foregroundColor
|
||||||
|
terminalView.installColors(theme.ansiColors)
|
||||||
|
}
|
||||||
|
|
||||||
func terminate() {
|
func terminate() {
|
||||||
process?.terminate()
|
process?.terminate()
|
||||||
process = nil
|
process = nil
|
||||||
117
Downterm/CommandNotch/Models/TerminalTheme.swift
Normal file
@@ -0,0 +1,117 @@
|
|||||||
|
import AppKit
|
||||||
|
import SwiftTerm
|
||||||
|
|
||||||
|
enum TerminalTheme: String, CaseIterable, Identifiable {
|
||||||
|
case terminalApp
|
||||||
|
case xterm
|
||||||
|
case solarizedDark
|
||||||
|
case dracula
|
||||||
|
case nord
|
||||||
|
|
||||||
|
var id: String { rawValue }
|
||||||
|
|
||||||
|
var label: String {
|
||||||
|
switch self {
|
||||||
|
case .terminalApp: return "Classic"
|
||||||
|
case .xterm: return "Xterm"
|
||||||
|
case .solarizedDark:return "Solarized Dark"
|
||||||
|
case .dracula: return "Dracula"
|
||||||
|
case .nord: return "Nord"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var detail: String {
|
||||||
|
switch self {
|
||||||
|
case .terminalApp:
|
||||||
|
return "Matches the app's current terminal palette."
|
||||||
|
case .xterm:
|
||||||
|
return "Traditional xterm-style ANSI colors."
|
||||||
|
case .solarizedDark:
|
||||||
|
return "Low-contrast dark palette with Solarized accents."
|
||||||
|
case .dracula:
|
||||||
|
return "Higher-contrast dark palette with vivid ANSI colors."
|
||||||
|
case .nord:
|
||||||
|
return "Cool blue-grey palette with restrained accents."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var foregroundColor: NSColor {
|
||||||
|
switch self {
|
||||||
|
case .terminalApp:
|
||||||
|
return Self.nsColor(0xE5E5E5)
|
||||||
|
case .xterm:
|
||||||
|
return Self.nsColor(0xE5E5E5)
|
||||||
|
case .solarizedDark:
|
||||||
|
return Self.nsColor(0x839496)
|
||||||
|
case .dracula:
|
||||||
|
return Self.nsColor(0xF8F8F2)
|
||||||
|
case .nord:
|
||||||
|
return Self.nsColor(0xD8DEE9)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var ansiColors: [Color] {
|
||||||
|
switch self {
|
||||||
|
case .terminalApp:
|
||||||
|
return Self.palette([
|
||||||
|
0x000000, 0xC23621, 0x25BC24, 0xADAD27,
|
||||||
|
0x492EE1, 0xD338D3, 0x33BBC8, 0xCBCCCD,
|
||||||
|
0x818383, 0xFC391F, 0x31E722, 0xEAEC23,
|
||||||
|
0x5833FF, 0xF935F8, 0x14F0F0, 0xE9EBEB
|
||||||
|
])
|
||||||
|
case .xterm:
|
||||||
|
return Self.palette([
|
||||||
|
0x000000, 0xCD0000, 0x00CD00, 0xCDCD00,
|
||||||
|
0x0000EE, 0xCD00CD, 0x00CDCD, 0xE5E5E5,
|
||||||
|
0x7F7F7F, 0xFF0000, 0x00FF00, 0xFFFF00,
|
||||||
|
0x5C5CFF, 0xFF00FF, 0x00FFFF, 0xFFFFFF
|
||||||
|
])
|
||||||
|
case .solarizedDark:
|
||||||
|
return Self.palette([
|
||||||
|
0x073642, 0xDC322F, 0x859900, 0xB58900,
|
||||||
|
0x268BD2, 0xD33682, 0x2AA198, 0xEEE8D5,
|
||||||
|
0x002B36, 0xCB4B16, 0x586E75, 0x657B83,
|
||||||
|
0x839496, 0x6C71C4, 0x93A1A1, 0xFDF6E3
|
||||||
|
])
|
||||||
|
case .dracula:
|
||||||
|
return Self.palette([
|
||||||
|
0x21222C, 0xFF5555, 0x50FA7B, 0xF1FA8C,
|
||||||
|
0xBD93F9, 0xFF79C6, 0x8BE9FD, 0xF8F8F2,
|
||||||
|
0x6272A4, 0xFF6E6E, 0x69FF94, 0xFFFFA5,
|
||||||
|
0xD6ACFF, 0xFF92DF, 0xA4FFFF, 0xFFFFFF
|
||||||
|
])
|
||||||
|
case .nord:
|
||||||
|
return Self.palette([
|
||||||
|
0x3B4252, 0xBF616A, 0xA3BE8C, 0xEBCB8B,
|
||||||
|
0x81A1C1, 0xB48EAD, 0x88C0D0, 0xE5E9F0,
|
||||||
|
0x4C566A, 0xBF616A, 0xA3BE8C, 0xEBCB8B,
|
||||||
|
0x81A1C1, 0xB48EAD, 0x8FBCBB, 0xECEFF4
|
||||||
|
])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static func resolve(_ rawValue: String) -> TerminalTheme {
|
||||||
|
TerminalTheme(rawValue: rawValue) ?? .terminalApp
|
||||||
|
}
|
||||||
|
|
||||||
|
private static func palette(_ hexValues: [UInt32]) -> [Color] {
|
||||||
|
hexValues.map(terminalColor)
|
||||||
|
}
|
||||||
|
|
||||||
|
private static func terminalColor(_ hex: UInt32) -> Color {
|
||||||
|
Color(
|
||||||
|
red: UInt16(((hex >> 16) & 0xFF) * 257),
|
||||||
|
green: UInt16(((hex >> 8) & 0xFF) * 257),
|
||||||
|
blue: UInt16((hex & 0xFF) * 257)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private static func nsColor(_ hex: UInt32) -> NSColor {
|
||||||
|
NSColor(
|
||||||
|
deviceRed: CGFloat((hex >> 16) & 0xFF) / 255.0,
|
||||||
|
green: CGFloat((hex >> 8) & 0xFF) / 255.0,
|
||||||
|
blue: CGFloat(hex & 0xFF) / 255.0,
|
||||||
|
alpha: 1.0
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,68 @@
|
|||||||
|
{
|
||||||
|
"images" : [
|
||||||
|
{
|
||||||
|
"filename" : "icon_16x16.png",
|
||||||
|
"idiom" : "mac",
|
||||||
|
"scale" : "1x",
|
||||||
|
"size" : "16x16"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename" : "icon_16x16@2x.png",
|
||||||
|
"idiom" : "mac",
|
||||||
|
"scale" : "2x",
|
||||||
|
"size" : "16x16"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename" : "icon_32x32.png",
|
||||||
|
"idiom" : "mac",
|
||||||
|
"scale" : "1x",
|
||||||
|
"size" : "32x32"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename" : "icon_32x32@2x.png",
|
||||||
|
"idiom" : "mac",
|
||||||
|
"scale" : "2x",
|
||||||
|
"size" : "32x32"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename" : "icon_128x128.png",
|
||||||
|
"idiom" : "mac",
|
||||||
|
"scale" : "1x",
|
||||||
|
"size" : "128x128"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename" : "icon_128x128@2x.png",
|
||||||
|
"idiom" : "mac",
|
||||||
|
"scale" : "2x",
|
||||||
|
"size" : "128x128"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename" : "icon_256x256.png",
|
||||||
|
"idiom" : "mac",
|
||||||
|
"scale" : "1x",
|
||||||
|
"size" : "256x256"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename" : "icon_256x256@2x.png",
|
||||||
|
"idiom" : "mac",
|
||||||
|
"scale" : "2x",
|
||||||
|
"size" : "256x256"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename" : "icon_512x512.png",
|
||||||
|
"idiom" : "mac",
|
||||||
|
"scale" : "1x",
|
||||||
|
"size" : "512x512"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename" : "icon_512x512@2x.png",
|
||||||
|
"idiom" : "mac",
|
||||||
|
"scale" : "2x",
|
||||||
|
"size" : "512x512"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"info" : {
|
||||||
|
"author" : "xcode",
|
||||||
|
"version" : 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
After Width: | Height: | Size: 945 B |
|
After Width: | Height: | Size: 1.9 KiB |
|
After Width: | Height: | Size: 735 B |
|
After Width: | Height: | Size: 290 B |
|
After Width: | Height: | Size: 1.9 KiB |
|
After Width: | Height: | Size: 4.3 KiB |
|
After Width: | Height: | Size: 958 B |
|
After Width: | Height: | Size: 512 B |
|
After Width: | Height: | Size: 4.3 KiB |
|
After Width: | Height: | Size: 29 KiB |
@@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"info" : {
|
||||||
|
"author" : "xcode",
|
||||||
|
"version" : 1
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -5,19 +5,19 @@
|
|||||||
<key>CFBundleDevelopmentRegion</key>
|
<key>CFBundleDevelopmentRegion</key>
|
||||||
<string>$(DEVELOPMENT_LANGUAGE)</string>
|
<string>$(DEVELOPMENT_LANGUAGE)</string>
|
||||||
<key>CFBundleDisplayName</key>
|
<key>CFBundleDisplayName</key>
|
||||||
<string>Downterm</string>
|
<string>CommandNotch</string>
|
||||||
<key>CFBundleExecutable</key>
|
<key>CFBundleExecutable</key>
|
||||||
<string>Downterm</string>
|
<string>CommandNotch</string>
|
||||||
<key>CFBundleIdentifier</key>
|
<key>CFBundleIdentifier</key>
|
||||||
<string>com.downterm.app</string>
|
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
|
||||||
<key>CFBundleInfoDictionaryVersion</key>
|
<key>CFBundleInfoDictionaryVersion</key>
|
||||||
<string>6.0</string>
|
<string>6.0</string>
|
||||||
<key>CFBundleName</key>
|
<key>CFBundleName</key>
|
||||||
<string>Downterm</string>
|
<string>CommandNotch</string>
|
||||||
<key>CFBundlePackageType</key>
|
<key>CFBundlePackageType</key>
|
||||||
<string>APPL</string>
|
<string>APPL</string>
|
||||||
<key>CFBundleShortVersionString</key>
|
<key>CFBundleShortVersionString</key>
|
||||||
<string>0.2.0</string>
|
<string>0.0.3</string>
|
||||||
<key>CFBundleVersion</key>
|
<key>CFBundleVersion</key>
|
||||||
<string>1</string>
|
<string>1</string>
|
||||||
<key>LSMinimumSystemVersion</key>
|
<key>LSMinimumSystemVersion</key>
|
||||||
@@ -25,6 +25,6 @@
|
|||||||
<key>LSUIElement</key>
|
<key>LSUIElement</key>
|
||||||
<true/>
|
<true/>
|
||||||
<key>NSHumanReadableCopyright</key>
|
<key>NSHumanReadableCopyright</key>
|
||||||
<string>Copyright © 2026 Downterm. All rights reserved.</string>
|
<string>Copyright © 2026 CommandNotch. All rights reserved.</string>
|
||||||
</dict>
|
</dict>
|
||||||
</plist>
|
</plist>
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
import AppKit
|
||||||
|
|
||||||
/// Tabbed settings panel with General, Appearance, Animation, Terminal, Hotkeys, and About.
|
/// Tabbed settings panel with General, Appearance, Animation, Terminal, Hotkeys, and About.
|
||||||
struct SettingsView: View {
|
struct SettingsView: View {
|
||||||
@@ -85,6 +86,14 @@ struct GeneralSettingsView: View {
|
|||||||
@AppStorage(NotchSettings.Keys.openWidth) private var openWidth = NotchSettings.Defaults.openWidth
|
@AppStorage(NotchSettings.Keys.openWidth) private var openWidth = NotchSettings.Defaults.openWidth
|
||||||
@AppStorage(NotchSettings.Keys.openHeight) private var openHeight = NotchSettings.Defaults.openHeight
|
@AppStorage(NotchSettings.Keys.openHeight) private var openHeight = NotchSettings.Defaults.openHeight
|
||||||
|
|
||||||
|
private var maxOpenWidth: Double {
|
||||||
|
max(openWidth, Double((NSScreen.screens.map { $0.frame.width - 40 }.max() ?? 1600).rounded()))
|
||||||
|
}
|
||||||
|
|
||||||
|
private var maxOpenHeight: Double {
|
||||||
|
max(openHeight, Double((NSScreen.screens.map { $0.frame.height - 20 }.max() ?? 900).rounded()))
|
||||||
|
}
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
Form {
|
Form {
|
||||||
Section("Display") {
|
Section("Display") {
|
||||||
@@ -146,12 +155,12 @@ struct GeneralSettingsView: View {
|
|||||||
Section("Open Notch Size") {
|
Section("Open Notch Size") {
|
||||||
HStack {
|
HStack {
|
||||||
Text("Width")
|
Text("Width")
|
||||||
Slider(value: $openWidth, in: 300...1200, step: 10)
|
Slider(value: $openWidth, in: 320...maxOpenWidth, step: 10)
|
||||||
Text("\(Int(openWidth))pt").monospacedDigit().frame(width: 60)
|
Text("\(Int(openWidth))pt").monospacedDigit().frame(width: 60)
|
||||||
}
|
}
|
||||||
HStack {
|
HStack {
|
||||||
Text("Height")
|
Text("Height")
|
||||||
Slider(value: $openHeight, in: 100...600, step: 10)
|
Slider(value: $openHeight, in: 140...maxOpenHeight, step: 10)
|
||||||
Text("\(Int(openHeight))pt").monospacedDigit().frame(width: 60)
|
Text("\(Int(openHeight))pt").monospacedDigit().frame(width: 60)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -218,6 +227,7 @@ struct AnimationSettingsView: View {
|
|||||||
@AppStorage(NotchSettings.Keys.closeSpringDamping) private var closeDamping = NotchSettings.Defaults.closeSpringDamping
|
@AppStorage(NotchSettings.Keys.closeSpringDamping) private var closeDamping = NotchSettings.Defaults.closeSpringDamping
|
||||||
@AppStorage(NotchSettings.Keys.hoverSpringResponse) private var hoverResponse = NotchSettings.Defaults.hoverSpringResponse
|
@AppStorage(NotchSettings.Keys.hoverSpringResponse) private var hoverResponse = NotchSettings.Defaults.hoverSpringResponse
|
||||||
@AppStorage(NotchSettings.Keys.hoverSpringDamping) private var hoverDamping = NotchSettings.Defaults.hoverSpringDamping
|
@AppStorage(NotchSettings.Keys.hoverSpringDamping) private var hoverDamping = NotchSettings.Defaults.hoverSpringDamping
|
||||||
|
@AppStorage(NotchSettings.Keys.resizeAnimationDuration) private var resizeDuration = NotchSettings.Defaults.resizeAnimationDuration
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
Form {
|
Form {
|
||||||
@@ -230,6 +240,9 @@ struct AnimationSettingsView: View {
|
|||||||
Section("Hover Animation") {
|
Section("Hover Animation") {
|
||||||
springControls(response: $hoverResponse, damping: $hoverDamping)
|
springControls(response: $hoverResponse, damping: $hoverDamping)
|
||||||
}
|
}
|
||||||
|
Section("Resize Animation") {
|
||||||
|
durationControl(duration: $resizeDuration)
|
||||||
|
}
|
||||||
Section {
|
Section {
|
||||||
Button("Reset to Defaults") {
|
Button("Reset to Defaults") {
|
||||||
openResponse = NotchSettings.Defaults.openSpringResponse
|
openResponse = NotchSettings.Defaults.openSpringResponse
|
||||||
@@ -238,6 +251,7 @@ struct AnimationSettingsView: View {
|
|||||||
closeDamping = NotchSettings.Defaults.closeSpringDamping
|
closeDamping = NotchSettings.Defaults.closeSpringDamping
|
||||||
hoverResponse = NotchSettings.Defaults.hoverSpringResponse
|
hoverResponse = NotchSettings.Defaults.hoverSpringResponse
|
||||||
hoverDamping = NotchSettings.Defaults.hoverSpringDamping
|
hoverDamping = NotchSettings.Defaults.hoverSpringDamping
|
||||||
|
resizeDuration = NotchSettings.Defaults.resizeAnimationDuration
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -257,6 +271,15 @@ struct AnimationSettingsView: View {
|
|||||||
Text(String(format: "%.2f", damping.wrappedValue)).monospacedDigit().frame(width: 50)
|
Text(String(format: "%.2f", damping.wrappedValue)).monospacedDigit().frame(width: 50)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ViewBuilder
|
||||||
|
private func durationControl(duration: Binding<Double>) -> some View {
|
||||||
|
HStack {
|
||||||
|
Text("Duration")
|
||||||
|
Slider(value: duration, in: 0.05...1.5, step: 0.01)
|
||||||
|
Text(String(format: "%.2fs", duration.wrappedValue)).monospacedDigit().frame(width: 56)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Terminal
|
// MARK: - Terminal
|
||||||
@@ -265,6 +288,11 @@ struct TerminalSettingsView: View {
|
|||||||
|
|
||||||
@AppStorage(NotchSettings.Keys.terminalFontSize) private var fontSize = NotchSettings.Defaults.terminalFontSize
|
@AppStorage(NotchSettings.Keys.terminalFontSize) private var fontSize = NotchSettings.Defaults.terminalFontSize
|
||||||
@AppStorage(NotchSettings.Keys.terminalShell) private var shellPath = NotchSettings.Defaults.terminalShell
|
@AppStorage(NotchSettings.Keys.terminalShell) private var shellPath = NotchSettings.Defaults.terminalShell
|
||||||
|
@AppStorage(NotchSettings.Keys.terminalTheme) private var theme = NotchSettings.Defaults.terminalTheme
|
||||||
|
@AppStorage(NotchSettings.Keys.openWidth) private var openWidth = NotchSettings.Defaults.openWidth
|
||||||
|
@AppStorage(NotchSettings.Keys.openHeight) private var openHeight = NotchSettings.Defaults.openHeight
|
||||||
|
|
||||||
|
@State private var sizePresets = TerminalSizePresetStore.load()
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
Form {
|
Form {
|
||||||
@@ -275,6 +303,21 @@ struct TerminalSettingsView: View {
|
|||||||
Text("\(Int(fontSize))pt").monospacedDigit().frame(width: 50)
|
Text("\(Int(fontSize))pt").monospacedDigit().frame(width: 50)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Section("Colors") {
|
||||||
|
Picker("Theme", selection: $theme) {
|
||||||
|
ForEach(TerminalTheme.allCases) { terminalTheme in
|
||||||
|
Text(terminalTheme.label).tag(terminalTheme.rawValue)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Text(TerminalTheme.resolve(theme).detail)
|
||||||
|
.font(.caption)
|
||||||
|
.foregroundStyle(.secondary)
|
||||||
|
|
||||||
|
Text("Applies to normal terminal text and the ANSI palette used by tools like `ls`.")
|
||||||
|
.font(.caption)
|
||||||
|
.foregroundStyle(.secondary)
|
||||||
|
}
|
||||||
Section("Shell") {
|
Section("Shell") {
|
||||||
TextField("Shell path (empty = $SHELL)", text: $shellPath)
|
TextField("Shell path (empty = $SHELL)", text: $shellPath)
|
||||||
.textFieldStyle(.roundedBorder)
|
.textFieldStyle(.roundedBorder)
|
||||||
@@ -282,8 +325,54 @@ struct TerminalSettingsView: View {
|
|||||||
.font(.caption)
|
.font(.caption)
|
||||||
.foregroundStyle(.secondary)
|
.foregroundStyle(.secondary)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Section("Size Presets") {
|
||||||
|
ForEach($sizePresets) { $preset in
|
||||||
|
TerminalSizePresetEditor(
|
||||||
|
preset: $preset,
|
||||||
|
currentOpenWidth: openWidth,
|
||||||
|
currentOpenHeight: openHeight,
|
||||||
|
onDelete: { deletePreset(id: preset.id) },
|
||||||
|
onApply: { applyPreset(preset) }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
HStack {
|
||||||
|
Button("Add Preset") {
|
||||||
|
sizePresets.append(
|
||||||
|
TerminalSizePreset(
|
||||||
|
name: "Preset \(sizePresets.count + 1)",
|
||||||
|
width: openWidth,
|
||||||
|
height: openHeight,
|
||||||
|
hotkey: TerminalSizePresetStore.suggestedHotkey(for: sizePresets)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Button("Reset Presets") {
|
||||||
|
sizePresets = TerminalSizePresetStore.loadDefaults()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Text("Size preset hotkeys are active when the notch is open. Default presets use ⌘⇧1, ⌘⇧2, and ⌘⇧3.")
|
||||||
|
.font(.caption)
|
||||||
|
.foregroundStyle(.secondary)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
.formStyle(.grouped)
|
.formStyle(.grouped)
|
||||||
|
.onChange(of: sizePresets) { _, newValue in
|
||||||
|
TerminalSizePresetStore.save(newValue)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func deletePreset(id: UUID) {
|
||||||
|
sizePresets.removeAll { $0.id == id }
|
||||||
|
}
|
||||||
|
|
||||||
|
private func applyPreset(_ preset: TerminalSizePreset) {
|
||||||
|
openWidth = preset.width
|
||||||
|
openHeight = preset.height
|
||||||
|
ScreenManager.shared.applySizePreset(preset)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -313,7 +402,7 @@ struct HotkeySettingsView: View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Section {
|
Section {
|
||||||
Text("⌘1–9 always switch to tab by number.")
|
Text("⌘1–9 always switch to tab by number. Size preset hotkeys are configured in Terminal > Size Presets.")
|
||||||
.font(.caption)
|
.font(.caption)
|
||||||
.foregroundStyle(.secondary)
|
.foregroundStyle(.secondary)
|
||||||
}
|
}
|
||||||
@@ -361,6 +450,52 @@ struct HotkeySettingsView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private struct TerminalSizePresetEditor: View {
|
||||||
|
@Binding var preset: TerminalSizePreset
|
||||||
|
|
||||||
|
let currentOpenWidth: Double
|
||||||
|
let currentOpenHeight: Double
|
||||||
|
let onDelete: () -> Void
|
||||||
|
let onApply: () -> Void
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
VStack(alignment: .leading, spacing: 10) {
|
||||||
|
HStack {
|
||||||
|
TextField("Preset name", text: $preset.name)
|
||||||
|
.textFieldStyle(.roundedBorder)
|
||||||
|
Button(role: .destructive, action: onDelete) {
|
||||||
|
Image(systemName: "trash")
|
||||||
|
}
|
||||||
|
.buttonStyle(.borderless)
|
||||||
|
}
|
||||||
|
|
||||||
|
HStack {
|
||||||
|
Text("Width")
|
||||||
|
TextField("Width", value: $preset.width, format: .number.precision(.fractionLength(0)))
|
||||||
|
.textFieldStyle(.roundedBorder)
|
||||||
|
.frame(width: 90)
|
||||||
|
|
||||||
|
Text("Height")
|
||||||
|
TextField("Height", value: $preset.height, format: .number.precision(.fractionLength(0)))
|
||||||
|
.textFieldStyle(.roundedBorder)
|
||||||
|
.frame(width: 90)
|
||||||
|
|
||||||
|
Spacer()
|
||||||
|
|
||||||
|
Button("Use Current Size") {
|
||||||
|
preset.width = currentOpenWidth
|
||||||
|
preset.height = currentOpenHeight
|
||||||
|
}
|
||||||
|
|
||||||
|
Button("Apply", action: onApply)
|
||||||
|
}
|
||||||
|
|
||||||
|
OptionalHotkeyRecorderView(label: "Hotkey", binding: $preset.hotkey)
|
||||||
|
}
|
||||||
|
.padding(.vertical, 4)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// MARK: - About
|
// MARK: - About
|
||||||
|
|
||||||
struct AboutSettingsView: View {
|
struct AboutSettingsView: View {
|
||||||
@@ -370,7 +505,7 @@ struct AboutSettingsView: View {
|
|||||||
Image(systemName: "terminal")
|
Image(systemName: "terminal")
|
||||||
.font(.system(size: 64))
|
.font(.system(size: 64))
|
||||||
.foregroundStyle(.secondary)
|
.foregroundStyle(.secondary)
|
||||||
Text("Downterm")
|
Text("CommandNotch")
|
||||||
.font(.largeTitle.bold())
|
.font(.largeTitle.bold())
|
||||||
Text("Version 0.3.0")
|
Text("Version 0.3.0")
|
||||||
.foregroundStyle(.secondary)
|
.foregroundStyle(.secondary)
|
||||||
@@ -1,250 +0,0 @@
|
|||||||
import AppKit
|
|
||||||
import SwiftUI
|
|
||||||
import Combine
|
|
||||||
|
|
||||||
/// Manages one NotchWindow per connected display.
|
|
||||||
/// Routes all open/close through centralized methods that handle
|
|
||||||
/// window activation, key status, and first responder assignment
|
|
||||||
/// so the terminal can receive keyboard input.
|
|
||||||
@MainActor
|
|
||||||
class ScreenManager: ObservableObject {
|
|
||||||
|
|
||||||
static let shared = ScreenManager()
|
|
||||||
|
|
||||||
private(set) var windows: [String: NotchWindow] = [:]
|
|
||||||
private(set) var viewModels: [String: NotchViewModel] = [:]
|
|
||||||
|
|
||||||
@AppStorage(NotchSettings.Keys.showOnAllDisplays)
|
|
||||||
private var showOnAllDisplays = NotchSettings.Defaults.showOnAllDisplays
|
|
||||||
|
|
||||||
private var cancellables = Set<AnyCancellable>()
|
|
||||||
|
|
||||||
private init() {}
|
|
||||||
|
|
||||||
// MARK: - Lifecycle
|
|
||||||
|
|
||||||
func start() {
|
|
||||||
observeScreenChanges()
|
|
||||||
rebuildWindows()
|
|
||||||
setupHotkeys()
|
|
||||||
}
|
|
||||||
|
|
||||||
func stop() {
|
|
||||||
cleanupAllWindows()
|
|
||||||
cancellables.removeAll()
|
|
||||||
HotkeyManager.shared.stop()
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: - Hotkey wiring
|
|
||||||
|
|
||||||
private func setupHotkeys() {
|
|
||||||
let hk = HotkeyManager.shared
|
|
||||||
let tm = TerminalManager.shared
|
|
||||||
|
|
||||||
// Callbacks are invoked on the main thread by HotkeyManager.
|
|
||||||
// MainActor.assumeIsolated lets us safely call @MainActor methods.
|
|
||||||
hk.onToggle = { [weak self] in
|
|
||||||
MainActor.assumeIsolated { self?.toggleNotchOnActiveScreen() }
|
|
||||||
}
|
|
||||||
hk.onNewTab = { MainActor.assumeIsolated { tm.newTab() } }
|
|
||||||
hk.onCloseTab = { MainActor.assumeIsolated { tm.closeActiveTab() } }
|
|
||||||
hk.onNextTab = { MainActor.assumeIsolated { tm.nextTab() } }
|
|
||||||
hk.onPreviousTab = { MainActor.assumeIsolated { tm.previousTab() } }
|
|
||||||
hk.onDetachTab = { [weak self] in
|
|
||||||
MainActor.assumeIsolated { self?.detachActiveTab() }
|
|
||||||
}
|
|
||||||
hk.onSwitchToTab = { index in
|
|
||||||
MainActor.assumeIsolated { tm.switchToTab(at: index) }
|
|
||||||
}
|
|
||||||
|
|
||||||
hk.start()
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: - Toggle
|
|
||||||
|
|
||||||
func toggleNotchOnActiveScreen() {
|
|
||||||
let mouseLocation = NSEvent.mouseLocation
|
|
||||||
let targetScreen = NSScreen.screens.first { NSMouseInRect(mouseLocation, $0.frame, false) }
|
|
||||||
?? NSScreen.main
|
|
||||||
guard let screen = targetScreen else { return }
|
|
||||||
let uuid = screen.displayUUID
|
|
||||||
|
|
||||||
// Close any other open notch first
|
|
||||||
for (otherUUID, otherVM) in viewModels where otherUUID != uuid {
|
|
||||||
if otherVM.notchState == .open {
|
|
||||||
closeNotch(screenUUID: otherUUID)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if let vm = viewModels[uuid] {
|
|
||||||
if vm.notchState == .open {
|
|
||||||
closeNotch(screenUUID: uuid)
|
|
||||||
} else {
|
|
||||||
openNotch(screenUUID: uuid)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: - Open / Close
|
|
||||||
|
|
||||||
func openNotch(screenUUID: String) {
|
|
||||||
guard let vm = viewModels[screenUUID],
|
|
||||||
let window = windows[screenUUID] else { return }
|
|
||||||
|
|
||||||
withAnimation(vm.openAnimation) {
|
|
||||||
vm.open()
|
|
||||||
}
|
|
||||||
|
|
||||||
window.isNotchOpen = true
|
|
||||||
HotkeyManager.shared.isNotchOpen = true
|
|
||||||
|
|
||||||
// Activate the app so the window can become key.
|
|
||||||
NSApp.activate(ignoringOtherApps: true)
|
|
||||||
window.makeKeyAndOrderFront(nil)
|
|
||||||
|
|
||||||
// After layout settles, push keyboard focus to the terminal view
|
|
||||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.05) {
|
|
||||||
if let tv = TerminalManager.shared.activeTab?.terminalView {
|
|
||||||
window.makeFirstResponder(tv)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func closeNotch(screenUUID: String) {
|
|
||||||
guard let vm = viewModels[screenUUID],
|
|
||||||
let window = windows[screenUUID] else { return }
|
|
||||||
|
|
||||||
withAnimation(vm.closeAnimation) {
|
|
||||||
vm.close()
|
|
||||||
}
|
|
||||||
|
|
||||||
window.isNotchOpen = false
|
|
||||||
HotkeyManager.shared.isNotchOpen = false
|
|
||||||
}
|
|
||||||
|
|
||||||
private func detachActiveTab() {
|
|
||||||
if let session = TerminalManager.shared.detachActiveTab() {
|
|
||||||
PopoutWindowController.shared.popout(session: session)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: - Window creation
|
|
||||||
|
|
||||||
func rebuildWindows() {
|
|
||||||
cleanupAllWindows()
|
|
||||||
|
|
||||||
let screens: [NSScreen]
|
|
||||||
if showOnAllDisplays {
|
|
||||||
screens = NSScreen.screens
|
|
||||||
} else {
|
|
||||||
screens = [NSScreen.main].compactMap { $0 }
|
|
||||||
}
|
|
||||||
for screen in screens {
|
|
||||||
createWindow(for: screen)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private func createWindow(for screen: NSScreen) {
|
|
||||||
let uuid = screen.displayUUID
|
|
||||||
let vm = NotchViewModel(screenUUID: uuid)
|
|
||||||
|
|
||||||
let shadowPadding: CGFloat = 20
|
|
||||||
let openSize = vm.openNotchSize
|
|
||||||
let windowWidth = max(openSize.width + 40, screen.frame.width * 0.5)
|
|
||||||
let windowHeight = openSize.height + shadowPadding
|
|
||||||
|
|
||||||
let windowRect = NSRect(
|
|
||||||
x: screen.frame.origin.x + (screen.frame.width - windowWidth) / 2,
|
|
||||||
y: screen.frame.origin.y + screen.frame.height - windowHeight,
|
|
||||||
width: windowWidth,
|
|
||||||
height: windowHeight
|
|
||||||
)
|
|
||||||
|
|
||||||
let window = NotchWindow(
|
|
||||||
contentRect: windowRect,
|
|
||||||
styleMask: [.borderless, .nonactivatingPanel, .utilityWindow],
|
|
||||||
backing: .buffered,
|
|
||||||
defer: false
|
|
||||||
)
|
|
||||||
|
|
||||||
// Close the notch when the window loses focus
|
|
||||||
window.onResignKey = { [weak self] in
|
|
||||||
self?.closeNotch(screenUUID: uuid)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Wire the ViewModel callbacks so ContentView routes through us
|
|
||||||
vm.requestOpen = { [weak self] in
|
|
||||||
self?.openNotch(screenUUID: uuid)
|
|
||||||
}
|
|
||||||
vm.requestClose = { [weak self] in
|
|
||||||
self?.closeNotch(screenUUID: uuid)
|
|
||||||
}
|
|
||||||
|
|
||||||
let hostingView = NSHostingView(
|
|
||||||
rootView: ContentView(vm: vm, terminalManager: TerminalManager.shared)
|
|
||||||
.preferredColorScheme(.dark)
|
|
||||||
)
|
|
||||||
hostingView.frame = NSRect(origin: .zero, size: windowRect.size)
|
|
||||||
window.contentView = hostingView
|
|
||||||
|
|
||||||
window.setFrame(windowRect, display: true)
|
|
||||||
window.orderFrontRegardless()
|
|
||||||
|
|
||||||
windows[uuid] = window
|
|
||||||
viewModels[uuid] = vm
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: - Repositioning
|
|
||||||
|
|
||||||
func repositionWindows() {
|
|
||||||
for (uuid, window) in windows {
|
|
||||||
guard let screen = NSScreen.screens.first(where: { $0.displayUUID == uuid }) else { continue }
|
|
||||||
guard let vm = viewModels[uuid] else { continue }
|
|
||||||
|
|
||||||
vm.refreshClosedSize()
|
|
||||||
|
|
||||||
let shadowPadding: CGFloat = 20
|
|
||||||
let openSize = vm.openNotchSize
|
|
||||||
let windowWidth = max(openSize.width + 40, screen.frame.width * 0.5)
|
|
||||||
let windowHeight = openSize.height + shadowPadding
|
|
||||||
|
|
||||||
let newFrame = NSRect(
|
|
||||||
x: screen.frame.origin.x + (screen.frame.width - windowWidth) / 2,
|
|
||||||
y: screen.frame.origin.y + screen.frame.height - windowHeight,
|
|
||||||
width: windowWidth,
|
|
||||||
height: windowHeight
|
|
||||||
)
|
|
||||||
window.setFrame(newFrame, display: true)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: - Cleanup
|
|
||||||
|
|
||||||
private func cleanupAllWindows() {
|
|
||||||
for (_, window) in windows {
|
|
||||||
window.orderOut(nil)
|
|
||||||
window.close()
|
|
||||||
}
|
|
||||||
windows.removeAll()
|
|
||||||
viewModels.removeAll()
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: - Screen observation
|
|
||||||
|
|
||||||
private func observeScreenChanges() {
|
|
||||||
NotificationCenter.default.publisher(for: NSApplication.didChangeScreenParametersNotification)
|
|
||||||
.debounce(for: .milliseconds(500), scheduler: RunLoop.main)
|
|
||||||
.sink { [weak self] _ in self?.handleScreenConfigurationChange() }
|
|
||||||
.store(in: &cancellables)
|
|
||||||
}
|
|
||||||
|
|
||||||
private func handleScreenConfigurationChange() {
|
|
||||||
let currentUUIDs = Set(NSScreen.screens.map { $0.displayUUID })
|
|
||||||
let knownUUIDs = Set(windows.keys)
|
|
||||||
if currentUUIDs != knownUUIDs {
|
|
||||||
rebuildWindows()
|
|
||||||
} else {
|
|
||||||
repositionWindows()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,66 +0,0 @@
|
|||||||
import SwiftUI
|
|
||||||
import Combine
|
|
||||||
|
|
||||||
/// Per-screen observable state that drives the notch UI.
|
|
||||||
@MainActor
|
|
||||||
class NotchViewModel: ObservableObject {
|
|
||||||
|
|
||||||
let screenUUID: String
|
|
||||||
|
|
||||||
@Published var notchState: NotchState = .closed
|
|
||||||
@Published var notchSize: CGSize
|
|
||||||
@Published var closedNotchSize: CGSize
|
|
||||||
@Published var isHovering: Bool = false
|
|
||||||
|
|
||||||
let terminalManager = TerminalManager.shared
|
|
||||||
|
|
||||||
/// Set by ScreenManager — routes open/close through proper
|
|
||||||
/// window activation so the terminal receives keyboard input.
|
|
||||||
var requestOpen: (() -> Void)?
|
|
||||||
var requestClose: (() -> Void)?
|
|
||||||
|
|
||||||
private var cancellables = Set<AnyCancellable>()
|
|
||||||
|
|
||||||
@AppStorage(NotchSettings.Keys.openWidth) private var openWidth = NotchSettings.Defaults.openWidth
|
|
||||||
@AppStorage(NotchSettings.Keys.openHeight) private var openHeight = NotchSettings.Defaults.openHeight
|
|
||||||
|
|
||||||
@AppStorage(NotchSettings.Keys.openSpringResponse) private var openSpringResponse = NotchSettings.Defaults.openSpringResponse
|
|
||||||
@AppStorage(NotchSettings.Keys.openSpringDamping) private var openSpringDamping = NotchSettings.Defaults.openSpringDamping
|
|
||||||
@AppStorage(NotchSettings.Keys.closeSpringResponse) private var closeSpringResponse = NotchSettings.Defaults.closeSpringResponse
|
|
||||||
@AppStorage(NotchSettings.Keys.closeSpringDamping) private var closeSpringDamping = NotchSettings.Defaults.closeSpringDamping
|
|
||||||
|
|
||||||
var openAnimation: Animation {
|
|
||||||
.spring(response: openSpringResponse, dampingFraction: openSpringDamping)
|
|
||||||
}
|
|
||||||
var closeAnimation: Animation {
|
|
||||||
.spring(response: closeSpringResponse, dampingFraction: closeSpringDamping)
|
|
||||||
}
|
|
||||||
|
|
||||||
init(screenUUID: String) {
|
|
||||||
self.screenUUID = screenUUID
|
|
||||||
let screen = NSScreen.screens.first { $0.displayUUID == screenUUID } ?? NSScreen.main
|
|
||||||
let closed = screen?.closedNotchSize() ?? CGSize(width: 220, height: 32)
|
|
||||||
self.closedNotchSize = closed
|
|
||||||
self.notchSize = closed
|
|
||||||
}
|
|
||||||
|
|
||||||
func open() {
|
|
||||||
notchSize = CGSize(width: openWidth, height: openHeight)
|
|
||||||
notchState = .open
|
|
||||||
}
|
|
||||||
|
|
||||||
func close() {
|
|
||||||
refreshClosedSize()
|
|
||||||
notchSize = closedNotchSize
|
|
||||||
notchState = .closed
|
|
||||||
}
|
|
||||||
|
|
||||||
func refreshClosedSize() {
|
|
||||||
let screen = NSScreen.screens.first { $0.displayUUID == screenUUID } ?? NSScreen.main
|
|
||||||
closedNotchSize = screen?.closedNotchSize() ?? CGSize(width: 220, height: 32)
|
|
||||||
}
|
|
||||||
|
|
||||||
var openNotchSize: CGSize {
|
|
||||||
CGSize(width: openWidth, height: openHeight)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
name: Downterm
|
name: CommandNotch
|
||||||
options:
|
options:
|
||||||
bundleIdPrefix: com.downterm
|
bundleIdPrefix: com.commandnotch
|
||||||
deploymentTarget:
|
deploymentTarget:
|
||||||
macOS: "14.0"
|
macOS: "14.0"
|
||||||
xcodeVersion: "16.0"
|
xcodeVersion: "16.0"
|
||||||
@@ -15,34 +15,34 @@ packages:
|
|||||||
url: https://github.com/migueldeicaza/SwiftTerm.git
|
url: https://github.com/migueldeicaza/SwiftTerm.git
|
||||||
from: "1.2.0"
|
from: "1.2.0"
|
||||||
targets:
|
targets:
|
||||||
Downterm:
|
CommandNotch:
|
||||||
type: application
|
type: application
|
||||||
platform: macOS
|
platform: macOS
|
||||||
sources:
|
sources:
|
||||||
- path: Downterm
|
- path: CommandNotch
|
||||||
excludes:
|
excludes:
|
||||||
- Resources/Info.plist
|
- Resources/Info.plist
|
||||||
dependencies:
|
dependencies:
|
||||||
- package: SwiftTerm
|
- package: SwiftTerm
|
||||||
info:
|
info:
|
||||||
path: Downterm/Resources/Info.plist
|
path: CommandNotch/Resources/Info.plist
|
||||||
properties:
|
properties:
|
||||||
CFBundleName: Downterm
|
CFBundleName: CommandNotch
|
||||||
CFBundleDisplayName: Downterm
|
CFBundleDisplayName: CommandNotch
|
||||||
CFBundleIdentifier: com.downterm.app
|
CFBundleIdentifier: com.commandnotch.app
|
||||||
CFBundleVersion: "1"
|
CFBundleVersion: "1"
|
||||||
CFBundleShortVersionString: "0.2.0"
|
CFBundleShortVersionString: "0.2.0"
|
||||||
CFBundlePackageType: APPL
|
CFBundlePackageType: APPL
|
||||||
CFBundleExecutable: Downterm
|
CFBundleExecutable: CommandNotch
|
||||||
LSMinimumSystemVersion: "14.0"
|
LSMinimumSystemVersion: "14.0"
|
||||||
LSUIElement: true
|
LSUIElement: true
|
||||||
NSHumanReadableCopyright: "Copyright © 2026 Downterm. All rights reserved."
|
NSHumanReadableCopyright: "Copyright © 2026 CommandNotch. All rights reserved."
|
||||||
entitlements:
|
entitlements:
|
||||||
path: Downterm/Resources/Downterm.entitlements
|
path: CommandNotch/Resources/CommandNotch.entitlements
|
||||||
settings:
|
settings:
|
||||||
base:
|
base:
|
||||||
CODE_SIGN_ENTITLEMENTS: Downterm/Resources/Downterm.entitlements
|
CODE_SIGN_ENTITLEMENTS: CommandNotch/Resources/CommandNotch.entitlements
|
||||||
INFOPLIST_FILE: Downterm/Resources/Info.plist
|
INFOPLIST_FILE: CommandNotch/Resources/Info.plist
|
||||||
PRODUCT_BUNDLE_IDENTIFIER: com.downterm.app
|
PRODUCT_BUNDLE_IDENTIFIER: com.commandnotch.app
|
||||||
PRODUCT_NAME: Downterm
|
PRODUCT_NAME: CommandNotch
|
||||||
COMBINE_HIDPI_IMAGES: true
|
COMBINE_HIDPI_IMAGES: true
|
||||||
|
|||||||
BIN
icons/Downterm-icon-128.png
Normal file
|
After Width: | Height: | Size: 945 B |
BIN
icons/Downterm-icon-256.png
Normal file
|
After Width: | Height: | Size: 1.9 KiB |
BIN
icons/Downterm-icon-32.png
Normal file
|
After Width: | Height: | Size: 290 B |
BIN
icons/Downterm-icon-512.png
Normal file
|
After Width: | Height: | Size: 4.3 KiB |
BIN
icons/Downterm-icon-64.png
Normal file
|
After Width: | Height: | Size: 512 B |
197
icons/Icon.svg
Normal file
@@ -0,0 +1,197 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||||
|
|
||||||
|
<svg
|
||||||
|
width="512"
|
||||||
|
height="512"
|
||||||
|
viewBox="0 0 512 512"
|
||||||
|
version="1.1"
|
||||||
|
id="svg1"
|
||||||
|
xml:space="preserve"
|
||||||
|
inkscape:export-batch-name="Downterm-icon"
|
||||||
|
inkscape:export-batch-path="/Users/harvmaster/Projects/Workflow/downterm/icons"
|
||||||
|
inkscape:export-filename="Downterm-icon-32.png"
|
||||||
|
inkscape:export-xdpi="6"
|
||||||
|
inkscape:export-ydpi="6"
|
||||||
|
inkscape:version="1.4.3 (0d15f75, 2025-12-25)"
|
||||||
|
sodipodi:docname="Icon.svg"
|
||||||
|
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||||
|
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
xmlns:svg="http://www.w3.org/2000/svg"><sodipodi:namedview
|
||||||
|
id="namedview1"
|
||||||
|
pagecolor="#ffffff"
|
||||||
|
bordercolor="#000000"
|
||||||
|
borderopacity="0.25"
|
||||||
|
inkscape:showpageshadow="2"
|
||||||
|
inkscape:pageopacity="0.0"
|
||||||
|
inkscape:pagecheckerboard="0"
|
||||||
|
inkscape:deskcolor="#d1d1d1"
|
||||||
|
inkscape:document-units="px"
|
||||||
|
showguides="true"
|
||||||
|
inkscape:zoom="1.6884766"
|
||||||
|
inkscape:cx="175.00983"
|
||||||
|
inkscape:cy="258.81319"
|
||||||
|
inkscape:window-width="2260"
|
||||||
|
inkscape:window-height="1251"
|
||||||
|
inkscape:window-x="0"
|
||||||
|
inkscape:window-y="30"
|
||||||
|
inkscape:window-maximized="0"
|
||||||
|
inkscape:current-layer="layer1" /><defs
|
||||||
|
id="defs1"><linearGradient
|
||||||
|
id="swatch428"
|
||||||
|
inkscape:swatch="solid"><stop
|
||||||
|
style="stop-color:#a4e0d3;stop-opacity:1;"
|
||||||
|
offset="0"
|
||||||
|
id="stop429" /></linearGradient><inkscape:path-effect
|
||||||
|
effect="fillet_chamfer"
|
||||||
|
id="path-effect9"
|
||||||
|
is_visible="true"
|
||||||
|
lpeversion="1"
|
||||||
|
nodesatellites_param="F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ IF,0,0,1,0,16,0,1 @ F,0,0,1,0,0,0,1"
|
||||||
|
radius="0"
|
||||||
|
unit="px"
|
||||||
|
method="auto"
|
||||||
|
mode="F"
|
||||||
|
chamfer_steps="1"
|
||||||
|
flexible="false"
|
||||||
|
use_knot_distance="true"
|
||||||
|
apply_no_radius="true"
|
||||||
|
apply_with_radius="true"
|
||||||
|
only_selected="false"
|
||||||
|
hide_knots="false" /><inkscape:path-effect
|
||||||
|
effect="fillet_chamfer"
|
||||||
|
id="path-effect8"
|
||||||
|
is_visible="true"
|
||||||
|
lpeversion="1"
|
||||||
|
nodesatellites_param="F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ IF,0,0,1,0,16,0,1"
|
||||||
|
radius="0"
|
||||||
|
unit="px"
|
||||||
|
method="auto"
|
||||||
|
mode="F"
|
||||||
|
chamfer_steps="1"
|
||||||
|
flexible="false"
|
||||||
|
use_knot_distance="true"
|
||||||
|
apply_no_radius="true"
|
||||||
|
apply_with_radius="true"
|
||||||
|
only_selected="false"
|
||||||
|
hide_knots="false" /><inkscape:path-effect
|
||||||
|
effect="fillet_chamfer"
|
||||||
|
id="path-effect7"
|
||||||
|
is_visible="true"
|
||||||
|
lpeversion="1"
|
||||||
|
nodesatellites_param="F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,16,0,1 @ F,0,0,1,0,16,0,1"
|
||||||
|
radius="0"
|
||||||
|
unit="px"
|
||||||
|
method="auto"
|
||||||
|
mode="F"
|
||||||
|
chamfer_steps="1"
|
||||||
|
flexible="false"
|
||||||
|
use_knot_distance="true"
|
||||||
|
apply_no_radius="true"
|
||||||
|
apply_with_radius="true"
|
||||||
|
only_selected="false"
|
||||||
|
hide_knots="false" /><inkscape:path-effect
|
||||||
|
effect="fillet_chamfer"
|
||||||
|
id="path-effect6"
|
||||||
|
is_visible="true"
|
||||||
|
lpeversion="1"
|
||||||
|
nodesatellites_param="F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1"
|
||||||
|
radius="0"
|
||||||
|
unit="px"
|
||||||
|
method="auto"
|
||||||
|
mode="F"
|
||||||
|
chamfer_steps="1"
|
||||||
|
flexible="false"
|
||||||
|
use_knot_distance="true"
|
||||||
|
apply_no_radius="true"
|
||||||
|
apply_with_radius="true"
|
||||||
|
only_selected="false"
|
||||||
|
hide_knots="false" /><inkscape:path-effect
|
||||||
|
effect="fillet_chamfer"
|
||||||
|
id="path-effect5"
|
||||||
|
is_visible="true"
|
||||||
|
lpeversion="1"
|
||||||
|
nodesatellites_param="F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1"
|
||||||
|
radius="0"
|
||||||
|
unit="px"
|
||||||
|
method="auto"
|
||||||
|
mode="F"
|
||||||
|
chamfer_steps="1"
|
||||||
|
flexible="false"
|
||||||
|
use_knot_distance="true"
|
||||||
|
apply_no_radius="true"
|
||||||
|
apply_with_radius="true"
|
||||||
|
only_selected="false"
|
||||||
|
hide_knots="false" /><inkscape:path-effect
|
||||||
|
effect="fillet_chamfer"
|
||||||
|
id="path-effect4"
|
||||||
|
is_visible="true"
|
||||||
|
lpeversion="1"
|
||||||
|
nodesatellites_param="F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1"
|
||||||
|
radius="0"
|
||||||
|
unit="px"
|
||||||
|
method="auto"
|
||||||
|
mode="F"
|
||||||
|
chamfer_steps="1"
|
||||||
|
flexible="false"
|
||||||
|
use_knot_distance="true"
|
||||||
|
apply_no_radius="true"
|
||||||
|
apply_with_radius="true"
|
||||||
|
only_selected="false"
|
||||||
|
hide_knots="false" /></defs><g
|
||||||
|
inkscape:label="Layer 1"
|
||||||
|
inkscape:groupmode="layer"
|
||||||
|
id="layer1"><rect
|
||||||
|
style="fill:#ddf4f7;fill-opacity:1;stroke:#ffffff;stroke-width:0;stroke-dasharray:none;stroke-opacity:1"
|
||||||
|
id="rect9"
|
||||||
|
width="512"
|
||||||
|
height="288"
|
||||||
|
x="0"
|
||||||
|
y="224" /><rect
|
||||||
|
style="fill:#000000;stroke-width:0.935414"
|
||||||
|
id="rect1"
|
||||||
|
width="512"
|
||||||
|
height="224"
|
||||||
|
x="0"
|
||||||
|
y="0" /><path
|
||||||
|
style="fill:#000000"
|
||||||
|
id="rect4"
|
||||||
|
width="128"
|
||||||
|
height="64"
|
||||||
|
x="191.59363"
|
||||||
|
y="255.87695"
|
||||||
|
sodipodi:type="rect"
|
||||||
|
inkscape:path-effect="#path-effect7"
|
||||||
|
d="m 191.59363,255.87695 h 128 v 48 a 16,16 135 0 1 -16,16 h -96 a 16,16 45 0 1 -16,-16 z"
|
||||||
|
transform="matrix(1.5,0,0,1,-127.39044,-31.876953)" /><path
|
||||||
|
style="fill:#000000"
|
||||||
|
id="rect7"
|
||||||
|
width="16"
|
||||||
|
height="16"
|
||||||
|
x="163.53511"
|
||||||
|
y="255.87695"
|
||||||
|
inkscape:path-effect="#path-effect8"
|
||||||
|
sodipodi:type="rect"
|
||||||
|
d="m 163.53511,255.87695 h 16 v 16 a 16,16 45 0 0 -16,-16 z"
|
||||||
|
transform="translate(-19.53511,-31.876953)" /><path
|
||||||
|
style="fill:#000000"
|
||||||
|
id="rect8"
|
||||||
|
width="16"
|
||||||
|
height="16"
|
||||||
|
x="319.96298"
|
||||||
|
y="255.85194"
|
||||||
|
inkscape:path-effect="#path-effect9"
|
||||||
|
sodipodi:type="rect"
|
||||||
|
d="m 319.96298,255.85194 h 16 a 16,16 135 0 0 -16,16 z"
|
||||||
|
transform="translate(32.037018,-31.851944)" /><g
|
||||||
|
style="fill:none;stroke:#ffffff;stroke-opacity:1"
|
||||||
|
id="g9"
|
||||||
|
transform="matrix(1.9934648,0,0,1.999414,232.01775,232.00703)"><path
|
||||||
|
id="Vector"
|
||||||
|
d="M 17,15 H 12 M 7,10 10,12.5 7,15 m -4,0.8002 v -7.6 C 3,7.08009 3,6.51962 3.21799,6.0918 3.40973,5.71547 3.71547,5.40973 4.0918,5.21799 4.51962,5 5.08009,5 6.2002,5 h 11.6 c 1.1201,0 1.6794,0 2.1072,0.21799 0.3763,0.19174 0.6831,0.49748 0.8748,0.87381 C 21,6.5192 21,7.07899 21,8.19691 v 7.60619 c 0,1.1179 0,1.6769 -0.2178,2.1043 -0.1917,0.3763 -0.4985,0.6831 -0.8748,0.8748 C 19.48,19 18.921,19 17.8031,19 H 6.19691 C 5.07899,19 4.5192,19 4.0918,18.7822 3.71547,18.5905 3.40973,18.2837 3.21799,17.9074 3,17.4796 3,16.9203 3,15.8002 Z"
|
||||||
|
stroke="#000000"
|
||||||
|
stroke-width="2"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
style="stroke:#ffffff;stroke-opacity:1" /></g></g></svg>
|
||||||
|
After Width: | Height: | Size: 6.7 KiB |