Merge b6fb8a2570 into 50cb1bafd7
commit
a034b39996
|
|
@ -971,6 +971,7 @@ ghostty_config_t ghostty_config_new();
|
|||
void ghostty_config_free(ghostty_config_t);
|
||||
ghostty_config_t ghostty_config_clone(ghostty_config_t);
|
||||
void ghostty_config_load_cli_args(ghostty_config_t);
|
||||
void ghostty_config_load_file(ghostty_config_t, const char*);
|
||||
void ghostty_config_load_default_files(ghostty_config_t);
|
||||
void ghostty_config_load_recursive_files(ghostty_config_t);
|
||||
void ghostty_config_finalize(ghostty_config_t);
|
||||
|
|
|
|||
|
|
@ -28,6 +28,13 @@
|
|||
/* End PBXBuildFile section */
|
||||
|
||||
/* Begin PBXContainerItemProxy section */
|
||||
810ACCA52E9D3302004F8F92 /* PBXContainerItemProxy */ = {
|
||||
isa = PBXContainerItemProxy;
|
||||
containerPortal = A5B30529299BEAAA0047F10C /* Project object */;
|
||||
proxyType = 1;
|
||||
remoteGlobalIDString = A5B30530299BEAAA0047F10C;
|
||||
remoteInfo = Ghostty;
|
||||
};
|
||||
A54F45F72E1F047A0046BD5C /* PBXContainerItemProxy */ = {
|
||||
isa = PBXContainerItemProxy;
|
||||
containerPortal = A5B30529299BEAAA0047F10C /* Project object */;
|
||||
|
|
@ -42,6 +49,7 @@
|
|||
3B39CAA42B33949B00DABEB8 /* GhosttyReleaseLocal.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = GhosttyReleaseLocal.entitlements; sourceTree = "<group>"; };
|
||||
55154BDF2B33911F001622DC /* ghostty */ = {isa = PBXFileReference; lastKnownFileType = folder; name = ghostty; path = "../zig-out/share/ghostty"; sourceTree = "<group>"; };
|
||||
552964E52B34A9B400030505 /* vim */ = {isa = PBXFileReference; lastKnownFileType = folder; name = vim; path = "../zig-out/share/vim"; sourceTree = "<group>"; };
|
||||
810ACC9F2E9D3301004F8F92 /* GhosttyUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = GhosttyUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
9351BE8E2D22937F003B3499 /* nvim */ = {isa = PBXFileReference; lastKnownFileType = folder; name = nvim; path = "../zig-out/share/nvim"; sourceTree = "<group>"; };
|
||||
A51BFC282B30F26D00E92F16 /* GhosttyDebug.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = GhosttyDebug.entitlements; sourceTree = "<group>"; };
|
||||
A546F1132D7B68D7003B11A0 /* locale */ = {isa = PBXFileReference; lastKnownFileType = folder; name = locale; path = "../zig-out/share/locale"; sourceTree = "<group>"; };
|
||||
|
|
@ -193,11 +201,19 @@
|
|||
/* End PBXFileSystemSynchronizedBuildFileExceptionSet section */
|
||||
|
||||
/* Begin PBXFileSystemSynchronizedRootGroup section */
|
||||
810ACCA02E9D3302004F8F92 /* GhosttyUITests */ = {isa = PBXFileSystemSynchronizedRootGroup; explicitFileTypes = {}; explicitFolders = (); path = GhosttyUITests; sourceTree = "<group>"; };
|
||||
81F82BC72E82815D001EDFA7 /* Sources */ = {isa = PBXFileSystemSynchronizedRootGroup; exceptions = (81F82CB12E8281F9001EDFA7 /* PBXFileSystemSynchronizedBuildFileExceptionSet */, 81F82CB02E8281F5001EDFA7 /* PBXFileSystemSynchronizedBuildFileExceptionSet */, ); explicitFileTypes = {}; explicitFolders = (); path = Sources; sourceTree = "<group>"; };
|
||||
A54F45F42E1F047A0046BD5C /* Tests */ = {isa = PBXFileSystemSynchronizedRootGroup; explicitFileTypes = {}; explicitFolders = (); path = Tests; sourceTree = "<group>"; };
|
||||
/* End PBXFileSystemSynchronizedRootGroup section */
|
||||
|
||||
/* Begin PBXFrameworksBuildPhase section */
|
||||
810ACC9C2E9D3301004F8F92 /* Frameworks */ = {
|
||||
isa = PBXFrameworksBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
A54F45F02E1F047A0046BD5C /* Frameworks */ = {
|
||||
isa = PBXFrameworksBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
|
|
@ -254,6 +270,7 @@
|
|||
3B39CAA42B33949B00DABEB8 /* GhosttyReleaseLocal.entitlements */,
|
||||
81F82BC72E82815D001EDFA7 /* Sources */,
|
||||
A54F45F42E1F047A0046BD5C /* Tests */,
|
||||
810ACCA02E9D3302004F8F92 /* GhosttyUITests */,
|
||||
A5D495A3299BECBA00DD1313 /* Frameworks */,
|
||||
A5A1F8862A489D7400D1E8BC /* Resources */,
|
||||
A5B30532299BEAAA0047F10C /* Products */,
|
||||
|
|
@ -266,6 +283,7 @@
|
|||
A5B30531299BEAAA0047F10C /* Ghostty.app */,
|
||||
A5D4499D2B53AE7B000F5B83 /* Ghostty-iOS.app */,
|
||||
A54F45F32E1F047A0046BD5C /* GhosttyTests.xctest */,
|
||||
810ACC9F2E9D3301004F8F92 /* GhosttyUITests.xctest */,
|
||||
);
|
||||
name = Products;
|
||||
sourceTree = "<group>";
|
||||
|
|
@ -282,6 +300,29 @@
|
|||
/* End PBXGroup section */
|
||||
|
||||
/* Begin PBXNativeTarget section */
|
||||
810ACC9E2E9D3301004F8F92 /* GhosttyUITests */ = {
|
||||
isa = PBXNativeTarget;
|
||||
buildConfigurationList = 810ACCA72E9D3302004F8F92 /* Build configuration list for PBXNativeTarget "GhosttyUITests" */;
|
||||
buildPhases = (
|
||||
810ACC9B2E9D3301004F8F92 /* Sources */,
|
||||
810ACC9C2E9D3301004F8F92 /* Frameworks */,
|
||||
810ACC9D2E9D3301004F8F92 /* Resources */,
|
||||
);
|
||||
buildRules = (
|
||||
);
|
||||
dependencies = (
|
||||
810ACCA62E9D3302004F8F92 /* PBXTargetDependency */,
|
||||
);
|
||||
fileSystemSynchronizedGroups = (
|
||||
810ACCA02E9D3302004F8F92 /* GhosttyUITests */,
|
||||
);
|
||||
name = GhosttyUITests;
|
||||
packageProductDependencies = (
|
||||
);
|
||||
productName = GhosttyUITests;
|
||||
productReference = 810ACC9F2E9D3301004F8F92 /* GhosttyUITests.xctest */;
|
||||
productType = "com.apple.product-type.bundle.ui-testing";
|
||||
};
|
||||
A54F45F22E1F047A0046BD5C /* GhosttyTests */ = {
|
||||
isa = PBXNativeTarget;
|
||||
buildConfigurationList = A54F45FC2E1F047A0046BD5C /* Build configuration list for PBXNativeTarget "GhosttyTests" */;
|
||||
|
|
@ -355,9 +396,13 @@
|
|||
isa = PBXProject;
|
||||
attributes = {
|
||||
BuildIndependentTargetsInParallel = 1;
|
||||
LastSwiftUpdateCheck = 2600;
|
||||
LastSwiftUpdateCheck = 2610;
|
||||
LastUpgradeCheck = 1610;
|
||||
TargetAttributes = {
|
||||
810ACC9E2E9D3301004F8F92 = {
|
||||
CreatedOnToolsVersion = 26.1;
|
||||
TestTargetID = A5B30530299BEAAA0047F10C;
|
||||
};
|
||||
A54F45F22E1F047A0046BD5C = {
|
||||
CreatedOnToolsVersion = 26.0;
|
||||
TestTargetID = A5B30530299BEAAA0047F10C;
|
||||
|
|
@ -390,11 +435,19 @@
|
|||
A5B30530299BEAAA0047F10C /* Ghostty */,
|
||||
A5D4499C2B53AE7B000F5B83 /* Ghostty-iOS */,
|
||||
A54F45F22E1F047A0046BD5C /* GhosttyTests */,
|
||||
810ACC9E2E9D3301004F8F92 /* GhosttyUITests */,
|
||||
);
|
||||
};
|
||||
/* End PBXProject section */
|
||||
|
||||
/* Begin PBXResourcesBuildPhase section */
|
||||
810ACC9D2E9D3301004F8F92 /* Resources */ = {
|
||||
isa = PBXResourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
A54F45F12E1F047A0046BD5C /* Resources */ = {
|
||||
isa = PBXResourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
|
|
@ -433,6 +486,13 @@
|
|||
/* End PBXResourcesBuildPhase section */
|
||||
|
||||
/* Begin PBXSourcesBuildPhase section */
|
||||
810ACC9B2E9D3301004F8F92 /* Sources */ = {
|
||||
isa = PBXSourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
A54F45EF2E1F047A0046BD5C /* Sources */ = {
|
||||
isa = PBXSourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
|
|
@ -457,6 +517,11 @@
|
|||
/* End PBXSourcesBuildPhase section */
|
||||
|
||||
/* Begin PBXTargetDependency section */
|
||||
810ACCA62E9D3302004F8F92 /* PBXTargetDependency */ = {
|
||||
isa = PBXTargetDependency;
|
||||
target = A5B30530299BEAAA0047F10C /* Ghostty */;
|
||||
targetProxy = 810ACCA52E9D3302004F8F92 /* PBXContainerItemProxy */;
|
||||
};
|
||||
A54F45F82E1F047A0046BD5C /* PBXTargetDependency */ = {
|
||||
isa = PBXTargetDependency;
|
||||
target = A5B30530299BEAAA0047F10C /* Ghostty */;
|
||||
|
|
@ -574,6 +639,73 @@
|
|||
};
|
||||
name = ReleaseLocal;
|
||||
};
|
||||
810ACCA82E9D3302004F8F92 /* Debug */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
ENABLE_USER_SCRIPT_SANDBOXING = YES;
|
||||
GCC_C_LANGUAGE_STANDARD = gnu17;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
|
||||
MACOSX_DEPLOYMENT_TARGET = 13.0;
|
||||
MARKETING_VERSION = 1.0;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.mitchellh.GhosttyUITests;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
STRING_CATALOG_GENERATE_SYMBOLS = NO;
|
||||
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)";
|
||||
SWIFT_APPROACHABLE_CONCURRENCY = YES;
|
||||
SWIFT_EMIT_LOC_STRINGS = NO;
|
||||
SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES;
|
||||
SWIFT_VERSION = 5.0;
|
||||
TEST_TARGET_NAME = Ghostty;
|
||||
};
|
||||
name = Debug;
|
||||
};
|
||||
810ACCA92E9D3302004F8F92 /* Release */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
ENABLE_USER_SCRIPT_SANDBOXING = YES;
|
||||
GCC_C_LANGUAGE_STANDARD = gnu17;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
|
||||
MACOSX_DEPLOYMENT_TARGET = 13.0;
|
||||
MARKETING_VERSION = 1.0;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.mitchellh.GhosttyUITests;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
STRING_CATALOG_GENERATE_SYMBOLS = NO;
|
||||
SWIFT_APPROACHABLE_CONCURRENCY = YES;
|
||||
SWIFT_EMIT_LOC_STRINGS = NO;
|
||||
SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES;
|
||||
SWIFT_VERSION = 5.0;
|
||||
TEST_TARGET_NAME = Ghostty;
|
||||
};
|
||||
name = Release;
|
||||
};
|
||||
810ACCAA2E9D3302004F8F92 /* ReleaseLocal */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
ENABLE_USER_SCRIPT_SANDBOXING = YES;
|
||||
GCC_C_LANGUAGE_STANDARD = gnu17;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
|
||||
MACOSX_DEPLOYMENT_TARGET = 13.0;
|
||||
MARKETING_VERSION = 1.0;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.mitchellh.GhosttyUITests;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
STRING_CATALOG_GENERATE_SYMBOLS = NO;
|
||||
SWIFT_APPROACHABLE_CONCURRENCY = YES;
|
||||
SWIFT_EMIT_LOC_STRINGS = NO;
|
||||
SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES;
|
||||
SWIFT_VERSION = 5.0;
|
||||
TEST_TARGET_NAME = Ghostty;
|
||||
};
|
||||
name = ReleaseLocal;
|
||||
};
|
||||
A54F45F92E1F047A0046BD5C /* Debug */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
|
|
@ -990,6 +1122,16 @@
|
|||
/* End XCBuildConfiguration section */
|
||||
|
||||
/* Begin XCConfigurationList section */
|
||||
810ACCA72E9D3302004F8F92 /* Build configuration list for PBXNativeTarget "GhosttyUITests" */ = {
|
||||
isa = XCConfigurationList;
|
||||
buildConfigurations = (
|
||||
810ACCA82E9D3302004F8F92 /* Debug */,
|
||||
810ACCA92E9D3302004F8F92 /* Release */,
|
||||
810ACCAA2E9D3302004F8F92 /* ReleaseLocal */,
|
||||
);
|
||||
defaultConfigurationIsVisible = 0;
|
||||
defaultConfigurationName = ReleaseLocal;
|
||||
};
|
||||
A54F45FC2E1F047A0046BD5C /* Build configuration list for PBXNativeTarget "GhosttyTests" */ = {
|
||||
isa = XCConfigurationList;
|
||||
buildConfigurations = (
|
||||
|
|
|
|||
|
|
@ -40,6 +40,17 @@
|
|||
ReferencedContainer = "container:Ghostty.xcodeproj">
|
||||
</BuildableReference>
|
||||
</TestableReference>
|
||||
<TestableReference
|
||||
skipped = "NO"
|
||||
parallelizable = "YES">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "810ACC9E2E9D3301004F8F92"
|
||||
BuildableName = "GhosttyUITests.xctest"
|
||||
BlueprintName = "GhosttyUITests"
|
||||
ReferencedContainer = "container:Ghostty.xcodeproj">
|
||||
</BuildableReference>
|
||||
</TestableReference>
|
||||
</Testables>
|
||||
</TestAction>
|
||||
<LaunchAction
|
||||
|
|
|
|||
|
|
@ -0,0 +1,34 @@
|
|||
//
|
||||
// AppKitExtensions.swift
|
||||
// Ghostty
|
||||
//
|
||||
// Created by luca on 27.10.2025.
|
||||
//
|
||||
|
||||
import AppKit
|
||||
|
||||
extension NSColor {
|
||||
var isLightColor: Bool {
|
||||
return self.luminance > 0.5
|
||||
}
|
||||
|
||||
var luminance: Double {
|
||||
var r: CGFloat = 0
|
||||
var g: CGFloat = 0
|
||||
var b: CGFloat = 0
|
||||
var a: CGFloat = 0
|
||||
|
||||
guard let rgb = self.usingColorSpace(.sRGB) else { return 0 }
|
||||
rgb.getRed(&r, green: &g, blue: &b, alpha: &a)
|
||||
return (0.299 * r) + (0.587 * g) + (0.114 * b)
|
||||
}
|
||||
}
|
||||
|
||||
extension NSImage {
|
||||
func colorAt(x: Int, y: Int) -> NSColor? {
|
||||
guard let cgImage = self.cgImage(forProposedRect: nil, context: nil, hints: nil) else {
|
||||
return nil
|
||||
}
|
||||
return NSBitmapImageRep(cgImage: cgImage).colorAt(x: x, y: y)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,59 @@
|
|||
//
|
||||
// GhosttyCustomConfigCase.swift
|
||||
// Ghostty
|
||||
//
|
||||
// Created by luca on 16.10.2025.
|
||||
//
|
||||
|
||||
import XCTest
|
||||
|
||||
class GhosttyCustomConfigCase: XCTestCase {
|
||||
/// We only want run these UI tests
|
||||
/// when testing manually with Xcode IDE
|
||||
///
|
||||
/// So that we don't have to wait for each ci check
|
||||
/// to run these tedious tests
|
||||
override class var defaultTestSuite: XCTestSuite {
|
||||
// https://lldb.llvm.org/cpp_reference/PlatformDarwin_8cpp_source.html#:~:text==%20%22-,IDE_DISABLED_OS_ACTIVITY_DT_MODE
|
||||
|
||||
if ProcessInfo.processInfo.environment["IDE_DISABLED_OS_ACTIVITY_DT_MODE"] != nil {
|
||||
return XCTestSuite(forTestCaseClass: Self.self)
|
||||
} else {
|
||||
return XCTestSuite(name: "Skipping \(className())")
|
||||
}
|
||||
}
|
||||
|
||||
override class var runsForEachTargetApplicationUIConfiguration: Bool {
|
||||
true
|
||||
}
|
||||
|
||||
var configFile: URL?
|
||||
override func setUpWithError() throws {
|
||||
continueAfterFailure = false
|
||||
}
|
||||
|
||||
override func tearDown() async throws {
|
||||
if let configFile {
|
||||
try FileManager.default.removeItem(at: configFile)
|
||||
}
|
||||
}
|
||||
|
||||
func updateConfig(_ newConfig: String) throws {
|
||||
if configFile == nil {
|
||||
let temporaryConfig = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString)
|
||||
.appendingPathExtension("ghostty")
|
||||
configFile = temporaryConfig
|
||||
}
|
||||
try newConfig.write(to: configFile!, atomically: true, encoding: .utf8)
|
||||
}
|
||||
|
||||
func ghosttyApplication() throws -> XCUIApplication {
|
||||
let app = XCUIApplication()
|
||||
app.launchArguments.append(contentsOf: ["-ApplePersistenceIgnoreState", "YES"])
|
||||
guard let configFile else {
|
||||
return app
|
||||
}
|
||||
app.launchEnvironment["GHOSTTY_CONFIG_PATH"] = configFile.path
|
||||
return app
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,159 @@
|
|||
//
|
||||
// GhosttyThemeTests.swift
|
||||
// Ghostty
|
||||
//
|
||||
// Created by luca on 27.10.2025.
|
||||
//
|
||||
|
||||
import AppKit
|
||||
import XCTest
|
||||
|
||||
final class GhosttyThemeTests: GhosttyCustomConfigCase {
|
||||
let windowTitle = "GhosttyThemeTests"
|
||||
private func assertTitlebarAppearance(
|
||||
_ appearance: XCUIDevice.Appearance,
|
||||
for app: XCUIApplication,
|
||||
title: String? = nil,
|
||||
colorLocation: CGPoint? = nil,
|
||||
file: StaticString = #filePath,
|
||||
line: UInt = #line
|
||||
) throws {
|
||||
for i in 0 ..< app.windows.count {
|
||||
let titleView = app.windows.element(boundBy: i).staticTexts.element(matching: NSPredicate(format: "value == '\(title ?? windowTitle)'"))
|
||||
|
||||
let image = titleView.screenshot().image
|
||||
guard let imageColor = image.colorAt(x: Int(colorLocation?.x ?? 1), y: Int(colorLocation?.y ?? 1)) else {
|
||||
throw XCTSkip("failed to get pixel color", file: file, line: line)
|
||||
}
|
||||
|
||||
switch appearance {
|
||||
case .dark:
|
||||
XCTAssertLessThanOrEqual(imageColor.luminance, 0.5, "Expected dark appearance for this test", file: file, line: line)
|
||||
default:
|
||||
XCTAssertGreaterThanOrEqual(imageColor.luminance, 0.5, "Expected light appearance for this test", file: file, line: line)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// https://github.com/ghostty-org/ghostty/issues/8282
|
||||
@MainActor
|
||||
func testIssue8282() async throws {
|
||||
try updateConfig("title=\(windowTitle) \n theme=light:3024 Day,dark:3024 Night")
|
||||
XCUIDevice.shared.appearance = .dark
|
||||
|
||||
let app = try ghosttyApplication()
|
||||
app.launch()
|
||||
try assertTitlebarAppearance(.dark, for: app)
|
||||
// create a split
|
||||
app.groups["Terminal pane"].typeKey("d", modifierFlags: .command)
|
||||
// reload config
|
||||
app.typeKey(",", modifierFlags: [.command, .shift])
|
||||
try await Task.sleep(for: .seconds(0.5))
|
||||
// create a new window
|
||||
app.typeKey("n", modifierFlags: [.command])
|
||||
try assertTitlebarAppearance(.dark, for: app)
|
||||
}
|
||||
|
||||
@MainActor
|
||||
func testLightTransparentWindowThemeWithDarkTerminal() async throws {
|
||||
try updateConfig("title=\(windowTitle) \n window-theme=light")
|
||||
let app = try ghosttyApplication()
|
||||
app.launch()
|
||||
try await Task.sleep(for: .seconds(0.5))
|
||||
try assertTitlebarAppearance(.dark, for: app)
|
||||
}
|
||||
|
||||
@MainActor
|
||||
func testLightNativeWindowThemeWithDarkTerminal() async throws {
|
||||
try updateConfig("title=\(windowTitle) \n window-theme = light \n macos-titlebar-style = native")
|
||||
let app = try ghosttyApplication()
|
||||
app.launch()
|
||||
try assertTitlebarAppearance(.light, for: app)
|
||||
}
|
||||
|
||||
@MainActor
|
||||
func testReloadingLightTransparentWindowTheme() async throws {
|
||||
try updateConfig("title=\(windowTitle) \n ")
|
||||
let app = try ghosttyApplication()
|
||||
app.launch()
|
||||
// default dark theme
|
||||
try assertTitlebarAppearance(.dark, for: app)
|
||||
try updateConfig("title=\(windowTitle) \n theme=light:3024 Day,dark:3024 Night \n window-theme = light")
|
||||
// reload config
|
||||
app.typeKey(",", modifierFlags: [.command, .shift])
|
||||
try await Task.sleep(for: .seconds(0.5))
|
||||
try assertTitlebarAppearance(.light, for: app)
|
||||
}
|
||||
|
||||
@MainActor
|
||||
func testSwitchingSystemTheme() async throws {
|
||||
try updateConfig("title=\(windowTitle) \n theme=light:3024 Day,dark:3024 Night")
|
||||
XCUIDevice.shared.appearance = .dark
|
||||
let app = try ghosttyApplication()
|
||||
app.launch()
|
||||
try assertTitlebarAppearance(.dark, for: app)
|
||||
XCUIDevice.shared.appearance = .light
|
||||
try await Task.sleep(for: .seconds(0.5))
|
||||
try assertTitlebarAppearance(.light, for: app)
|
||||
}
|
||||
|
||||
@MainActor
|
||||
func testReloadFromLightWindowThemeToDefaultTheme() async throws {
|
||||
try updateConfig("title=\(windowTitle) \n theme=light:3024 Day,dark:3024 Night")
|
||||
XCUIDevice.shared.appearance = .light
|
||||
let app = try ghosttyApplication()
|
||||
app.launch()
|
||||
try assertTitlebarAppearance(.light, for: app)
|
||||
try updateConfig("title=\(windowTitle) \n ")
|
||||
// reload config
|
||||
app.typeKey(",", modifierFlags: [.command, .shift])
|
||||
try await Task.sleep(for: .seconds(0.5))
|
||||
try assertTitlebarAppearance(.dark, for: app)
|
||||
}
|
||||
|
||||
@MainActor
|
||||
func testReloadFromDefaultThemeToDarkWindowTheme() async throws {
|
||||
try updateConfig("title=\(windowTitle) \n ")
|
||||
XCUIDevice.shared.appearance = .light
|
||||
let app = try ghosttyApplication()
|
||||
app.launch()
|
||||
try assertTitlebarAppearance(.dark, for: app)
|
||||
try updateConfig("title=\(windowTitle) \n theme=light:3024 Day,dark:3024 Night \n window-theme=dark")
|
||||
// reload config
|
||||
app.typeKey(",", modifierFlags: [.command, .shift])
|
||||
try await Task.sleep(for: .seconds(0.5))
|
||||
try assertTitlebarAppearance(.dark, for: app)
|
||||
}
|
||||
|
||||
@MainActor
|
||||
func testReloadingFromDarkThemeToSystemLightTheme() async throws {
|
||||
try updateConfig("title=\(windowTitle) \n theme=light:3024 Day,dark:3024 Night \n window-theme=dark")
|
||||
XCUIDevice.shared.appearance = .light
|
||||
let app = try ghosttyApplication()
|
||||
app.launch()
|
||||
try assertTitlebarAppearance(.dark, for: app)
|
||||
try updateConfig("title=\(windowTitle) \n theme=light:3024 Day,dark:3024 Night")
|
||||
// reload config
|
||||
app.typeKey(",", modifierFlags: [.command, .shift])
|
||||
try await Task.sleep(for: .seconds(0.5))
|
||||
try assertTitlebarAppearance(.light, for: app)
|
||||
}
|
||||
|
||||
@MainActor
|
||||
func testQuickTerminalThemeChange() async throws {
|
||||
try updateConfig("title=\(windowTitle) \n theme=light:3024 Day,dark:3024 Night \n confirm-close-surface=false")
|
||||
XCUIDevice.shared.appearance = .light
|
||||
let app = try ghosttyApplication()
|
||||
app.launch()
|
||||
// close default window
|
||||
app.typeKey("w", modifierFlags: [.command])
|
||||
// open quick terminal
|
||||
app.menuBarItems["View"].firstMatch.click()
|
||||
app.menuItems["Quick Terminal"].firstMatch.click()
|
||||
let title = "Debug builds of Ghostty are very slow and you may experience performance problems. Debug builds are only recommended during development."
|
||||
try assertTitlebarAppearance(.light, for: app, title: title, colorLocation: CGPoint(x: 5, y: 5)) // to avoid dark edge
|
||||
XCUIDevice.shared.appearance = .dark
|
||||
try await Task.sleep(for: .seconds(0.5))
|
||||
try assertTitlebarAppearance(.dark, for: app, title: title, colorLocation: CGPoint(x: 5, y: 5))
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
//
|
||||
// GhosttyTitleUITests.swift
|
||||
// GhosttyUITests
|
||||
//
|
||||
// Created by luca on 13.10.2025.
|
||||
//
|
||||
|
||||
import XCTest
|
||||
|
||||
final class GhosttyTitleUITests: GhosttyCustomConfigCase {
|
||||
override func setUp() async throws {
|
||||
try await super.setUp()
|
||||
try updateConfig(#"title = "GhosttyUITestsLaunchTests""#)
|
||||
}
|
||||
|
||||
@MainActor
|
||||
func testTitle() throws {
|
||||
let app = try ghosttyApplication()
|
||||
app.launch()
|
||||
|
||||
XCTAssertEqual(app.windows.firstMatch.title, "GhosttyUITestsLaunchTests", "Oops, `title=` doesn't work!")
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,143 @@
|
|||
//
|
||||
// GhosttyTitlebarTabsUITests.swift
|
||||
// Ghostty
|
||||
//
|
||||
// Created by luca on 16.10.2025.
|
||||
//
|
||||
|
||||
import XCTest
|
||||
|
||||
final class GhosttyTitlebarTabsUITests: GhosttyCustomConfigCase {
|
||||
override func setUp() async throws {
|
||||
try await super.setUp()
|
||||
|
||||
try updateConfig(
|
||||
"""
|
||||
macos-titlebar-style = tabs
|
||||
title = "GhosttyTitlebarTabsUITests"
|
||||
"""
|
||||
)
|
||||
}
|
||||
|
||||
@MainActor
|
||||
func testCustomTitlebar() throws {
|
||||
let app = try ghosttyApplication()
|
||||
app.launch()
|
||||
// create a split
|
||||
app.groups["Terminal pane"].typeKey("d", modifierFlags: .command)
|
||||
app.typeKey("\n", modifierFlags: [.command, .shift])
|
||||
let resetZoomButton = app.groups.buttons["ResetZoom"]
|
||||
let windowTitle = app.windows.firstMatch.title
|
||||
let titleView = app.staticTexts.element(matching: NSPredicate(format: "value == '\(windowTitle)'"))
|
||||
|
||||
XCTAssertEqual(titleView.frame.midY, resetZoomButton.frame.midY, accuracy: 1, "Window title should be vertically centered with reset zoom button: \(titleView.frame.midY) != \(resetZoomButton.frame.midY)")
|
||||
}
|
||||
|
||||
@MainActor
|
||||
func testTabsGeometryInNormalWindow() throws {
|
||||
let app = try ghosttyApplication()
|
||||
app.launch()
|
||||
app.groups["Terminal pane"].typeKey("t", modifierFlags: .command)
|
||||
XCTAssertEqual(app.tabs.count, 2, "There should be 2 tabs")
|
||||
checkTabsGeometry(app.windows.firstMatch)
|
||||
}
|
||||
|
||||
@MainActor
|
||||
func testTabsGeometryInFullscreen() throws {
|
||||
let app = try ghosttyApplication()
|
||||
app.launch()
|
||||
app.typeKey("f", modifierFlags: [.command, .control])
|
||||
// using app to type ⌘+t might not be able to create tabs
|
||||
app.groups["Terminal pane"].typeKey("t", modifierFlags: .command)
|
||||
XCTAssertEqual(app.tabs.count, 2, "There should be 2 tabs")
|
||||
checkTabsGeometry(app.windows.firstMatch)
|
||||
}
|
||||
|
||||
@MainActor
|
||||
func testTabsGeometryAfterMovingTabs() throws {
|
||||
let app = try ghosttyApplication()
|
||||
app.launch()
|
||||
XCTAssertTrue(app.windows.firstMatch.waitForExistence(timeout: 1), "Main window should exist")
|
||||
// create another 2 tabs
|
||||
app.groups["Terminal pane"].typeKey("t", modifierFlags: .command)
|
||||
app.groups["Terminal pane"].typeKey("t", modifierFlags: .command)
|
||||
|
||||
// move to the left
|
||||
app.menuItems["_zoomLeft:"].firstMatch.click()
|
||||
|
||||
// create another window with 2 tabs
|
||||
app.windows.firstMatch.groups["Terminal pane"].typeKey("n", modifierFlags: .command)
|
||||
XCTAssertEqual(app.windows.count, 2, "There should be 2 windows")
|
||||
|
||||
// move to the right
|
||||
app.menuItems["_zoomRight:"].firstMatch.click()
|
||||
|
||||
// now second window is the first/main one in the list
|
||||
app.windows.firstMatch.groups["Terminal pane"].typeKey("t", modifierFlags: .command)
|
||||
|
||||
app.windows.element(boundBy: 1).tabs.firstMatch.click() // focus first window
|
||||
|
||||
// now the first window is the main one
|
||||
let firstTabInFirstWindow = app.windows.firstMatch.tabs.firstMatch
|
||||
let firstTabInSecondWindow = app.windows.element(boundBy: 1).tabs.firstMatch
|
||||
|
||||
// drag a tab from one window to another
|
||||
firstTabInFirstWindow.press(forDuration: 0.2, thenDragTo: firstTabInSecondWindow)
|
||||
|
||||
// check tabs in the first
|
||||
checkTabsGeometry(app.windows.firstMatch)
|
||||
// focus another window
|
||||
app.windows.element(boundBy: 1).tabs.firstMatch.click()
|
||||
checkTabsGeometry(app.windows.firstMatch)
|
||||
}
|
||||
|
||||
@MainActor
|
||||
func testTabsGeometryAfterMergingAllWindows() throws {
|
||||
let app = try ghosttyApplication()
|
||||
app.launch()
|
||||
XCTAssertTrue(app.windows.firstMatch.waitForExistence(timeout: 1), "Main window should exist")
|
||||
|
||||
// create another 2 windows
|
||||
app.typeKey("n", modifierFlags: .command)
|
||||
app.typeKey("n", modifierFlags: .command)
|
||||
|
||||
// merge into one window, resulting 3 tabs
|
||||
app.menuItems["mergeAllWindows:"].firstMatch.click()
|
||||
|
||||
XCTAssertTrue(app.wait(for: \.tabs.count, toEqual: 3, timeout: 1), "There should be 3 tabs")
|
||||
checkTabsGeometry(app.windows.firstMatch)
|
||||
}
|
||||
|
||||
func checkTabsGeometry(_ window: XCUIElement) {
|
||||
let closeTabButtons = window.buttons.matching(identifier: "_closeButton")
|
||||
|
||||
XCTAssertEqual(closeTabButtons.count, window.tabs.count, "Close tab buttons count should match tabs count")
|
||||
|
||||
var previousTabHeight: CGFloat?
|
||||
for idx in 0 ..< window.tabs.count {
|
||||
let currentTab = window.tabs.element(boundBy: idx)
|
||||
// focus
|
||||
currentTab.click()
|
||||
// switch to the tab
|
||||
window.typeKey("\(idx + 1)", modifierFlags: .command)
|
||||
// add a split
|
||||
window.typeKey("d", modifierFlags: .command)
|
||||
// zoom this split
|
||||
// haven't found a way to locate our reset zoom button yet..
|
||||
window.typeKey("\n", modifierFlags: [.command, .shift])
|
||||
window.typeKey("\n", modifierFlags: [.command, .shift])
|
||||
|
||||
if let previousHeight = previousTabHeight {
|
||||
XCTAssertEqual(currentTab.frame.height, previousHeight, accuracy: 1, "The tab's height should stay the same")
|
||||
}
|
||||
previousTabHeight = currentTab.frame.height
|
||||
|
||||
let titleFrame = currentTab.frame
|
||||
let shortcutLabelFrame = window.staticTexts.element(matching: NSPredicate(format: "value CONTAINS[c] '⌘\(idx + 1)'")).firstMatch.frame
|
||||
let closeButtonFrame = closeTabButtons.element(boundBy: idx).frame
|
||||
|
||||
XCTAssertEqual(titleFrame.midY, shortcutLabelFrame.midY, accuracy: 1, "Tab title should be vertically centered with its shortcut label: \(titleFrame.midY) != \(shortcutLabelFrame.midY)")
|
||||
XCTAssertEqual(titleFrame.midY, closeButtonFrame.midY, accuracy: 1, "Tab title should be vertically centered with its close button: \(titleFrame.midY) != \(closeButtonFrame.midY)")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -94,7 +94,7 @@ class AppDelegate: NSObject,
|
|||
private var derivedConfig: DerivedConfig = DerivedConfig()
|
||||
|
||||
/// The ghostty global state. Only one per process.
|
||||
let ghostty: Ghostty.App = Ghostty.App()
|
||||
let ghostty: Ghostty.App
|
||||
|
||||
/// The global undo manager for app-level state such as window restoration.
|
||||
lazy var undoManager = ExpiringUndoManager()
|
||||
|
|
@ -153,6 +153,11 @@ class AppDelegate: NSObject,
|
|||
@Published private(set) var appIcon: NSImage? = nil
|
||||
|
||||
override init() {
|
||||
#if DEBUG
|
||||
ghostty = Ghostty.App(configPath: ProcessInfo.processInfo.environment["GHOSTTY_CONFIG_PATH"])
|
||||
#else
|
||||
ghostty = Ghostty.App()
|
||||
#endif
|
||||
super.init()
|
||||
|
||||
ghostty.delegate = self
|
||||
|
|
|
|||
|
|
@ -29,6 +29,8 @@ extension Ghostty {
|
|||
/// configuration (i.e. font size) from the previously focused window. This would override this.
|
||||
@Published private(set) var config: Config
|
||||
|
||||
/// Preferred config file than the default ones
|
||||
private var configPath: String?
|
||||
/// The ghostty app instance. We only have one of these for the entire app, although I guess
|
||||
/// in theory you can have multiple... I don't know why you would...
|
||||
@Published var app: ghostty_app_t? = nil {
|
||||
|
|
@ -44,9 +46,10 @@ extension Ghostty {
|
|||
return ghostty_app_needs_confirm_quit(app)
|
||||
}
|
||||
|
||||
init() {
|
||||
init(configPath: String? = nil) {
|
||||
self.configPath = configPath
|
||||
// Initialize the global configuration.
|
||||
self.config = Config()
|
||||
self.config = Config(at: configPath)
|
||||
if self.config.config == nil {
|
||||
readiness = .error
|
||||
return
|
||||
|
|
@ -143,7 +146,7 @@ extension Ghostty {
|
|||
}
|
||||
|
||||
// Hard or full updates have to reload the full configuration
|
||||
let newConfig = Config()
|
||||
let newConfig = Config(at: configPath)
|
||||
guard newConfig.loaded else {
|
||||
Ghostty.logger.warning("failed to reload configuration")
|
||||
return
|
||||
|
|
@ -163,7 +166,7 @@ extension Ghostty {
|
|||
// Hard or full updates have to reload the full configuration.
|
||||
// NOTE: We never set this on self.config because this is a surface-only
|
||||
// config. We free it after the call.
|
||||
let newConfig = Config()
|
||||
let newConfig = Config(at: configPath)
|
||||
guard newConfig.loaded else {
|
||||
Ghostty.logger.warning("failed to reload configuration")
|
||||
return
|
||||
|
|
|
|||
|
|
@ -33,14 +33,16 @@ extension Ghostty {
|
|||
return diags
|
||||
}
|
||||
|
||||
init() {
|
||||
if let cfg = Self.loadConfig() {
|
||||
self.config = cfg
|
||||
}
|
||||
init(config: ghostty_config_t?) {
|
||||
self.config = config
|
||||
}
|
||||
|
||||
init(clone config: ghostty_config_t) {
|
||||
self.config = ghostty_config_clone(config)
|
||||
convenience init(at path: String? = nil, finalize: Bool = true) {
|
||||
self.init(config: Self.loadConfig(at: path, finalize: finalize))
|
||||
}
|
||||
|
||||
convenience init(clone config: ghostty_config_t) {
|
||||
self.init(config: ghostty_config_clone(config))
|
||||
}
|
||||
|
||||
deinit {
|
||||
|
|
@ -48,7 +50,10 @@ extension Ghostty {
|
|||
}
|
||||
|
||||
/// Initializes a new configuration and loads all the values.
|
||||
static private func loadConfig() -> ghostty_config_t? {
|
||||
/// - Parameters:
|
||||
/// - path: An optional preferred config file path. Pass `nil` to load the default configuration files.
|
||||
/// - finalize: Whether to finalize the configuration to populate default values.
|
||||
static private func loadConfig(at path: String?, finalize: Bool) -> ghostty_config_t? {
|
||||
// Initialize the global configuration.
|
||||
guard let cfg = ghostty_config_new() else {
|
||||
logger.critical("ghostty_config_new failed")
|
||||
|
|
@ -59,7 +64,11 @@ extension Ghostty {
|
|||
// We only do this on macOS because other Apple platforms do not have the
|
||||
// same filesystem concept.
|
||||
#if os(macOS)
|
||||
ghostty_config_load_default_files(cfg);
|
||||
if let path {
|
||||
ghostty_config_load_file(cfg, path)
|
||||
} else {
|
||||
ghostty_config_load_default_files(cfg)
|
||||
}
|
||||
|
||||
// We only load CLI args when not running in Xcode because in Xcode we
|
||||
// pass some special parameters to control the debugger.
|
||||
|
|
@ -74,9 +83,10 @@ extension Ghostty {
|
|||
// have to do this synchronously. When we support config updating we can do
|
||||
// this async and update later.
|
||||
|
||||
// Finalize will make our defaults available.
|
||||
ghostty_config_finalize(cfg)
|
||||
|
||||
if finalize {
|
||||
// Finalize will make our defaults available.
|
||||
ghostty_config_finalize(cfg)
|
||||
}
|
||||
// Log any configuration errors. These will be automatically shown in a
|
||||
// pop-up window too.
|
||||
let diagsCount = ghostty_config_diagnostics_count(cfg)
|
||||
|
|
|
|||
|
|
@ -65,6 +65,15 @@ export fn ghostty_config_load_default_files(self: *Config) void {
|
|||
};
|
||||
}
|
||||
|
||||
/// Load the configuration from a specific file path.
|
||||
/// The path must be null-terminated.
|
||||
export fn ghostty_config_load_file(self: *Config, path: [*:0]const u8) void {
|
||||
const path_slice = std.mem.span(path);
|
||||
self.loadFile(state.alloc, path_slice) catch |err| {
|
||||
log.err("error loading config from file path={s} err={}", .{ path_slice, err });
|
||||
};
|
||||
}
|
||||
|
||||
/// Load the configuration from the user-specified configuration
|
||||
/// file locations in the previously loaded configuration. This will
|
||||
/// recursively continue to load up to a built-in limit.
|
||||
|
|
|
|||
Loading…
Reference in New Issue