Compare commits

...

2 Commits

52 changed files with 495 additions and 114 deletions

BIN
.DS_Store vendored Normal file

Binary file not shown.

View File

@@ -7,8 +7,9 @@
objects = {
/* 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 */; };
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 */; };
295653929D5B9C0E6C90D6D7 /* SwiftTerm in Frameworks */ = {isa = PBXBuildFile; productRef = 032AECA58EA4C274BE9F3320 /* SwiftTerm */; };
37FC0A7CEEA37C9DCC6A8351 /* TerminalSession.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B598809B19C892470DE7268 /* TerminalSession.swift */; };
@@ -35,6 +36,7 @@
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>"; };
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>"; };
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>"; };
@@ -42,13 +44,13 @@
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>"; };
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>"; };
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>"; };
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>"; };
9547A79F60E46F4521A70674 /* Downterm.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Downterm.entitlements; 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>"; };
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>"; };
@@ -72,7 +74,8 @@
0EF94ED46B4860C241540F0A /* Resources */ = {
isa = PBXGroup;
children = (
9547A79F60E46F4521A70674 /* Downterm.entitlements */,
0F4A88A33D93B6E100A1C002 /* Assets.xcassets */,
9547A79F60E46F4521A70674 /* CommandNotch.entitlements */,
);
path = Resources;
sourceTree = "<group>";
@@ -92,7 +95,7 @@
792DD4F8C079680683D8FF7A /* Products */ = {
isa = PBXGroup;
children = (
665CFC051CF185B71199608D /* Downterm.app */,
665CFC051CF185B71199608D /* CommandNotch.app */,
);
name = Products;
sourceTree = "<group>";
@@ -118,12 +121,12 @@
path = Extensions;
sourceTree = "<group>";
};
9E1CA4816F67033BBD52D8A3 /* Downterm */ = {
9E1CA4816F67033BBD52D8A3 /* CommandNotch */ = {
isa = PBXGroup;
children = (
5C0779490DE9020FBBC464BE /* AppDelegate.swift */,
20BA7F4716DA3909DA8BC381 /* ContentView.swift */,
4B671125208055E5334CB85E /* DowntermApp.swift */,
4B671125208055E5334CB85E /* CommandNotchApp.swift */,
F32F526005A2589010E63C76 /* Components */,
8D95E0324E6AFC9E4DC0C087 /* Extensions */,
27C90448ECAC906F0DA429C0 /* Managers */,
@@ -131,7 +134,7 @@
0EF94ED46B4860C241540F0A /* Resources */,
C2B8955F4D0A1DAA7E60326A /* Views */,
);
path = Downterm;
path = CommandNotch;
sourceTree = "<group>";
};
C2B8955F4D0A1DAA7E60326A /* Views */ = {
@@ -157,7 +160,7 @@
FC6F23514BFE2235BD4154E8 = {
isa = PBXGroup;
children = (
9E1CA4816F67033BBD52D8A3 /* Downterm */,
9E1CA4816F67033BBD52D8A3 /* CommandNotch */,
792DD4F8C079680683D8FF7A /* Products */,
);
sourceTree = "<group>";
@@ -165,23 +168,24 @@
/* End PBXGroup section */
/* Begin PBXNativeTarget section */
1485207FA11756EC2DF4F08B /* Downterm */ = {
1485207FA11756EC2DF4F08B /* CommandNotch */ = {
isa = PBXNativeTarget;
buildConfigurationList = 74CB98309F5464CDCB00C63A /* Build configuration list for PBXNativeTarget "Downterm" */;
buildConfigurationList = 74CB98309F5464CDCB00C63A /* Build configuration list for PBXNativeTarget "CommandNotch" */;
buildPhases = (
F3C6D5CD1247D246A3F6F7AB /* Sources */,
6085DF2BDFFB2A99C4ABD514 /* Frameworks */,
0F4A88A33D93B6E100A1C003 /* Resources */,
);
buildRules = (
);
dependencies = (
);
name = Downterm;
name = CommandNotch;
packageProductDependencies = (
032AECA58EA4C274BE9F3320 /* SwiftTerm */,
);
productName = Downterm;
productReference = 665CFC051CF185B71199608D /* Downterm.app */;
productName = CommandNotch;
productReference = 665CFC051CF185B71199608D /* CommandNotch.app */;
productType = "com.apple.product-type.application";
};
/* End PBXNativeTarget section */
@@ -193,7 +197,7 @@
BuildIndependentTargetsInParallel = YES;
LastUpgradeCheck = 1600;
};
buildConfigurationList = D1C4019FEAFC83BB053C9E6E /* Build configuration list for PBXProject "Downterm" */;
buildConfigurationList = D1C4019FEAFC83BB053C9E6E /* Build configuration list for PBXProject "CommandNotch" */;
compatibilityVersion = "Xcode 14.0";
developmentRegion = en;
hasScannedForEncodings = 0;
@@ -209,11 +213,22 @@
projectDirPath = "";
projectRoot = "";
targets = (
1485207FA11756EC2DF4F08B /* Downterm */,
1485207FA11756EC2DF4F08B /* CommandNotch */,
);
};
/* 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 */
F3C6D5CD1247D246A3F6F7AB /* Sources */ = {
isa = PBXSourcesBuildPhase;
@@ -221,7 +236,7 @@
files = (
81A912E3E16165D999882078 /* AppDelegate.swift in Sources */,
888C45C650327089EBD39B2E /* ContentView.swift in Sources */,
247C6F84E7ADE7AED43381E2 /* DowntermApp.swift in Sources */,
247C6F84E7ADE7AED43381E2 /* CommandNotchApp.swift in Sources */,
88EBFBB2292659EA7C42A8F9 /* HotkeyBinding.swift in Sources */,
4D5125E11B4DDBDB3DFACBAF /* HotkeyManager.swift in Sources */,
7BD705CA6A34117929B362EC /* HotkeyRecorderView.swift in Sources */,
@@ -250,18 +265,21 @@
isa = XCBuildConfiguration;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
ASSETCATALOG_COMPILER_INCLUDE_ALL_APPICON_ASSETS = 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";
COMBINE_HIDPI_IMAGES = YES;
"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 = (
"$(inherited)",
"@executable_path/../Frameworks",
);
PRODUCT_BUNDLE_IDENTIFIER = com.downterm.app;
PRODUCT_NAME = Downterm;
MARKETING_VERSION = 0.0.3;
PRODUCT_BUNDLE_IDENTIFIER = com.commandnotch.app;
PRODUCT_NAME = CommandNotch;
SDKROOT = macosx;
};
name = Release;
@@ -334,18 +352,21 @@
isa = XCBuildConfiguration;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
ASSETCATALOG_COMPILER_INCLUDE_ALL_APPICON_ASSETS = 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";
COMBINE_HIDPI_IMAGES = YES;
"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 = (
"$(inherited)",
"@executable_path/../Frameworks",
);
PRODUCT_BUNDLE_IDENTIFIER = com.downterm.app;
PRODUCT_NAME = Downterm;
MARKETING_VERSION = 0.0.3;
PRODUCT_BUNDLE_IDENTIFIER = com.commandnotch.app;
PRODUCT_NAME = CommandNotch;
SDKROOT = macosx;
};
name = Debug;
@@ -410,7 +431,7 @@
/* End XCBuildConfiguration section */
/* Begin XCConfigurationList section */
74CB98309F5464CDCB00C63A /* Build configuration list for PBXNativeTarget "Downterm" */ = {
74CB98309F5464CDCB00C63A /* Build configuration list for PBXNativeTarget "CommandNotch" */ = {
isa = XCConfigurationList;
buildConfigurations = (
7020C02C1BDF63690CC9A3AC /* Debug */,
@@ -419,7 +440,7 @@
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Debug;
};
D1C4019FEAFC83BB053C9E6E /* Build configuration list for PBXProject "Downterm" */ = {
D1C4019FEAFC83BB053C9E6E /* Build configuration list for PBXProject "CommandNotch" */ = {
isa = XCConfigurationList;
buildConfigurations = (
3595A9212275B9AEC4448C64 /* Debug */,

View File

@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<Workspace
version = "1.0">
<FileRef
location = "self:">
</FileRef>
</Workspace>

View File

@@ -16,9 +16,9 @@
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "1485207FA11756EC2DF4F08B"
BuildableName = "Downterm.app"
BlueprintName = "Downterm"
ReferencedContainer = "container:Downterm.xcodeproj">
BuildableName = "CommandNotch.app"
BlueprintName = "CommandNotch"
ReferencedContainer = "container:CommandNotch.xcodeproj">
</BuildableReference>
</BuildActionEntry>
</BuildActionEntries>
@@ -45,9 +45,9 @@
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "1485207FA11756EC2DF4F08B"
BuildableName = "Downterm.app"
BlueprintName = "Downterm"
ReferencedContainer = "container:Downterm.xcodeproj">
BuildableName = "CommandNotch.app"
BlueprintName = "CommandNotch"
ReferencedContainer = "container:CommandNotch.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</LaunchAction>
@@ -62,9 +62,9 @@
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "1485207FA11756EC2DF4F08B"
BuildableName = "Downterm.app"
BlueprintName = "Downterm"
ReferencedContainer = "container:Downterm.xcodeproj">
BuildableName = "CommandNotch.app"
BlueprintName = "CommandNotch"
ReferencedContainer = "container:CommandNotch.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</ProfileAction>

View File

@@ -16,9 +16,9 @@
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "1485207FA11756EC2DF4F08B"
BuildableName = "Downterm.app"
BlueprintName = "Downterm"
ReferencedContainer = "container:Downterm.xcodeproj">
BuildableName = "CommandNotch.app"
BlueprintName = "CommandNotch"
ReferencedContainer = "container:CommandNotch.xcodeproj">
</BuildableReference>
</BuildActionEntry>
</BuildActionEntries>
@@ -45,9 +45,9 @@
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "1485207FA11756EC2DF4F08B"
BuildableName = "Downterm.app"
BlueprintName = "Downterm"
ReferencedContainer = "container:Downterm.xcodeproj">
BuildableName = "CommandNotch.app"
BlueprintName = "CommandNotch"
ReferencedContainer = "container:CommandNotch.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</LaunchAction>
@@ -62,9 +62,9 @@
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "1485207FA11756EC2DF4F08B"
BuildableName = "Downterm.app"
BlueprintName = "Downterm"
ReferencedContainer = "container:Downterm.xcodeproj">
BuildableName = "CommandNotch.app"
BlueprintName = "CommandNotch"
ReferencedContainer = "container:CommandNotch.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</ProfileAction>

View File

@@ -4,12 +4,12 @@
<dict>
<key>SchemeUserState</key>
<dict>
<key>Downterm.xcscheme_^#shared#^_</key>
<key>CommandNotch.xcscheme_^#shared#^_</key>
<dict>
<key>orderHint</key>
<integer>0</integer>
</dict>
<key>Release-Downterm.xcscheme_^#shared#^_</key>
<key>Release-CommandNotch.xcscheme_^#shared#^_</key>
<dict>
<key>orderHint</key>
<integer>1</integer>

View File

@@ -1,10 +1,10 @@
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.
/// The notch windows and terminal sessions are managed by AppDelegate + ScreenManager.
@main
struct DowntermApp: App {
struct CommandNotchApp: App {
@NSApplicationDelegateAdaptor(AppDelegate.self) var appDelegate
@@ -12,7 +12,7 @@ struct DowntermApp: App {
private var showMenuBarIcon = NotchSettings.Defaults.showMenuBarIcon
var body: some Scene {
MenuBarExtra("Downterm", systemImage: "terminal", isInserted: $showMenuBarIcon) {
MenuBarExtra("CommandNotch", systemImage: "terminal", isInserted: $showMenuBarIcon) {
Button("Toggle Notch") {
ScreenManager.shared.toggleNotchOnActiveScreen()
}
@@ -27,7 +27,7 @@ struct DowntermApp: App {
Divider()
Button("Quit Downterm") {
Button("Quit CommandNotch") {
NSApplication.shared.terminate(nil)
}
.keyboardShortcut("Q", modifiers: .command)

View File

@@ -5,13 +5,13 @@ import SwiftUI
/// between the compact closed state and the expanded open state.
///
/// 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).
struct NotchShape: Shape {
/// Radius applied to the top-left and top-right outer corners (the "ears").
/// When close to 0, the top corners become sharp and the shape is a
/// rectangle with rounded bottom corners no visible ear ramps.
/// Radius applied to the top-left and top-right transitions where the notch
/// curves away from the screen edge. When close to 0, the top corners become
/// sharp and the shape is a rectangle with rounded bottom corners.
var topCornerRadius: CGFloat
/// 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))
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(
to: CGPoint(x: minX + topR, y: minY + topR),
control: CGPoint(x: minX, y: minY + topR)
control: CGPoint(x: minX + topR, y: minY)
)
} else {
path.addLine(to: CGPoint(x: minX, y: minY))
@@ -73,14 +73,14 @@ struct NotchShape: Shape {
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))
if topR > 0.5 {
// Top-right ear: curve back up to the top edge
// Mirror the top-left transition.
path.addQuadCurve(
to: CGPoint(x: maxX, y: minY),
control: CGPoint(x: maxX, y: minY + topR)
control: CGPoint(x: maxX - topR, y: minY)
)
} else {
path.addLine(to: CGPoint(x: maxX, y: minY))
@@ -100,8 +100,8 @@ extension NotchShape {
NotchShape(topCornerRadius: 6, bottomCornerRadius: 14)
}
/// Open-state shape: no ear ramps, just rounded bottom corners.
/// topCornerRadius is near-zero so the ears disappear and the panel
/// Open-state shape: no top-edge cut-ins, just rounded bottom corners.
/// topCornerRadius is near-zero so the top becomes effectively flat and the panel
/// extends flush to the top edge of the screen.
static var opened: NotchShape {
NotchShape(topCornerRadius: 0, bottomCornerRadius: 24)

View File

@@ -65,6 +65,16 @@ struct ContentView: View {
.animation(vm.notchState == .open ? vm.openAnimation : vm.closeAnimation, value: vm.notchSize.width)
.animation(vm.notchState == .open ? vm.openAnimation : vm.closeAnimation, value: vm.notchSize.height)
.onHover { handleHover($0) }
.onChange(of: vm.isCloseTransitionActive) { _, isClosing in
if isClosing {
hoverTask?.cancel()
} else {
scheduleHoverOpenIfNeeded()
}
}
.onDisappear {
hoverTask?.cancel()
}
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .top)
.edgesIgnoringSafeArea(.all)
}
@@ -146,20 +156,30 @@ struct ContentView: View {
private func handleHover(_ hovering: Bool) {
if hovering {
withAnimation(hoverAnimation) { vm.isHovering = true }
guard openNotchOnHover, vm.notchState == .closed else { return }
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?()
}
scheduleHoverOpenIfNeeded()
} else {
hoverTask?.cancel()
withAnimation(hoverAnimation) { vm.isHovering = false }
}
}
private func scheduleHoverOpenIfNeeded() {
hoverTask?.cancel()
guard openNotchOnHover,
vm.notchState == .closed,
!vm.isCloseTransitionActive,
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 else { return }
vm.requestOpen?()
}
}
private func abbreviate(_ title: String) -> String {
title.count <= 30 ? title : String(title.prefix(28)) + ""
}

View File

@@ -1,6 +1,6 @@
import AppKit
import SwiftUI
import SwiftTerm
import Combine
/// Manages standalone pop-out terminal windows for detached tabs.
/// 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.
private var windows: [UUID: NSWindow] = [:]
private var sessions: [UUID: TerminalSession] = [:]
private var titleObservers: [UUID: AnyCancellable] = [:]
private override init() {
super.init()
@@ -21,6 +22,12 @@ class PopoutWindowController: NSObject, NSWindowDelegate {
func popout(session: TerminalSession) {
let windowID = session.id
if let existingWindow = windows[windowID] {
existingWindow.makeKeyAndOrderFront(nil)
NSApp.activate(ignoringOtherApps: true)
return
}
let win = NSWindow(
contentRect: NSRect(x: 0, y: 0, width: 720, height: 480),
styleMask: [.titled, .closable, .resizable, .miniaturizable],
@@ -33,12 +40,14 @@ class PopoutWindowController: NSObject, NSWindowDelegate {
win.delegate = self
win.isReleasedWhenClosed = false
// Embed the terminal view directly
let tv = session.terminalView
tv.removeFromSuperview()
tv.frame = NSRect(origin: .zero, size: win.contentView!.bounds.size)
tv.autoresizingMask = [.width, .height]
win.contentView?.addSubview(tv)
let hostingView = NSHostingView(
rootView: SwiftTermView(session: session)
.frame(maxWidth: .infinity, maxHeight: .infinity)
.background(Color.black)
.preferredColorScheme(.dark)
)
hostingView.frame = NSRect(origin: .zero, size: win.contentRect(forFrameRect: win.frame).size)
win.contentView = hostingView
win.center()
win.makeKeyAndOrderFront(nil)
@@ -48,16 +57,22 @@ class PopoutWindowController: NSObject, NSWindowDelegate {
sessions[windowID] = session
// Update window title when the terminal title changes
session.$title
titleObservers[windowID] = session.$title
.receive(on: RunLoop.main)
.sink { [weak win] title in win?.title = title }
.store(in: &popoutCancellables)
}
private var popoutCancellables = Set<AnyCancellable>()
// 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) {
guard let closingWindow = notification.object as? NSWindow else { return }
@@ -66,8 +81,7 @@ class PopoutWindowController: NSObject, NSWindowDelegate {
sessions[entry.key]?.terminate()
sessions.removeValue(forKey: entry.key)
windows.removeValue(forKey: entry.key)
titleObservers.removeValue(forKey: entry.key)
}
}
}
import Combine

View File

@@ -10,6 +10,7 @@ import Combine
class ScreenManager: ObservableObject {
static let shared = ScreenManager()
private let focusRetryDelay: TimeInterval = 0.01
private(set) var windows: [String: NotchWindow] = [:]
private(set) var viewModels: [String: NotchViewModel] = [:]
@@ -91,6 +92,8 @@ class ScreenManager: ObservableObject {
guard let vm = viewModels[screenUUID],
let window = windows[screenUUID] else { return }
vm.cancelCloseTransition()
withAnimation(vm.openAnimation) {
vm.open()
}
@@ -102,18 +105,15 @@ class ScreenManager: ObservableObject {
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)
}
}
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()
}
@@ -124,7 +124,9 @@ class ScreenManager: ObservableObject {
private func detachActiveTab() {
if let session = TerminalManager.shared.detachActiveTab() {
PopoutWindowController.shared.popout(session: session)
DispatchQueue.main.async {
PopoutWindowController.shared.popout(session: session)
}
}
}
@@ -247,4 +249,20 @@ class ScreenManager: ObservableObject {
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)
}
}
}

View File

@@ -33,7 +33,7 @@ class SettingsWindowController: NSObject, NSWindowDelegate {
backing: .buffered,
defer: false
)
win.title = "Downterm Settings"
win.title = "CommandNotch Settings"
win.contentView = hostingView
win.center()
win.delegate = self

View File

@@ -11,6 +11,7 @@ class NotchViewModel: ObservableObject {
@Published var notchSize: CGSize
@Published var closedNotchSize: CGSize
@Published var isHovering: Bool = false
@Published var isCloseTransitionActive: Bool = false
let terminalManager = TerminalManager.shared
@@ -29,6 +30,8 @@ class NotchViewModel: ObservableObject {
@AppStorage(NotchSettings.Keys.closeSpringResponse) private var closeSpringResponse = NotchSettings.Defaults.closeSpringResponse
@AppStorage(NotchSettings.Keys.closeSpringDamping) private var closeSpringDamping = NotchSettings.Defaults.closeSpringDamping
private var closeTransitionTask: Task<Void, Never>?
var openAnimation: Animation {
.spring(response: openSpringResponse, dampingFraction: openSpringDamping)
}
@@ -63,4 +66,31 @@ class NotchViewModel: ObservableObject {
var openNotchSize: CGSize {
CGSize(width: openWidth, height: openHeight)
}
var closeInteractionLockDuration: TimeInterval {
max(closeSpringResponse + 0.2, 0.35)
}
func beginCloseTransition() {
closeTransitionTask?.cancel()
isCloseTransitionActive = 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
}
deinit {
closeTransitionTask?.cancel()
}
}

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 945 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 735 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 290 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 958 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 512 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

View File

@@ -0,0 +1,6 @@
{
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@@ -5,19 +5,19 @@
<key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleDisplayName</key>
<string>Downterm</string>
<string>CommandNotch</string>
<key>CFBundleExecutable</key>
<string>Downterm</string>
<string>CommandNotch</string>
<key>CFBundleIdentifier</key>
<string>com.downterm.app</string>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>Downterm</string>
<string>CommandNotch</string>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>0.2.0</string>
<string>0.0.3</string>
<key>CFBundleVersion</key>
<string>1</string>
<key>LSMinimumSystemVersion</key>
@@ -25,6 +25,6 @@
<key>LSUIElement</key>
<true/>
<key>NSHumanReadableCopyright</key>
<string>Copyright © 2026 Downterm. All rights reserved.</string>
<string>Copyright © 2026 CommandNotch. All rights reserved.</string>
</dict>
</plist>

View File

@@ -370,7 +370,7 @@ struct AboutSettingsView: View {
Image(systemName: "terminal")
.font(.system(size: 64))
.foregroundStyle(.secondary)
Text("Downterm")
Text("CommandNotch")
.font(.largeTitle.bold())
Text("Version 0.3.0")
.foregroundStyle(.secondary)

View File

@@ -1,6 +1,6 @@
name: Downterm
name: CommandNotch
options:
bundleIdPrefix: com.downterm
bundleIdPrefix: com.commandnotch
deploymentTarget:
macOS: "14.0"
xcodeVersion: "16.0"
@@ -15,34 +15,34 @@ packages:
url: https://github.com/migueldeicaza/SwiftTerm.git
from: "1.2.0"
targets:
Downterm:
CommandNotch:
type: application
platform: macOS
sources:
- path: Downterm
- path: CommandNotch
excludes:
- Resources/Info.plist
dependencies:
- package: SwiftTerm
info:
path: Downterm/Resources/Info.plist
path: CommandNotch/Resources/Info.plist
properties:
CFBundleName: Downterm
CFBundleDisplayName: Downterm
CFBundleIdentifier: com.downterm.app
CFBundleName: CommandNotch
CFBundleDisplayName: CommandNotch
CFBundleIdentifier: com.commandnotch.app
CFBundleVersion: "1"
CFBundleShortVersionString: "0.2.0"
CFBundlePackageType: APPL
CFBundleExecutable: Downterm
CFBundleExecutable: CommandNotch
LSMinimumSystemVersion: "14.0"
LSUIElement: true
NSHumanReadableCopyright: "Copyright © 2026 Downterm. All rights reserved."
NSHumanReadableCopyright: "Copyright © 2026 CommandNotch. All rights reserved."
entitlements:
path: Downterm/Resources/Downterm.entitlements
path: CommandNotch/Resources/CommandNotch.entitlements
settings:
base:
CODE_SIGN_ENTITLEMENTS: Downterm/Resources/Downterm.entitlements
INFOPLIST_FILE: Downterm/Resources/Info.plist
PRODUCT_BUNDLE_IDENTIFIER: com.downterm.app
PRODUCT_NAME: Downterm
CODE_SIGN_ENTITLEMENTS: CommandNotch/Resources/CommandNotch.entitlements
INFOPLIST_FILE: CommandNotch/Resources/Info.plist
PRODUCT_BUNDLE_IDENTIFIER: com.commandnotch.app
PRODUCT_NAME: CommandNotch
COMBINE_HIDPI_IMAGES: true

BIN
icons/.DS_Store vendored Normal file

Binary file not shown.

BIN
icons/Downterm-icon-128.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 945 B

BIN
icons/Downterm-icon-256.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

BIN
icons/Downterm-icon-32.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 290 B

BIN
icons/Downterm-icon-512.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 KiB

BIN
icons/Downterm-icon-64.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 512 B

197
icons/Icon.svg Normal file
View 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