Compare commits
4 Commits
v0.2.0
...
c8cb209165
| Author | SHA1 | Date | |
|---|---|---|---|
|
c8cb209165
|
|||
|
645af1f660
|
|||
|
bb87d7d84c
|
|||
|
507d77a0de
|
5
.gitignore
vendored
5
.gitignore
vendored
@@ -80,4 +80,7 @@ dist/
|
||||
build/
|
||||
|
||||
# Mac... files
|
||||
**/.DS_Store
|
||||
**/.DS_Store
|
||||
|
||||
# Releases
|
||||
releases/
|
||||
@@ -15,6 +15,7 @@
|
||||
187F4B521BFC3BD29ADA79E3 /* HotkeyManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = EEB7FE2BDE5C25B7E599F340 /* HotkeyManager.swift */; };
|
||||
1AB4A0F1BE668D3130EFBA93 /* TerminalTheme.swift in Sources */ = {isa = PBXBuildFile; fileRef = CFE9E5BADB0C427903A0D874 /* TerminalTheme.swift */; };
|
||||
2089566A2BBAA65EA82119B3 /* NotchOrchestratorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF6D136A0B3FC79DDE12A826 /* NotchOrchestratorTests.swift */; };
|
||||
21373D6E9C2F34FD63CEC6A5 /* TerminalScrollCoordinatorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 40D9E323185F6D722B360C3C /* TerminalScrollCoordinatorTests.swift */; };
|
||||
2375B9DA559A0777FE558A8B /* WorkspaceStoreTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4FA62DFE9AD003F4C5B55F14 /* WorkspaceStoreTests.swift */; };
|
||||
23E2DDCF36D0DAB2EA72C39C /* NotchState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B352301BC9CAD7C9D8B7AA9 /* NotchState.swift */; };
|
||||
26AE379040149CBE05B314BB /* WorkspacesSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F70A31EFACF23DD9262A040E /* WorkspacesSettingsView.swift */; };
|
||||
@@ -22,6 +23,7 @@
|
||||
2DF22798D3A7514E2A9183FC /* WorkspaceSummary.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B2BCA543CE54DAB1DB80E43 /* WorkspaceSummary.swift */; };
|
||||
34AF69BF7AF8DC78ADE3774A /* GeneralSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27E7EEEDFBD8D9CEB8FD86A5 /* GeneralSettingsView.swift */; };
|
||||
3B69CB3CDEC2E5F2DCE600F9 /* NotchSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 297BA3E5B837FBDDEED9DE66 /* NotchSettings.swift */; };
|
||||
40183737D8C237022D0882FD /* TerminalScrollbackEstimatorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D77DC2AED643512C786AD70A /* TerminalScrollbackEstimatorTests.swift */; };
|
||||
4CFA0C79095ACE0A327A2469 /* WorkspaceRegistry.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E97758F68FACCFFACA895B7 /* WorkspaceRegistry.swift */; };
|
||||
4D335F67B71F7DD977B6AEF9 /* AppSettingsStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E6C98C406899F4B242075AF /* AppSettingsStore.swift */; };
|
||||
4E0AD14B7427532271E485AA /* NotchWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = EFAC70814C72BAF76D90B9DF /* NotchWindow.swift */; };
|
||||
@@ -56,6 +58,7 @@
|
||||
D4CEB4B895F75D91FA892A06 /* PopoutWindowController.swift in Sources */ = {isa = PBXBuildFile; fileRef = CB593A2546BF2C0BE8E40387 /* PopoutWindowController.swift */; };
|
||||
DAD3AB4A0DAADA32C02D959E /* SettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3125FD3DC55420122CF85D80 /* SettingsView.swift */; };
|
||||
DCFD5B03E64A46783F46726B /* TerminalCommandArrowBehavior.swift in Sources */ = {isa = PBXBuildFile; fileRef = F22AA47452CF798A977A6F47 /* TerminalCommandArrowBehavior.swift */; };
|
||||
DD116B0FCC341D66F1534EC4 /* TerminalScrollbackEstimator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 165DCCD7BB164A6470D49BBF /* TerminalScrollbackEstimator.swift */; };
|
||||
E63FD5862C0EC54E284F6A0F /* ScreenRegistry.swift in Sources */ = {isa = PBXBuildFile; fileRef = AAF23753B5A0CAF04D7566A3 /* ScreenRegistry.swift */; };
|
||||
E792411FA82E79E810F4B4C3 /* HotkeyRecorderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2B81432CECBDB61D21EE4DC3 /* HotkeyRecorderView.swift */; };
|
||||
EE72479BA5A25FF31BACCC50 /* NotchOrchestrator.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC7912AF01E1600B8619AF31 /* NotchOrchestrator.swift */; };
|
||||
@@ -82,6 +85,7 @@
|
||||
|
||||
/* Begin PBXFileReference section */
|
||||
0E97758F68FACCFFACA895B7 /* WorkspaceRegistry.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WorkspaceRegistry.swift; sourceTree = "<group>"; };
|
||||
165DCCD7BB164A6470D49BBF /* TerminalScrollbackEstimator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TerminalScrollbackEstimator.swift; sourceTree = "<group>"; };
|
||||
27E7EEEDFBD8D9CEB8FD86A5 /* GeneralSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GeneralSettingsView.swift; sourceTree = "<group>"; };
|
||||
297BA3E5B837FBDDEED9DE66 /* NotchSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotchSettings.swift; sourceTree = "<group>"; };
|
||||
2B81432CECBDB61D21EE4DC3 /* HotkeyRecorderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HotkeyRecorderView.swift; sourceTree = "<group>"; };
|
||||
@@ -91,6 +95,7 @@
|
||||
3CB1DFD6FCDF64B4DF24230A /* WorkspaceController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WorkspaceController.swift; sourceTree = "<group>"; };
|
||||
3F57837A7115DEEE11E14B40 /* NotchShape.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotchShape.swift; sourceTree = "<group>"; };
|
||||
3F5FF5623898FA150C3B70D4 /* AppSettingsControllerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppSettingsControllerTests.swift; sourceTree = "<group>"; };
|
||||
40D9E323185F6D722B360C3C /* TerminalScrollCoordinatorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TerminalScrollCoordinatorTests.swift; sourceTree = "<group>"; };
|
||||
48198AFE5473B0F7AECAB3FB /* AboutSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AboutSettingsView.swift; sourceTree = "<group>"; };
|
||||
496267F03E261FEC9EBD5A9D /* CommandNotchUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = CommandNotchUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
49E1791BB45E1505500ACC67 /* TerminalSession.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TerminalSession.swift; sourceTree = "<group>"; };
|
||||
@@ -124,6 +129,7 @@
|
||||
CFE9E5BADB0C427903A0D874 /* TerminalTheme.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TerminalTheme.swift; sourceTree = "<group>"; };
|
||||
D03D042117E59DCA9D553844 /* HotkeySettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HotkeySettingsView.swift; sourceTree = "<group>"; };
|
||||
D288132700770C4A625A15F6 /* WindowFrameCalculatorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WindowFrameCalculatorTests.swift; sourceTree = "<group>"; };
|
||||
D77DC2AED643512C786AD70A /* TerminalScrollbackEstimatorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TerminalScrollbackEstimatorTests.swift; sourceTree = "<group>"; };
|
||||
DC7912AF01E1600B8619AF31 /* NotchOrchestrator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotchOrchestrator.swift; sourceTree = "<group>"; };
|
||||
DF0FFBC96F2446687D6474F4 /* WorkspaceSwitcherView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WorkspaceSwitcherView.swift; sourceTree = "<group>"; };
|
||||
E37A6DCD9C5DE1FE11C4C1CD /* TerminalSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TerminalSettingsView.swift; sourceTree = "<group>"; };
|
||||
@@ -165,6 +171,7 @@
|
||||
7181BB1F3926B457445105E5 /* ScreenContext.swift */,
|
||||
AAF23753B5A0CAF04D7566A3 /* ScreenRegistry.swift */,
|
||||
567E85A2ED628460CEC760DB /* TerminalManager.swift */,
|
||||
165DCCD7BB164A6470D49BBF /* TerminalScrollbackEstimator.swift */,
|
||||
49E1791BB45E1505500ACC67 /* TerminalSession.swift */,
|
||||
CFE9E5BADB0C427903A0D874 /* TerminalTheme.swift */,
|
||||
3CB1DFD6FCDF64B4DF24230A /* WorkspaceController.swift */,
|
||||
@@ -218,6 +225,8 @@
|
||||
A770A63582CF9834F4E7F058 /* ScreenContextTests.swift */,
|
||||
EEC7F7D8D15A1BC4EE43DDDB /* ScreenRegistryTests.swift */,
|
||||
C0D19729317029008D81F361 /* TerminalCommandArrowBehaviorTests.swift */,
|
||||
D77DC2AED643512C786AD70A /* TerminalScrollbackEstimatorTests.swift */,
|
||||
40D9E323185F6D722B360C3C /* TerminalScrollCoordinatorTests.swift */,
|
||||
D288132700770C4A625A15F6 /* WindowFrameCalculatorTests.swift */,
|
||||
591FCE91AF83A8A8E44E1625 /* WorkspaceRegistryTests.swift */,
|
||||
4FA62DFE9AD003F4C5B55F14 /* WorkspaceStoreTests.swift */,
|
||||
@@ -370,7 +379,6 @@
|
||||
};
|
||||
};
|
||||
buildConfigurationList = C47A3896770C98F2A3E62B7A /* Build configuration list for PBXProject "CommandNotch" */;
|
||||
compatibilityVersion = "Xcode 14.0";
|
||||
developmentRegion = en;
|
||||
hasScannedForEncodings = 0;
|
||||
knownRegions = (
|
||||
@@ -383,6 +391,7 @@
|
||||
28377BE3F9997892D4929B6E /* XCRemoteSwiftPackageReference "SwiftTerm" */,
|
||||
);
|
||||
preferredProjectObjectVersion = 77;
|
||||
productRefGroup = B269158E04E8E603B61448F0 /* Products */;
|
||||
projectDirPath = "";
|
||||
projectRoot = "";
|
||||
targets = (
|
||||
@@ -423,6 +432,8 @@
|
||||
D2468B19D6F0A2C1DFDFE2B7 /* ScreenContextTests.swift in Sources */,
|
||||
8A1B2A4CD61B0D9A4BAB075B /* ScreenRegistryTests.swift in Sources */,
|
||||
CFBBE994BA6BAD4658AAB9CB /* TerminalCommandArrowBehaviorTests.swift in Sources */,
|
||||
21373D6E9C2F34FD63CEC6A5 /* TerminalScrollCoordinatorTests.swift in Sources */,
|
||||
40183737D8C237022D0882FD /* TerminalScrollbackEstimatorTests.swift in Sources */,
|
||||
0F133E8A88D2E313D90C32AD /* WindowFrameCalculatorTests.swift in Sources */,
|
||||
154F363D434A26105C5999B5 /* WorkspaceRegistryTests.swift in Sources */,
|
||||
2375B9DA559A0777FE558A8B /* WorkspaceStoreTests.swift in Sources */,
|
||||
@@ -465,6 +476,7 @@
|
||||
88113BA9B217DA579C36BEBE /* TabBar.swift in Sources */,
|
||||
DCFD5B03E64A46783F46726B /* TerminalCommandArrowBehavior.swift in Sources */,
|
||||
6F249EDFA2D654457DF385F1 /* TerminalManager.swift in Sources */,
|
||||
DD116B0FCC341D66F1534EC4 /* TerminalScrollbackEstimator.swift in Sources */,
|
||||
7A69B5AAC686174BCA54D0F0 /* TerminalSession.swift in Sources */,
|
||||
65C7DB7296C6C6A77598A1F4 /* TerminalSettingsView.swift in Sources */,
|
||||
1AB4A0F1BE668D3130EFBA93 /* TerminalTheme.swift in Sources */,
|
||||
|
||||
Binary file not shown.
@@ -5,33 +5,9 @@
|
||||
<key>SchemeUserState</key>
|
||||
<dict>
|
||||
<key>CommandNotch.xcscheme_^#shared#^_</key>
|
||||
<dict>
|
||||
<key>orderHint</key>
|
||||
<integer>0</integer>
|
||||
</dict>
|
||||
<key>CommandNotchTests.xcscheme_^#shared#^_</key>
|
||||
<dict>
|
||||
<key>orderHint</key>
|
||||
<integer>2</integer>
|
||||
</dict>
|
||||
<key>CommandNotchUITests.xcscheme_^#shared#^_</key>
|
||||
<dict>
|
||||
<key>orderHint</key>
|
||||
<integer>2</integer>
|
||||
</dict>
|
||||
<dict/>
|
||||
<key>Release-CommandNotch.xcscheme_^#shared#^_</key>
|
||||
<dict>
|
||||
<key>orderHint</key>
|
||||
<integer>1</integer>
|
||||
</dict>
|
||||
</dict>
|
||||
<key>SuppressBuildableAutocreation</key>
|
||||
<dict>
|
||||
<key>1485207FA11756EC2DF4F08B</key>
|
||||
<dict>
|
||||
<key>primary</key>
|
||||
<true/>
|
||||
</dict>
|
||||
<dict/>
|
||||
</dict>
|
||||
</dict>
|
||||
</plist>
|
||||
|
||||
@@ -33,6 +33,7 @@ class AppDelegate: NSObject, NSApplicationDelegate {
|
||||
observeSizePreferences()
|
||||
observeFontSizeChanges()
|
||||
observeTerminalThemeChanges()
|
||||
observeTerminalScrollbackChanges()
|
||||
applyUITestLaunchBehaviorIfNeeded()
|
||||
}
|
||||
|
||||
@@ -90,6 +91,16 @@ class AppDelegate: NSObject, NSApplicationDelegate {
|
||||
.store(in: &cancellables)
|
||||
}
|
||||
|
||||
private func observeTerminalScrollbackChanges() {
|
||||
settingsController.$settings
|
||||
.map(\.terminal.scrollbackLines)
|
||||
.removeDuplicates()
|
||||
.sink { scrollbackLines in
|
||||
WorkspaceRegistry.shared.updateAllWorkspacesScrollbackLines(scrollbackLines)
|
||||
}
|
||||
.store(in: &cancellables)
|
||||
}
|
||||
|
||||
private var launchArguments: [String] {
|
||||
ProcessInfo.processInfo.arguments
|
||||
}
|
||||
|
||||
@@ -48,6 +48,7 @@ struct AppSettings: Equatable, Codable {
|
||||
fontSize: NotchSettings.Defaults.terminalFontSize,
|
||||
shellPath: NotchSettings.Defaults.terminalShell,
|
||||
themeRawValue: NotchSettings.Defaults.terminalTheme,
|
||||
scrollbackLines: NotchSettings.Defaults.terminalScrollbackLines,
|
||||
sizePresetsJSON: NotchSettings.Defaults.terminalSizePresets
|
||||
),
|
||||
hotkeys: HotkeySettings(
|
||||
@@ -106,6 +107,7 @@ extension AppSettings {
|
||||
var fontSize: Double
|
||||
var shellPath: String
|
||||
var themeRawValue: String
|
||||
var scrollbackLines: Int
|
||||
var sizePresetsJSON: String
|
||||
|
||||
var theme: TerminalTheme {
|
||||
@@ -155,6 +157,7 @@ struct TerminalSessionConfiguration: Equatable {
|
||||
var fontSize: CGFloat
|
||||
var theme: TerminalTheme
|
||||
var shellPath: String
|
||||
var scrollbackLines: Int
|
||||
}
|
||||
|
||||
@MainActor
|
||||
|
||||
@@ -46,7 +46,8 @@ final class AppSettingsController: ObservableObject, TerminalSessionConfiguratio
|
||||
TerminalSessionConfiguration(
|
||||
fontSize: CGFloat(settings.terminal.fontSize),
|
||||
theme: settings.terminal.theme,
|
||||
shellPath: settings.terminal.shellPath
|
||||
shellPath: settings.terminal.shellPath,
|
||||
scrollbackLines: settings.terminal.scrollbackLines
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -52,6 +52,7 @@ struct UserDefaultsAppSettingsStore: AppSettingsStoreType {
|
||||
fontSize: double(NotchSettings.Keys.terminalFontSize, default: NotchSettings.Defaults.terminalFontSize),
|
||||
shellPath: string(NotchSettings.Keys.terminalShell, default: NotchSettings.Defaults.terminalShell),
|
||||
themeRawValue: string(NotchSettings.Keys.terminalTheme, default: NotchSettings.Defaults.terminalTheme),
|
||||
scrollbackLines: integer(NotchSettings.Keys.terminalScrollbackLines, default: NotchSettings.Defaults.terminalScrollbackLines),
|
||||
sizePresetsJSON: string(NotchSettings.Keys.terminalSizePresets, default: NotchSettings.Defaults.terminalSizePresets)
|
||||
),
|
||||
hotkeys: .init(
|
||||
@@ -101,6 +102,7 @@ struct UserDefaultsAppSettingsStore: AppSettingsStoreType {
|
||||
defaults.set(settings.terminal.fontSize, forKey: NotchSettings.Keys.terminalFontSize)
|
||||
defaults.set(settings.terminal.shellPath, forKey: NotchSettings.Keys.terminalShell)
|
||||
defaults.set(settings.terminal.themeRawValue, forKey: NotchSettings.Keys.terminalTheme)
|
||||
defaults.set(settings.terminal.scrollbackLines, forKey: NotchSettings.Keys.terminalScrollbackLines)
|
||||
defaults.set(settings.terminal.sizePresetsJSON, forKey: NotchSettings.Keys.terminalSizePresets)
|
||||
|
||||
defaults.set(settings.hotkeys.toggle.toJSON(), forKey: NotchSettings.Keys.hotkeyToggle)
|
||||
|
||||
@@ -47,6 +47,7 @@ enum NotchSettings {
|
||||
static let terminalFontSize = "terminalFontSize"
|
||||
static let terminalShell = "terminalShell"
|
||||
static let terminalTheme = "terminalTheme"
|
||||
static let terminalScrollbackLines = "terminalScrollbackLines"
|
||||
static let terminalSizePresets = "terminalSizePresets"
|
||||
static let workspaceSummaries = "workspaceSummaries"
|
||||
static let screenAssignments = "screenAssignments"
|
||||
@@ -98,6 +99,7 @@ enum NotchSettings {
|
||||
static let terminalFontSize: Double = 13
|
||||
static let terminalShell: String = ""
|
||||
static let terminalTheme: String = TerminalTheme.terminalApp.rawValue
|
||||
static let terminalScrollbackLines: Int = 500
|
||||
static let terminalSizePresets: String = TerminalSizePresetStore.defaultPresetsJSON()
|
||||
|
||||
// Default hotkey bindings as JSON
|
||||
@@ -148,6 +150,7 @@ enum NotchSettings {
|
||||
Keys.terminalFontSize: Defaults.terminalFontSize,
|
||||
Keys.terminalShell: Defaults.terminalShell,
|
||||
Keys.terminalTheme: Defaults.terminalTheme,
|
||||
Keys.terminalScrollbackLines: Defaults.terminalScrollbackLines,
|
||||
Keys.terminalSizePresets: Defaults.terminalSizePresets,
|
||||
|
||||
Keys.hotkeyToggle: Defaults.hotkeyToggle,
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
import AppKit
|
||||
import CoreText
|
||||
import SwiftTerm
|
||||
|
||||
enum TerminalScrollbackEstimator {
|
||||
private static let minimumColumns = 1
|
||||
private static let minimumRows = 1
|
||||
private static let defaultBytesPerLineOverhead = 256
|
||||
|
||||
struct Estimate: Equatable {
|
||||
let bytes: Int
|
||||
let columns: Int
|
||||
let rows: Int
|
||||
|
||||
var formattedBytes: String {
|
||||
ByteCountFormatter.string(fromByteCount: Int64(bytes), countStyle: .memory)
|
||||
}
|
||||
}
|
||||
|
||||
static func estimate(
|
||||
scrollbackLines: Int,
|
||||
fontSize: Double,
|
||||
openWidth: Double,
|
||||
openHeight: Double
|
||||
) -> Estimate {
|
||||
let safeScrollbackLines = max(0, scrollbackLines)
|
||||
let dimensions = terminalGridDimensions(
|
||||
fontSize: fontSize,
|
||||
openWidth: openWidth,
|
||||
openHeight: openHeight
|
||||
)
|
||||
let totalLines = safeScrollbackLines + dimensions.rows
|
||||
let bytesPerCell = MemoryLayout<CharData>.stride
|
||||
let bytesPerLine = (dimensions.columns * bytesPerCell) + defaultBytesPerLineOverhead
|
||||
let totalBytes = max(0, totalLines * bytesPerLine)
|
||||
|
||||
return Estimate(bytes: totalBytes, columns: dimensions.columns, rows: dimensions.rows)
|
||||
}
|
||||
|
||||
private static func terminalGridDimensions(
|
||||
fontSize: Double,
|
||||
openWidth: Double,
|
||||
openHeight: Double
|
||||
) -> (columns: Int, rows: Int) {
|
||||
let font = NSFont.monospacedSystemFont(ofSize: CGFloat(max(1, fontSize)), weight: .regular)
|
||||
let cellWidth = max(1, font.advancement(forGlyph: font.glyph(withName: "W")).width)
|
||||
let cellHeight = max(1, ceil(CTFontGetAscent(font) + CTFontGetDescent(font) + CTFontGetLeading(font)))
|
||||
|
||||
let columns = max(minimumColumns, Int(CGFloat(max(1, openWidth)) / cellWidth))
|
||||
let rows = max(minimumRows, Int(CGFloat(max(1, openHeight)) / cellHeight))
|
||||
|
||||
return (columns, rows)
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,58 @@ import AppKit
|
||||
import SwiftTerm
|
||||
import Combine
|
||||
|
||||
/// Tracks whether the terminal viewport should follow live output or preserve
|
||||
/// the user's current scrollback position.
|
||||
final class TerminalScrollCoordinator {
|
||||
private let bottomThreshold: Double
|
||||
private var suppressScrollTracking = false
|
||||
|
||||
private(set) var followsOutput = true
|
||||
private(set) var preservedScrollPosition: Double = 1
|
||||
|
||||
init(bottomThreshold: Double = 0.999) {
|
||||
self.bottomThreshold = bottomThreshold
|
||||
}
|
||||
|
||||
func terminalDidScroll(to position: Double, canScroll: Bool) {
|
||||
guard !suppressScrollTracking else { return }
|
||||
|
||||
guard canScroll else {
|
||||
followsOutput = true
|
||||
preservedScrollPosition = 1
|
||||
return
|
||||
}
|
||||
|
||||
let clampedPosition = min(max(position, 0), 1)
|
||||
if clampedPosition >= bottomThreshold {
|
||||
followsOutput = true
|
||||
preservedScrollPosition = 1
|
||||
} else {
|
||||
followsOutput = false
|
||||
preservedScrollPosition = clampedPosition
|
||||
}
|
||||
}
|
||||
|
||||
func outputRestorePosition(canScroll: Bool) -> Double? {
|
||||
guard canScroll, !followsOutput else { return nil }
|
||||
return preservedScrollPosition
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
func userDidStartTyping() -> Bool {
|
||||
let shouldJumpToBottom = !followsOutput
|
||||
followsOutput = true
|
||||
preservedScrollPosition = 1
|
||||
return shouldJumpToBottom
|
||||
}
|
||||
|
||||
func suppressTracking<T>(_ body: () -> T) -> T {
|
||||
suppressScrollTracking = true
|
||||
defer { suppressScrollTracking = false }
|
||||
return body()
|
||||
}
|
||||
}
|
||||
|
||||
/// Wraps a single SwiftTerm TerminalView + LocalProcess pair.
|
||||
@MainActor
|
||||
class TerminalSession: NSObject, ObservableObject, LocalProcessDelegate, @preconcurrency TerminalViewDelegate {
|
||||
@@ -12,7 +64,9 @@ class TerminalSession: NSObject, ObservableObject, LocalProcessDelegate, @precon
|
||||
private var keyEventMonitor: Any?
|
||||
private let backgroundColor = NSColor.black
|
||||
private let configuredShellPath: String
|
||||
private var scrollbackLines: Int
|
||||
private let launchDirectory: String
|
||||
private let scrollCoordinator = TerminalScrollCoordinator()
|
||||
|
||||
@Published var title: String = "shell"
|
||||
@Published var isRunning: Bool = true
|
||||
@@ -22,20 +76,24 @@ class TerminalSession: NSObject, ObservableObject, LocalProcessDelegate, @precon
|
||||
fontSize: CGFloat,
|
||||
theme: TerminalTheme,
|
||||
shellPath: String,
|
||||
scrollbackLines: Int,
|
||||
initialDirectory: String? = nil,
|
||||
startImmediately: Bool = true
|
||||
) {
|
||||
terminalView = TerminalView(frame: NSRect(x: 0, y: 0, width: 600, height: 300))
|
||||
configuredShellPath = shellPath
|
||||
self.scrollbackLines = max(0, scrollbackLines)
|
||||
launchDirectory = Self.resolveInitialDirectory(initialDirectory)
|
||||
currentDirectory = launchDirectory
|
||||
super.init()
|
||||
|
||||
terminalView.terminalDelegate = self
|
||||
installOsc52ClipboardHandler()
|
||||
|
||||
let font = NSFont.monospacedSystemFont(ofSize: fontSize, weight: .regular)
|
||||
terminalView.font = font
|
||||
applyTheme(theme)
|
||||
updateScrollbackLines(self.scrollbackLines)
|
||||
installCommandArrowMonitor()
|
||||
|
||||
if startImmediately {
|
||||
@@ -96,6 +154,23 @@ class TerminalSession: NSObject, ObservableObject, LocalProcessDelegate, @precon
|
||||
return ProcessInfo.processInfo.environment["SHELL"] ?? "/bin/zsh"
|
||||
}
|
||||
|
||||
private func installOsc52ClipboardHandler() {
|
||||
let maxPayloadSize = 1_048_576 // 1 MB
|
||||
terminalView.getTerminal().registerOscHandler(code: 52) { [weak self] data in
|
||||
guard data.count >= 2,
|
||||
data[data.startIndex] == UInt8(ascii: "c"),
|
||||
data[data.startIndex + 1] == UInt8(ascii: ";") else { return }
|
||||
|
||||
let base64 = Data(data[(data.startIndex + 2)...])
|
||||
guard let content = Data(base64Encoded: base64),
|
||||
content.count <= maxPayloadSize,
|
||||
let string = String(data: content, encoding: .utf8) else { return }
|
||||
|
||||
NSPasteboard.general.clearContents()
|
||||
NSPasteboard.general.setString(string, forType: .string)
|
||||
}
|
||||
}
|
||||
|
||||
private func installCommandArrowMonitor() {
|
||||
keyEventMonitor = NSEvent.addLocalMonitorForEvents(matching: .keyDown) { [weak self] event in
|
||||
guard let self else { return event }
|
||||
@@ -128,6 +203,12 @@ class TerminalSession: NSObject, ObservableObject, LocalProcessDelegate, @precon
|
||||
terminalView.installColors(theme.ansiColors)
|
||||
}
|
||||
|
||||
func updateScrollbackLines(_ scrollbackLines: Int) {
|
||||
let sanitizedScrollbackLines = max(0, scrollbackLines)
|
||||
self.scrollbackLines = sanitizedScrollbackLines
|
||||
terminalView.getTerminal().changeHistorySize(sanitizedScrollbackLines)
|
||||
}
|
||||
|
||||
func terminate() {
|
||||
process?.terminate()
|
||||
process = nil
|
||||
@@ -137,12 +218,43 @@ class TerminalSession: NSObject, ObservableObject, LocalProcessDelegate, @precon
|
||||
// MARK: - LocalProcessDelegate
|
||||
|
||||
nonisolated func processTerminated(_ source: LocalProcess, exitCode: Int32?) {
|
||||
Task { @MainActor in self.isRunning = false }
|
||||
Task { @MainActor in
|
||||
self.isRunning = false
|
||||
self.resetTerminalModes()
|
||||
}
|
||||
}
|
||||
|
||||
private func resetTerminalModes() {
|
||||
let resetSequences: [[UInt8]] = [
|
||||
Array("\u{1b}[?9l".utf8),
|
||||
Array("\u{1b}[?1000l".utf8),
|
||||
Array("\u{1b}[?1002l".utf8),
|
||||
Array("\u{1b}[?1003l".utf8),
|
||||
Array("\u{1b}[?1006l".utf8),
|
||||
Array("\u{1b}[?1015l".utf8),
|
||||
Array("\u{1b}[?2004l".utf8),
|
||||
Array("\u{1b}[?1l".utf8),
|
||||
Array("\u{1b}[?1049l".utf8),
|
||||
Array("\u{1b}[?25h".utf8),
|
||||
]
|
||||
|
||||
for seq in resetSequences {
|
||||
terminalView.feed(byteArray: seq[...])
|
||||
}
|
||||
}
|
||||
|
||||
nonisolated func dataReceived(slice: ArraySlice<UInt8>) {
|
||||
let data = slice
|
||||
Task { @MainActor in self.terminalView.feed(byteArray: data) }
|
||||
Task { @MainActor in
|
||||
if let restorePosition = self.scrollCoordinator.outputRestorePosition(canScroll: self.terminalView.canScroll) {
|
||||
self.scrollCoordinator.suppressTracking {
|
||||
self.terminalView.feed(byteArray: data)
|
||||
self.terminalView.scroll(toPosition: restorePosition)
|
||||
}
|
||||
} else {
|
||||
self.terminalView.feed(byteArray: data)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
nonisolated func getWindowSize() -> winsize {
|
||||
@@ -155,6 +267,9 @@ class TerminalSession: NSObject, ObservableObject, LocalProcessDelegate, @precon
|
||||
// MARK: - TerminalViewDelegate
|
||||
|
||||
func send(source: TerminalView, data: ArraySlice<UInt8>) {
|
||||
if scrollCoordinator.userDidStartTyping() {
|
||||
terminalView.scroll(toPosition: 1)
|
||||
}
|
||||
process?.send(data: data)
|
||||
}
|
||||
|
||||
@@ -179,7 +294,9 @@ class TerminalSession: NSObject, ObservableObject, LocalProcessDelegate, @precon
|
||||
currentDirectory = normalizedDirectory
|
||||
}
|
||||
|
||||
func scrolled(source: TerminalView, position: Double) {}
|
||||
func scrolled(source: TerminalView, position: Double) {
|
||||
scrollCoordinator.terminalDidScroll(to: position, canScroll: source.canScroll)
|
||||
}
|
||||
func rangeChanged(source: TerminalView, startY: Int, endY: Int) {}
|
||||
|
||||
func clipboardCopy(source: TerminalView, content: Data) {
|
||||
|
||||
@@ -7,6 +7,7 @@ protocol TerminalSessionFactoryType {
|
||||
fontSize: CGFloat,
|
||||
theme: TerminalTheme,
|
||||
shellPath: String,
|
||||
scrollbackLines: Int,
|
||||
initialDirectory: String?
|
||||
) -> TerminalSession
|
||||
}
|
||||
@@ -16,12 +17,14 @@ struct LiveTerminalSessionFactory: TerminalSessionFactoryType {
|
||||
fontSize: CGFloat,
|
||||
theme: TerminalTheme,
|
||||
shellPath: String,
|
||||
scrollbackLines: Int,
|
||||
initialDirectory: String?
|
||||
) -> TerminalSession {
|
||||
TerminalSession(
|
||||
fontSize: fontSize,
|
||||
theme: theme,
|
||||
shellPath: shellPath,
|
||||
scrollbackLines: scrollbackLines,
|
||||
initialDirectory: initialDirectory
|
||||
)
|
||||
}
|
||||
@@ -106,6 +109,7 @@ final class WorkspaceController: ObservableObject {
|
||||
fontSize: config.fontSize,
|
||||
theme: config.theme,
|
||||
shellPath: config.shellPath,
|
||||
scrollbackLines: config.scrollbackLines,
|
||||
initialDirectory: activeTab?.currentDirectory
|
||||
)
|
||||
|
||||
@@ -187,4 +191,10 @@ final class WorkspaceController: ObservableObject {
|
||||
tab.applyTheme(theme)
|
||||
}
|
||||
}
|
||||
|
||||
func updateAllScrollbackLines(_ scrollbackLines: Int) {
|
||||
for tab in tabs {
|
||||
tab.updateScrollbackLines(scrollbackLines)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -157,6 +157,12 @@ final class WorkspaceRegistry: ObservableObject {
|
||||
}
|
||||
}
|
||||
|
||||
func updateAllWorkspacesScrollbackLines(_ scrollbackLines: Int) {
|
||||
for controller in controllers.values {
|
||||
controller.updateAllScrollbackLines(scrollbackLines)
|
||||
}
|
||||
}
|
||||
|
||||
private func resolvedWorkspaceName(from proposedName: String?) -> String {
|
||||
let trimmed = proposedName?.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
|
||||
if !trimmed.isEmpty {
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import SwiftUI
|
||||
|
||||
struct TerminalSettingsView: View {
|
||||
private static let scrollbackRange = 0...1_000_000
|
||||
|
||||
@ObservedObject private var settingsController = AppSettingsController.shared
|
||||
@State private var sizePresets: [TerminalSizePreset] = []
|
||||
|
||||
@@ -40,6 +42,34 @@ struct TerminalSettingsView: View {
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
|
||||
Section("Scrollback") {
|
||||
HStack {
|
||||
Text("Lines")
|
||||
Spacer()
|
||||
TextField(
|
||||
"Scrollback lines",
|
||||
value: scrollbackBinding,
|
||||
format: .number.grouping(.automatic)
|
||||
)
|
||||
.textFieldStyle(.roundedBorder)
|
||||
.frame(width: 140)
|
||||
}
|
||||
|
||||
Stepper(
|
||||
value: scrollbackBinding,
|
||||
in: Self.scrollbackRange,
|
||||
step: scrollbackStepSize
|
||||
) {
|
||||
Text("\(settingsController.settings.terminal.scrollbackLines.formatted()) lines")
|
||||
.monospacedDigit()
|
||||
}
|
||||
|
||||
let estimate = scrollbackEstimate
|
||||
Text("Based on the current terminal size, this may use ~\(estimate.formattedBytes) of RAM (\(estimate.columns) columns x \(estimate.rows) rows visible).")
|
||||
.font(.caption)
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
|
||||
Section("Size Presets") {
|
||||
ForEach($sizePresets) { $preset in
|
||||
TerminalSizePresetEditor(
|
||||
@@ -94,6 +124,39 @@ struct TerminalSettingsView: View {
|
||||
sizePresets.removeAll { $0.id == id }
|
||||
}
|
||||
|
||||
private var scrollbackBinding: Binding<Int> {
|
||||
Binding(
|
||||
get: { settingsController.settings.terminal.scrollbackLines },
|
||||
set: { newValue in
|
||||
let clampedValue = min(max(Self.scrollbackRange.lowerBound, newValue), Self.scrollbackRange.upperBound)
|
||||
settingsController.update {
|
||||
$0.terminal.scrollbackLines = clampedValue
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
private var scrollbackEstimate: TerminalScrollbackEstimator.Estimate {
|
||||
TerminalScrollbackEstimator.estimate(
|
||||
scrollbackLines: settingsController.settings.terminal.scrollbackLines,
|
||||
fontSize: settingsController.settings.terminal.fontSize,
|
||||
openWidth: settingsController.settings.display.openWidth,
|
||||
openHeight: settingsController.settings.display.openHeight
|
||||
)
|
||||
}
|
||||
|
||||
private var scrollbackStepSize: Int {
|
||||
let lines = settingsController.settings.terminal.scrollbackLines
|
||||
switch lines {
|
||||
case ..<10_000:
|
||||
return 500
|
||||
case ..<100_000:
|
||||
return 5_000
|
||||
default:
|
||||
return 10_000
|
||||
}
|
||||
}
|
||||
|
||||
private func applyPreset(_ preset: TerminalSizePreset) {
|
||||
settingsController.update {
|
||||
$0.display.openWidth = preset.width
|
||||
|
||||
@@ -7,11 +7,13 @@ final class AppSettingsControllerTests: XCTestCase {
|
||||
let store = InMemoryAppSettingsStore()
|
||||
var settings = AppSettings.default
|
||||
settings.terminal.shellPath = "/opt/homebrew/bin/fish"
|
||||
settings.terminal.scrollbackLines = 12_000
|
||||
store.storedSettings = settings
|
||||
|
||||
let controller = AppSettingsController(store: store)
|
||||
|
||||
XCTAssertEqual(controller.terminalSessionConfiguration.shellPath, "/opt/homebrew/bin/fish")
|
||||
XCTAssertEqual(controller.terminalSessionConfiguration.scrollbackLines, 12_000)
|
||||
}
|
||||
|
||||
func testTerminalSizePresetsDecodeFromTypedSettings() {
|
||||
|
||||
@@ -26,6 +26,7 @@ final class AppSettingsStoreTests: XCTestCase {
|
||||
settings.appearance.blurRadius = 4.5
|
||||
settings.terminal.fontSize = 16
|
||||
settings.terminal.themeRawValue = TerminalTheme.dracula.rawValue
|
||||
settings.terminal.scrollbackLines = 25_000
|
||||
settings.terminal.sizePresetsJSON = TerminalSizePresetStore.encodePresets([
|
||||
TerminalSizePreset(name: "Wide", width: 960, height: 420, hotkey: .cmdShiftDigit(4))
|
||||
])
|
||||
|
||||
@@ -306,7 +306,7 @@ private final class TestAppSettingsStore: AppSettingsStoreType {
|
||||
}
|
||||
|
||||
private final class ScreenRegistryTestSettingsProvider: TerminalSessionConfigurationProviding {
|
||||
let terminalSessionConfiguration = TerminalSessionConfiguration(fontSize: 13, theme: .terminalApp, shellPath: "")
|
||||
let terminalSessionConfiguration = TerminalSessionConfiguration(fontSize: 13, theme: .terminalApp, shellPath: "", scrollbackLines: 500)
|
||||
let hotkeySettings = AppSettings.default.hotkeys
|
||||
let terminalSizePresets = TerminalSizePresetStore.loadDefaults()
|
||||
}
|
||||
@@ -317,6 +317,7 @@ private struct ScreenRegistryUnusedTerminalSessionFactory: TerminalSessionFactor
|
||||
fontSize: CGFloat,
|
||||
theme: TerminalTheme,
|
||||
shellPath: String,
|
||||
scrollbackLines: Int,
|
||||
initialDirectory: String?
|
||||
) -> TerminalSession {
|
||||
fatalError("ScreenRegistryTests should not create live terminal sessions.")
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
import XCTest
|
||||
@testable import CommandNotch
|
||||
|
||||
final class TerminalScrollCoordinatorTests: XCTestCase {
|
||||
func testScrollAwayFromBottomDisablesOutputFollow() {
|
||||
let coordinator = TerminalScrollCoordinator()
|
||||
|
||||
coordinator.terminalDidScroll(to: 0.42, canScroll: true)
|
||||
|
||||
XCTAssertFalse(coordinator.followsOutput)
|
||||
XCTAssertEqual(coordinator.outputRestorePosition(canScroll: true) ?? .nan, 0.42, accuracy: 0.0001)
|
||||
}
|
||||
|
||||
func testTypingReEnablesFollowAndRequestsJumpToBottom() {
|
||||
let coordinator = TerminalScrollCoordinator()
|
||||
coordinator.terminalDidScroll(to: 0.42, canScroll: true)
|
||||
|
||||
let shouldJump = coordinator.userDidStartTyping()
|
||||
|
||||
XCTAssertTrue(shouldJump)
|
||||
XCTAssertTrue(coordinator.followsOutput)
|
||||
XCTAssertNil(coordinator.outputRestorePosition(canScroll: true))
|
||||
}
|
||||
|
||||
func testScrollingBackToBottomReEnablesOutputFollow() {
|
||||
let coordinator = TerminalScrollCoordinator()
|
||||
coordinator.terminalDidScroll(to: 0.42, canScroll: true)
|
||||
|
||||
coordinator.terminalDidScroll(to: 1, canScroll: true)
|
||||
|
||||
XCTAssertTrue(coordinator.followsOutput)
|
||||
XCTAssertNil(coordinator.outputRestorePosition(canScroll: true))
|
||||
}
|
||||
|
||||
func testSuppressedTrackingIgnoresProgrammaticScrollUpdates() {
|
||||
let coordinator = TerminalScrollCoordinator()
|
||||
coordinator.terminalDidScroll(to: 0.42, canScroll: true)
|
||||
|
||||
coordinator.suppressTracking {
|
||||
coordinator.terminalDidScroll(to: 1, canScroll: true)
|
||||
}
|
||||
|
||||
XCTAssertFalse(coordinator.followsOutput)
|
||||
XCTAssertEqual(coordinator.outputRestorePosition(canScroll: true) ?? .nan, 0.42, accuracy: 0.0001)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
import XCTest
|
||||
@testable import CommandNotch
|
||||
|
||||
final class TerminalScrollbackEstimatorTests: XCTestCase {
|
||||
func testEstimateIncreasesAsScrollbackGrows() {
|
||||
let small = TerminalScrollbackEstimator.estimate(
|
||||
scrollbackLines: 5_000,
|
||||
fontSize: 13,
|
||||
openWidth: 640,
|
||||
openHeight: 350
|
||||
)
|
||||
let large = TerminalScrollbackEstimator.estimate(
|
||||
scrollbackLines: 100_000,
|
||||
fontSize: 13,
|
||||
openWidth: 640,
|
||||
openHeight: 350
|
||||
)
|
||||
|
||||
XCTAssertGreaterThan(large.bytes, small.bytes)
|
||||
XCTAssertGreaterThan(large.columns, 0)
|
||||
XCTAssertGreaterThan(large.rows, 0)
|
||||
}
|
||||
|
||||
func testEstimateClampsNegativeScrollbackToZero() {
|
||||
let estimate = TerminalScrollbackEstimator.estimate(
|
||||
scrollbackLines: -1_000,
|
||||
fontSize: 13,
|
||||
openWidth: 640,
|
||||
openHeight: 350
|
||||
)
|
||||
|
||||
XCTAssertGreaterThan(estimate.bytes, 0)
|
||||
}
|
||||
}
|
||||
@@ -191,7 +191,7 @@ private final class InMemoryWorkspaceStore: WorkspaceStoreType {
|
||||
}
|
||||
|
||||
private final class TestSettingsProvider: TerminalSessionConfigurationProviding {
|
||||
let terminalSessionConfiguration = TerminalSessionConfiguration(fontSize: 13, theme: .terminalApp, shellPath: "")
|
||||
let terminalSessionConfiguration = TerminalSessionConfiguration(fontSize: 13, theme: .terminalApp, shellPath: "", scrollbackLines: 500)
|
||||
let hotkeySettings = AppSettings.default.hotkeys
|
||||
let terminalSizePresets = TerminalSizePresetStore.loadDefaults()
|
||||
}
|
||||
@@ -204,6 +204,7 @@ private final class RecordingTerminalSessionFactory: TerminalSessionFactoryType
|
||||
fontSize: CGFloat,
|
||||
theme: TerminalTheme,
|
||||
shellPath: String,
|
||||
scrollbackLines: Int,
|
||||
initialDirectory: String?
|
||||
) -> TerminalSession {
|
||||
requestedDirectories.append(initialDirectory)
|
||||
@@ -211,6 +212,7 @@ private final class RecordingTerminalSessionFactory: TerminalSessionFactoryType
|
||||
fontSize: fontSize,
|
||||
theme: theme,
|
||||
shellPath: shellPath,
|
||||
scrollbackLines: scrollbackLines,
|
||||
initialDirectory: initialDirectory,
|
||||
startImmediately: false
|
||||
)
|
||||
@@ -223,6 +225,7 @@ private struct UnusedTerminalSessionFactory: TerminalSessionFactoryType {
|
||||
fontSize: CGFloat,
|
||||
theme: TerminalTheme,
|
||||
shellPath: String,
|
||||
scrollbackLines: Int,
|
||||
initialDirectory: String?
|
||||
) -> TerminalSession {
|
||||
fatalError("WorkspaceRegistryTests should not create live terminal sessions.")
|
||||
|
||||
28
README.md
28
README.md
@@ -63,6 +63,7 @@ Click the preview above to watch the demo recording.
|
||||
- macOS 14 or later
|
||||
- Xcode 16 or later
|
||||
- Homebrew `xcodegen`
|
||||
- Homebrew `create-dmg` for release `.dmg` packaging
|
||||
|
||||
### Build
|
||||
|
||||
@@ -81,6 +82,33 @@ DEVELOPER_DIR=/Applications/Xcode.app/Contents/Developer \
|
||||
xcodebuild build -project CommandNotch.xcodeproj -scheme CommandNotch -destination 'platform=macOS'
|
||||
```
|
||||
|
||||
### Build a release `.dmg`
|
||||
|
||||
Use `create-dmg` to build the styled Finder installer window with the usual drag-to-`Applications` layout.
|
||||
|
||||
Install the packaging dependency once:
|
||||
|
||||
```bash
|
||||
brew install create-dmg
|
||||
```
|
||||
|
||||
Then build from the `app/` directory:
|
||||
|
||||
```bash
|
||||
./scripts/build-release-dmg.sh
|
||||
```
|
||||
|
||||
That produces:
|
||||
|
||||
- `releases/CommandNotch YYYY-MM-DD HH-MM-SS/CommandNotch.app`
|
||||
- `releases/CommandNotch YYYY-MM-DD HH-MM-SS/CommandNotch.dmg`
|
||||
|
||||
Notes:
|
||||
|
||||
- The script regenerates the Xcode project, archives the Release build, then packages the archived app into a styled `.dmg`.
|
||||
- The archive is written to `/tmp` and is only used as the source for the exported `.app`.
|
||||
- If you want a distributable build signed with a specific identity, make sure your Xcode signing settings are configured before running the archive step.
|
||||
|
||||
## Project Layout
|
||||
|
||||
```text
|
||||
|
||||
63
scripts/build-release-dmg.sh
Executable file
63
scripts/build-release-dmg.sh
Executable file
@@ -0,0 +1,63 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
APP_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
|
||||
PROJECT_DIR="$APP_ROOT/CommandNotch"
|
||||
|
||||
if ! command -v xcodegen >/dev/null 2>&1; then
|
||||
echo "error: xcodegen is required. Install it with: brew install xcodegen" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if ! command -v create-dmg >/dev/null 2>&1; then
|
||||
echo "error: create-dmg is required. Install it with: brew install create-dmg" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
timestamp="$(date '+%Y-%m-%d %H-%M-%S')"
|
||||
release_dir="$APP_ROOT/releases/CommandNotch $timestamp"
|
||||
archive_path="/tmp/CommandNotch-$timestamp.xcarchive"
|
||||
staging_dir="$(mktemp -d)"
|
||||
app_path="$release_dir/CommandNotch.app"
|
||||
dmg_path="$release_dir/CommandNotch.dmg"
|
||||
|
||||
cleanup() {
|
||||
rm -rf "$staging_dir"
|
||||
}
|
||||
|
||||
trap cleanup EXIT
|
||||
|
||||
mkdir -p "$release_dir"
|
||||
|
||||
cd "$PROJECT_DIR"
|
||||
xcodegen generate --spec project.yml
|
||||
|
||||
DEVELOPER_DIR="${DEVELOPER_DIR:-/Applications/Xcode.app/Contents/Developer}" \
|
||||
xcodebuild archive \
|
||||
-project CommandNotch.xcodeproj \
|
||||
-scheme CommandNotch \
|
||||
-configuration Release \
|
||||
-destination 'generic/platform=macOS' \
|
||||
-archivePath "$archive_path"
|
||||
|
||||
ditto "$archive_path/Products/Applications/CommandNotch.app" "$app_path"
|
||||
ditto "$app_path" "$staging_dir/CommandNotch.app"
|
||||
ln -s /Applications "$staging_dir/Applications"
|
||||
|
||||
create-dmg \
|
||||
--volname "CommandNotch" \
|
||||
--window-pos 200 120 \
|
||||
--window-size 720 420 \
|
||||
--icon-size 128 \
|
||||
--icon "CommandNotch.app" 180 210 \
|
||||
--icon "Applications" 540 210 \
|
||||
--hide-extension "CommandNotch.app" \
|
||||
--app-drop-link 540 210 \
|
||||
"$dmg_path" \
|
||||
"$staging_dir"
|
||||
|
||||
echo "Created:"
|
||||
echo " $app_path"
|
||||
echo " $dmg_path"
|
||||
Reference in New Issue
Block a user