初始提交:terminalX 可运行态(M0/M1/M1.5 已真机验证)
- M0: libghostty SSH 终端(渲染/输入/连接)+ 白屏修复(OutputGate) + 会话状态机/自动重连 - M1: tsnet 用户态组网 + SSH-over-tsnet(fd 桥),shell 级真机验证;R5(Go+gvisor+C+++Swift 同进程) retire - M1.5: tmux -CC 原生 tab(MVP) - 结构: packages/(TXCore·TXTransport), apps/TerminalX, vendor/(libghostty-spm/libssh2/mbedtls/tsnet-bridge), artifacts/ - 文档: CLAUDE.md + docs/HANDOFF.md(新会话入口) - 环境: 认证代理→依赖 vendor 本地化;Go 在 ~/.local/go;仅模拟器/未签名 - 待续: M2 mosh, tmux 多 pane, M4 安全(host key/SE) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
8
vendor/MSDisplayLink/.gitignore
vendored
Normal file
8
vendor/MSDisplayLink/.gitignore
vendored
Normal file
@@ -0,0 +1,8 @@
|
||||
.DS_Store
|
||||
/.build
|
||||
/Packages
|
||||
xcuserdata/
|
||||
DerivedData/
|
||||
.swiftpm/configuration/registries.json
|
||||
.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata
|
||||
.netrc
|
||||
378
vendor/MSDisplayLink/Example/DLExample.xcodeproj/project.pbxproj
vendored
Normal file
378
vendor/MSDisplayLink/Example/DLExample.xcodeproj/project.pbxproj
vendored
Normal file
@@ -0,0 +1,378 @@
|
||||
// !$*UTF8*$!
|
||||
{
|
||||
archiveVersion = 1;
|
||||
classes = {
|
||||
};
|
||||
objectVersion = 56;
|
||||
objects = {
|
||||
|
||||
/* Begin PBXBuildFile section */
|
||||
506240E32C6CBFFA00605212 /* App.swift in Sources */ = {isa = PBXBuildFile; fileRef = 506240E22C6CBFFA00605212 /* App.swift */; };
|
||||
506240E52C6CBFFA00605212 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 506240E42C6CBFFA00605212 /* ContentView.swift */; };
|
||||
506240E72C6CBFFB00605212 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 506240E62C6CBFFB00605212 /* Assets.xcassets */; };
|
||||
506240F32C6CC02C00605212 /* MSDisplayLink in Frameworks */ = {isa = PBXBuildFile; productRef = 506240F22C6CC02C00605212 /* MSDisplayLink */; };
|
||||
/* End PBXBuildFile section */
|
||||
|
||||
/* Begin PBXFileReference section */
|
||||
506240DF2C6CBFFA00605212 /* DLExample.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = DLExample.app; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
506240E22C6CBFFA00605212 /* App.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = App.swift; sourceTree = "<group>"; };
|
||||
506240E42C6CBFFA00605212 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = "<group>"; };
|
||||
506240E62C6CBFFB00605212 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
|
||||
506240E82C6CBFFB00605212 /* Entitlement.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Entitlement.entitlements; sourceTree = "<group>"; };
|
||||
/* End PBXFileReference section */
|
||||
|
||||
/* Begin PBXFrameworksBuildPhase section */
|
||||
506240DC2C6CBFFA00605212 /* Frameworks */ = {
|
||||
isa = PBXFrameworksBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
506240F32C6CC02C00605212 /* MSDisplayLink in Frameworks */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
/* End PBXFrameworksBuildPhase section */
|
||||
|
||||
/* Begin PBXGroup section */
|
||||
506240D62C6CBFFA00605212 = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
506240E12C6CBFFA00605212 /* DLExample */,
|
||||
506240E02C6CBFFA00605212 /* Products */,
|
||||
506240F12C6CC02C00605212 /* Frameworks */,
|
||||
);
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
506240E02C6CBFFA00605212 /* Products */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
506240DF2C6CBFFA00605212 /* DLExample.app */,
|
||||
);
|
||||
name = Products;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
506240E12C6CBFFA00605212 /* DLExample */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
506240E62C6CBFFB00605212 /* Assets.xcassets */,
|
||||
506240E82C6CBFFB00605212 /* Entitlement.entitlements */,
|
||||
506240E22C6CBFFA00605212 /* App.swift */,
|
||||
506240E42C6CBFFA00605212 /* ContentView.swift */,
|
||||
);
|
||||
path = DLExample;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
506240F12C6CC02C00605212 /* Frameworks */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
);
|
||||
name = Frameworks;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
/* End PBXGroup section */
|
||||
|
||||
/* Begin PBXNativeTarget section */
|
||||
506240DE2C6CBFFA00605212 /* DLExample */ = {
|
||||
isa = PBXNativeTarget;
|
||||
buildConfigurationList = 506240EE2C6CBFFB00605212 /* Build configuration list for PBXNativeTarget "DLExample" */;
|
||||
buildPhases = (
|
||||
506240DB2C6CBFFA00605212 /* Sources */,
|
||||
506240DC2C6CBFFA00605212 /* Frameworks */,
|
||||
506240DD2C6CBFFA00605212 /* Resources */,
|
||||
);
|
||||
buildRules = (
|
||||
);
|
||||
dependencies = (
|
||||
);
|
||||
name = DLExample;
|
||||
packageProductDependencies = (
|
||||
506240F22C6CC02C00605212 /* MSDisplayLink */,
|
||||
);
|
||||
productName = DLExample;
|
||||
productReference = 506240DF2C6CBFFA00605212 /* DLExample.app */;
|
||||
productType = "com.apple.product-type.application";
|
||||
};
|
||||
/* End PBXNativeTarget section */
|
||||
|
||||
/* Begin PBXProject section */
|
||||
506240D72C6CBFFA00605212 /* Project object */ = {
|
||||
isa = PBXProject;
|
||||
attributes = {
|
||||
BuildIndependentTargetsInParallel = 1;
|
||||
LastSwiftUpdateCheck = 1540;
|
||||
LastUpgradeCheck = 1540;
|
||||
TargetAttributes = {
|
||||
506240DE2C6CBFFA00605212 = {
|
||||
CreatedOnToolsVersion = 15.4;
|
||||
};
|
||||
};
|
||||
};
|
||||
buildConfigurationList = 506240DA2C6CBFFA00605212 /* Build configuration list for PBXProject "DLExample" */;
|
||||
compatibilityVersion = "Xcode 14.0";
|
||||
developmentRegion = en;
|
||||
hasScannedForEncodings = 0;
|
||||
knownRegions = (
|
||||
en,
|
||||
Base,
|
||||
);
|
||||
mainGroup = 506240D62C6CBFFA00605212;
|
||||
productRefGroup = 506240E02C6CBFFA00605212 /* Products */;
|
||||
projectDirPath = "";
|
||||
projectRoot = "";
|
||||
targets = (
|
||||
506240DE2C6CBFFA00605212 /* DLExample */,
|
||||
);
|
||||
};
|
||||
/* End PBXProject section */
|
||||
|
||||
/* Begin PBXResourcesBuildPhase section */
|
||||
506240DD2C6CBFFA00605212 /* Resources */ = {
|
||||
isa = PBXResourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
506240E72C6CBFFB00605212 /* Assets.xcassets in Resources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
/* End PBXResourcesBuildPhase section */
|
||||
|
||||
/* Begin PBXSourcesBuildPhase section */
|
||||
506240DB2C6CBFFA00605212 /* Sources */ = {
|
||||
isa = PBXSourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
506240E52C6CBFFA00605212 /* ContentView.swift in Sources */,
|
||||
506240E32C6CBFFA00605212 /* App.swift in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
/* End PBXSourcesBuildPhase section */
|
||||
|
||||
/* Begin XCBuildConfiguration section */
|
||||
506240EC2C6CBFFB00605212 /* Debug */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
ALWAYS_SEARCH_USER_PATHS = NO;
|
||||
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
|
||||
CLANG_ANALYZER_NONNULL = YES;
|
||||
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
|
||||
CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CLANG_ENABLE_OBJC_ARC = YES;
|
||||
CLANG_ENABLE_OBJC_WEAK = YES;
|
||||
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
|
||||
CLANG_WARN_BOOL_CONVERSION = YES;
|
||||
CLANG_WARN_COMMA = YES;
|
||||
CLANG_WARN_CONSTANT_CONVERSION = YES;
|
||||
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
|
||||
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
|
||||
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
|
||||
CLANG_WARN_EMPTY_BODY = YES;
|
||||
CLANG_WARN_ENUM_CONVERSION = YES;
|
||||
CLANG_WARN_INFINITE_RECURSION = YES;
|
||||
CLANG_WARN_INT_CONVERSION = YES;
|
||||
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
|
||||
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
|
||||
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
|
||||
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
|
||||
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
|
||||
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
|
||||
CLANG_WARN_STRICT_PROTOTYPES = YES;
|
||||
CLANG_WARN_SUSPICIOUS_MOVE = YES;
|
||||
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
|
||||
CLANG_WARN_UNREACHABLE_CODE = YES;
|
||||
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
|
||||
COPY_PHASE_STRIP = NO;
|
||||
DEBUG_INFORMATION_FORMAT = dwarf;
|
||||
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
||||
ENABLE_TESTABILITY = YES;
|
||||
ENABLE_USER_SCRIPT_SANDBOXING = YES;
|
||||
GCC_C_LANGUAGE_STANDARD = gnu17;
|
||||
GCC_DYNAMIC_NO_PIC = NO;
|
||||
GCC_NO_COMMON_BLOCKS = YES;
|
||||
GCC_OPTIMIZATION_LEVEL = 0;
|
||||
GCC_PREPROCESSOR_DEFINITIONS = (
|
||||
"DEBUG=1",
|
||||
"$(inherited)",
|
||||
);
|
||||
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
|
||||
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
|
||||
GCC_WARN_UNDECLARED_SELECTOR = YES;
|
||||
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
|
||||
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
|
||||
MTL_FAST_MATH = YES;
|
||||
ONLY_ACTIVE_ARCH = YES;
|
||||
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)";
|
||||
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
|
||||
};
|
||||
name = Debug;
|
||||
};
|
||||
506240ED2C6CBFFB00605212 /* Release */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
ALWAYS_SEARCH_USER_PATHS = NO;
|
||||
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
|
||||
CLANG_ANALYZER_NONNULL = YES;
|
||||
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
|
||||
CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CLANG_ENABLE_OBJC_ARC = YES;
|
||||
CLANG_ENABLE_OBJC_WEAK = YES;
|
||||
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
|
||||
CLANG_WARN_BOOL_CONVERSION = YES;
|
||||
CLANG_WARN_COMMA = YES;
|
||||
CLANG_WARN_CONSTANT_CONVERSION = YES;
|
||||
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
|
||||
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
|
||||
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
|
||||
CLANG_WARN_EMPTY_BODY = YES;
|
||||
CLANG_WARN_ENUM_CONVERSION = YES;
|
||||
CLANG_WARN_INFINITE_RECURSION = YES;
|
||||
CLANG_WARN_INT_CONVERSION = YES;
|
||||
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
|
||||
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
|
||||
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
|
||||
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
|
||||
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
|
||||
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
|
||||
CLANG_WARN_STRICT_PROTOTYPES = YES;
|
||||
CLANG_WARN_SUSPICIOUS_MOVE = YES;
|
||||
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
|
||||
CLANG_WARN_UNREACHABLE_CODE = YES;
|
||||
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
|
||||
COPY_PHASE_STRIP = NO;
|
||||
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
|
||||
ENABLE_NS_ASSERTIONS = NO;
|
||||
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
||||
ENABLE_USER_SCRIPT_SANDBOXING = YES;
|
||||
GCC_C_LANGUAGE_STANDARD = gnu17;
|
||||
GCC_NO_COMMON_BLOCKS = YES;
|
||||
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
|
||||
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
|
||||
GCC_WARN_UNDECLARED_SELECTOR = YES;
|
||||
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
|
||||
MTL_ENABLE_DEBUG_INFO = NO;
|
||||
MTL_FAST_MATH = YES;
|
||||
SWIFT_COMPILATION_MODE = wholemodule;
|
||||
};
|
||||
name = Release;
|
||||
};
|
||||
506240EF2C6CBFFB00605212 /* Debug */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
||||
CODE_SIGN_ENTITLEMENTS = DLExample/Entitlement.entitlements;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
DEVELOPMENT_ASSET_PATHS = "";
|
||||
DEVELOPMENT_TEAM = 964G86XT2P;
|
||||
ENABLE_HARDENED_RUNTIME = YES;
|
||||
ENABLE_PREVIEWS = NO;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
"INFOPLIST_KEY_UIApplicationSceneManifest_Generation[sdk=iphoneos*]" = YES;
|
||||
"INFOPLIST_KEY_UIApplicationSceneManifest_Generation[sdk=iphonesimulator*]" = YES;
|
||||
"INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents[sdk=iphoneos*]" = YES;
|
||||
"INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents[sdk=iphonesimulator*]" = YES;
|
||||
"INFOPLIST_KEY_UILaunchScreen_Generation[sdk=iphoneos*]" = YES;
|
||||
"INFOPLIST_KEY_UILaunchScreen_Generation[sdk=iphonesimulator*]" = YES;
|
||||
"INFOPLIST_KEY_UIStatusBarStyle[sdk=iphoneos*]" = UIStatusBarStyleDefault;
|
||||
"INFOPLIST_KEY_UIStatusBarStyle[sdk=iphonesimulator*]" = UIStatusBarStyleDefault;
|
||||
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
|
||||
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 17.0;
|
||||
LD_RUNPATH_SEARCH_PATHS = "@executable_path/Frameworks";
|
||||
"LD_RUNPATH_SEARCH_PATHS[sdk=macosx*]" = "@executable_path/../Frameworks";
|
||||
MACOSX_DEPLOYMENT_TARGET = 14.0;
|
||||
MARKETING_VERSION = 1.0;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = wiki.qaq.DLExample;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SDKROOT = auto;
|
||||
SUPPORTED_PLATFORMS = "appletvos appletvsimulator iphoneos iphonesimulator macosx xros xrsimulator";
|
||||
SUPPORTS_MACCATALYST = YES;
|
||||
SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = YES;
|
||||
SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = YES;
|
||||
SWIFT_EMIT_LOC_STRINGS = YES;
|
||||
SWIFT_VERSION = 5.0;
|
||||
TARGETED_DEVICE_FAMILY = "1,2,3,7";
|
||||
};
|
||||
name = Debug;
|
||||
};
|
||||
506240F02C6CBFFB00605212 /* Release */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
||||
CODE_SIGN_ENTITLEMENTS = DLExample/Entitlement.entitlements;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
DEVELOPMENT_ASSET_PATHS = "";
|
||||
DEVELOPMENT_TEAM = 964G86XT2P;
|
||||
ENABLE_HARDENED_RUNTIME = YES;
|
||||
ENABLE_PREVIEWS = NO;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
"INFOPLIST_KEY_UIApplicationSceneManifest_Generation[sdk=iphoneos*]" = YES;
|
||||
"INFOPLIST_KEY_UIApplicationSceneManifest_Generation[sdk=iphonesimulator*]" = YES;
|
||||
"INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents[sdk=iphoneos*]" = YES;
|
||||
"INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents[sdk=iphonesimulator*]" = YES;
|
||||
"INFOPLIST_KEY_UILaunchScreen_Generation[sdk=iphoneos*]" = YES;
|
||||
"INFOPLIST_KEY_UILaunchScreen_Generation[sdk=iphonesimulator*]" = YES;
|
||||
"INFOPLIST_KEY_UIStatusBarStyle[sdk=iphoneos*]" = UIStatusBarStyleDefault;
|
||||
"INFOPLIST_KEY_UIStatusBarStyle[sdk=iphonesimulator*]" = UIStatusBarStyleDefault;
|
||||
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
|
||||
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 17.0;
|
||||
LD_RUNPATH_SEARCH_PATHS = "@executable_path/Frameworks";
|
||||
"LD_RUNPATH_SEARCH_PATHS[sdk=macosx*]" = "@executable_path/../Frameworks";
|
||||
MACOSX_DEPLOYMENT_TARGET = 14.0;
|
||||
MARKETING_VERSION = 1.0;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = wiki.qaq.DLExample;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SDKROOT = auto;
|
||||
SUPPORTED_PLATFORMS = "appletvos appletvsimulator iphoneos iphonesimulator macosx xros xrsimulator";
|
||||
SUPPORTS_MACCATALYST = YES;
|
||||
SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = YES;
|
||||
SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = YES;
|
||||
SWIFT_EMIT_LOC_STRINGS = YES;
|
||||
SWIFT_VERSION = 5.0;
|
||||
TARGETED_DEVICE_FAMILY = "1,2,3,7";
|
||||
};
|
||||
name = Release;
|
||||
};
|
||||
/* End XCBuildConfiguration section */
|
||||
|
||||
/* Begin XCConfigurationList section */
|
||||
506240DA2C6CBFFA00605212 /* Build configuration list for PBXProject "DLExample" */ = {
|
||||
isa = XCConfigurationList;
|
||||
buildConfigurations = (
|
||||
506240EC2C6CBFFB00605212 /* Debug */,
|
||||
506240ED2C6CBFFB00605212 /* Release */,
|
||||
);
|
||||
defaultConfigurationIsVisible = 0;
|
||||
defaultConfigurationName = Release;
|
||||
};
|
||||
506240EE2C6CBFFB00605212 /* Build configuration list for PBXNativeTarget "DLExample" */ = {
|
||||
isa = XCConfigurationList;
|
||||
buildConfigurations = (
|
||||
506240EF2C6CBFFB00605212 /* Debug */,
|
||||
506240F02C6CBFFB00605212 /* Release */,
|
||||
);
|
||||
defaultConfigurationIsVisible = 0;
|
||||
defaultConfigurationName = Release;
|
||||
};
|
||||
/* End XCConfigurationList section */
|
||||
|
||||
/* Begin XCSwiftPackageProductDependency section */
|
||||
506240F22C6CC02C00605212 /* MSDisplayLink */ = {
|
||||
isa = XCSwiftPackageProductDependency;
|
||||
productName = MSDisplayLink;
|
||||
};
|
||||
/* End XCSwiftPackageProductDependency section */
|
||||
};
|
||||
rootObject = 506240D72C6CBFFA00605212 /* Project object */;
|
||||
}
|
||||
7
vendor/MSDisplayLink/Example/DLExample.xcodeproj/project.xcworkspace/contents.xcworkspacedata
generated
vendored
Normal file
7
vendor/MSDisplayLink/Example/DLExample.xcodeproj/project.xcworkspace/contents.xcworkspacedata
generated
vendored
Normal file
@@ -0,0 +1,7 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Workspace
|
||||
version = "1.0">
|
||||
<FileRef
|
||||
location = "self:">
|
||||
</FileRef>
|
||||
</Workspace>
|
||||
@@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>IDEDidComputeMac32BitWarning</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</plist>
|
||||
10
vendor/MSDisplayLink/Example/DLExample.xcworkspace/contents.xcworkspacedata
generated
vendored
Normal file
10
vendor/MSDisplayLink/Example/DLExample.xcworkspace/contents.xcworkspacedata
generated
vendored
Normal file
@@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Workspace
|
||||
version = "1.0">
|
||||
<FileRef
|
||||
location = "group:DLExample.xcodeproj">
|
||||
</FileRef>
|
||||
<FileRef
|
||||
location = "group:..">
|
||||
</FileRef>
|
||||
</Workspace>
|
||||
8
vendor/MSDisplayLink/Example/DLExample.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist
vendored
Normal file
8
vendor/MSDisplayLink/Example/DLExample.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist
vendored
Normal file
@@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>IDEDidComputeMac32BitWarning</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</plist>
|
||||
37
vendor/MSDisplayLink/Example/DLExample/App.swift
vendored
Normal file
37
vendor/MSDisplayLink/Example/DLExample/App.swift
vendored
Normal file
@@ -0,0 +1,37 @@
|
||||
//
|
||||
// App.swift
|
||||
// DLExample
|
||||
//
|
||||
// Created by 秋星桥 on 2024/8/14.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
@main
|
||||
struct App: SwiftUI.App {
|
||||
var body: some Scene {
|
||||
WindowGroup {
|
||||
ContentView()
|
||||
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
||||
#if os(macOS)
|
||||
.background(VisualEffectView().ignoresSafeArea())
|
||||
#endif
|
||||
}
|
||||
#if os(macOS)
|
||||
.windowStyle(.hiddenTitleBar)
|
||||
.windowResizability(.contentMinSize)
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
#if os(macOS)
|
||||
struct VisualEffectView: NSViewRepresentable {
|
||||
func makeNSView(context _: Context) -> NSVisualEffectView {
|
||||
let effectView = NSVisualEffectView()
|
||||
effectView.state = .active
|
||||
return effectView
|
||||
}
|
||||
|
||||
func updateNSView(_: NSVisualEffectView, context _: Context) {}
|
||||
}
|
||||
#endif
|
||||
11
vendor/MSDisplayLink/Example/DLExample/Assets.xcassets/AccentColor.colorset/Contents.json
vendored
Normal file
11
vendor/MSDisplayLink/Example/DLExample/Assets.xcassets/AccentColor.colorset/Contents.json
vendored
Normal file
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"colors" : [
|
||||
{
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
63
vendor/MSDisplayLink/Example/DLExample/Assets.xcassets/AppIcon.appiconset/Contents.json
vendored
Normal file
63
vendor/MSDisplayLink/Example/DLExample/Assets.xcassets/AppIcon.appiconset/Contents.json
vendored
Normal file
@@ -0,0 +1,63 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"platform" : "ios",
|
||||
"size" : "1024x1024"
|
||||
},
|
||||
{
|
||||
"idiom" : "mac",
|
||||
"scale" : "1x",
|
||||
"size" : "16x16"
|
||||
},
|
||||
{
|
||||
"idiom" : "mac",
|
||||
"scale" : "2x",
|
||||
"size" : "16x16"
|
||||
},
|
||||
{
|
||||
"idiom" : "mac",
|
||||
"scale" : "1x",
|
||||
"size" : "32x32"
|
||||
},
|
||||
{
|
||||
"idiom" : "mac",
|
||||
"scale" : "2x",
|
||||
"size" : "32x32"
|
||||
},
|
||||
{
|
||||
"idiom" : "mac",
|
||||
"scale" : "1x",
|
||||
"size" : "128x128"
|
||||
},
|
||||
{
|
||||
"idiom" : "mac",
|
||||
"scale" : "2x",
|
||||
"size" : "128x128"
|
||||
},
|
||||
{
|
||||
"idiom" : "mac",
|
||||
"scale" : "1x",
|
||||
"size" : "256x256"
|
||||
},
|
||||
{
|
||||
"idiom" : "mac",
|
||||
"scale" : "2x",
|
||||
"size" : "256x256"
|
||||
},
|
||||
{
|
||||
"idiom" : "mac",
|
||||
"scale" : "1x",
|
||||
"size" : "512x512"
|
||||
},
|
||||
{
|
||||
"idiom" : "mac",
|
||||
"scale" : "2x",
|
||||
"size" : "512x512"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
6
vendor/MSDisplayLink/Example/DLExample/Assets.xcassets/Contents.json
vendored
Normal file
6
vendor/MSDisplayLink/Example/DLExample/Assets.xcassets/Contents.json
vendored
Normal file
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
25
vendor/MSDisplayLink/Example/DLExample/ContentView.swift
vendored
Normal file
25
vendor/MSDisplayLink/Example/DLExample/ContentView.swift
vendored
Normal file
@@ -0,0 +1,25 @@
|
||||
//
|
||||
// ContentView.swift
|
||||
// DLExample
|
||||
//
|
||||
// Created by 秋星桥 on 2024/8/14.
|
||||
//
|
||||
|
||||
import MSDisplayLink
|
||||
import SwiftUI
|
||||
|
||||
struct ContentView: View {
|
||||
@State var frame: Int = 0
|
||||
var body: some View {
|
||||
VStack {
|
||||
Text("MSDisplayLink Trigger: \(frame)")
|
||||
.monospacedDigit()
|
||||
.contentTransition(.numericText())
|
||||
}
|
||||
.animation(.interactiveSpring, value: frame)
|
||||
.modifier(DisplayLinkModifier(scheduleToMainThread: true) {
|
||||
frame += 1
|
||||
})
|
||||
.padding()
|
||||
}
|
||||
}
|
||||
10
vendor/MSDisplayLink/Example/DLExample/Entitlement.entitlements
vendored
Normal file
10
vendor/MSDisplayLink/Example/DLExample/Entitlement.entitlements
vendored
Normal file
@@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>com.apple.security.app-sandbox</key>
|
||||
<true/>
|
||||
<key>com.apple.security.files.user-selected.read-only</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</plist>
|
||||
21
vendor/MSDisplayLink/LICENSE
vendored
Normal file
21
vendor/MSDisplayLink/LICENSE
vendored
Normal file
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2024 Lakr Aream
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
20
vendor/MSDisplayLink/Package.swift
vendored
Normal file
20
vendor/MSDisplayLink/Package.swift
vendored
Normal file
@@ -0,0 +1,20 @@
|
||||
// swift-tools-version: 6.0
|
||||
|
||||
import PackageDescription
|
||||
|
||||
let package = Package(
|
||||
name: "MSDisplayLink",
|
||||
platforms: [
|
||||
.iOS(.v13),
|
||||
.macOS(.v11),
|
||||
.macCatalyst(.v13),
|
||||
.tvOS(.v13),
|
||||
.visionOS(.v1),
|
||||
],
|
||||
products: [
|
||||
.library(name: "MSDisplayLink", targets: ["MSDisplayLink"]),
|
||||
],
|
||||
targets: [
|
||||
.target(name: "MSDisplayLink"),
|
||||
]
|
||||
)
|
||||
73
vendor/MSDisplayLink/README.md
vendored
Normal file
73
vendor/MSDisplayLink/README.md
vendored
Normal file
@@ -0,0 +1,73 @@
|
||||
# MSDisplayLink
|
||||
|
||||
The missing DisplayLink driver for Apple platforms. Use CADisplayLink on UIKit and CVDisplayLink on AppKit.
|
||||
|
||||
Battle tested in [ColorfulX](https://github.com/Lakr233/ColorfulX)
|
||||
|
||||
Called MS but it has nothing to do with Microsoft. :P
|
||||
|
||||
## Preview
|
||||
|
||||

|
||||
|
||||
## Installation
|
||||
|
||||
### Swift Package Manager
|
||||
|
||||
```swift
|
||||
.package(url: "https://github.com/Lakr233/MSDisplayLink.git", from: "2.0.8")
|
||||
```
|
||||
|
||||
### CocoaPods
|
||||
|
||||
**Never thought about it.**
|
||||
|
||||
## Usage
|
||||
|
||||
### UIKit/AppKit
|
||||
|
||||
Follow the steps to get called. The `DisplayLinkDelegate` is friendly to confirm for a UIView or NSView.
|
||||
|
||||
- Hold reference to `DisplayLink` object.
|
||||
- Confirm to `DisplayLinkDelegate` protocol to get called within `synchronization()`.
|
||||
- Call `displayLink.delegatingObject` with a `DisplayLinkDelegate` object.
|
||||
|
||||
Tech Tips:
|
||||
|
||||
- Call to `synchronization()` is performed at a background thread.
|
||||
- Scheduler is a serial queue unique to each `DisplayLinkDriver` holds by `DisplayLink`.
|
||||
- Queue inherits the quality of service from the where you created the `DisplayLink`.
|
||||
- Does not make scene to use concurrent queue.
|
||||
- Does not share the queue across different `DisplayLink` objects.
|
||||
- Queue names are same.
|
||||
|
||||
### SwiftUI
|
||||
|
||||
```
|
||||
import MSDisplayLink
|
||||
import SwiftUI
|
||||
|
||||
struct ContentView: View {
|
||||
@State var frame: Int = 0
|
||||
var body: some View {
|
||||
VStack {
|
||||
Text("MSDisplayLink Trigger: \(frame)")
|
||||
.monospacedDigit()
|
||||
.contentTransition(.numericText())
|
||||
}
|
||||
.animation(.interactiveSpring, value: frame)
|
||||
.modifier(DisplayLinkModifier(scheduleToMainThread: true) {
|
||||
frame += 1
|
||||
})
|
||||
.padding()
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## License
|
||||
|
||||
[MIT License](LICENSE)
|
||||
|
||||
---
|
||||
|
||||
2024.08.14
|
||||
BIN
vendor/MSDisplayLink/Resources/SCR-20240814-qqri.jpeg
vendored
Normal file
BIN
vendor/MSDisplayLink/Resources/SCR-20240814-qqri.jpeg
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 110 KiB |
36
vendor/MSDisplayLink/Scripts/test.build.sh
vendored
Executable file
36
vendor/MSDisplayLink/Scripts/test.build.sh
vendored
Executable file
@@ -0,0 +1,36 @@
|
||||
#!/bin/bash
|
||||
|
||||
cd "$(dirname "$0")"
|
||||
cd ..
|
||||
|
||||
WORKSPACE="./Example/DLExample.xcworkspace"
|
||||
SCEHEME="DLExample"
|
||||
|
||||
function test_build() {
|
||||
DESTINATION=$1
|
||||
echo "[*] test build for $DESTINATION"
|
||||
xcodebuild \
|
||||
-workspace "$WORKSPACE" \
|
||||
-scheme "$SCEHEME" \
|
||||
-destination "$DESTINATION" \
|
||||
CODE_SIGN_IDENTITY="" \
|
||||
CODE_SIGNING_ALLOWED=NO \
|
||||
| xcbeautify
|
||||
EXIT_CODE=${PIPESTATUS[0]}
|
||||
echo "[*] finished with exit code $EXIT_CODE"
|
||||
if [ $EXIT_CODE -ne 0 ]; then
|
||||
echo "[!] failed to build for $DESTINATION"
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
test_build "generic/platform=macOS"
|
||||
test_build "generic/platform=macOS,variant=Mac Catalyst"
|
||||
test_build "generic/platform=iOS"
|
||||
test_build "generic/platform=iOS Simulator"
|
||||
test_build "generic/platform=tvOS"
|
||||
test_build "generic/platform=tvOS Simulator"
|
||||
test_build "generic/platform=xrOS"
|
||||
test_build "generic/platform=xrOS Simulator"
|
||||
|
||||
echo "[*] all builds succeeded"
|
||||
56
vendor/MSDisplayLink/Sources/MSDisplayLink/DisplayLink+SwiftUI.swift
vendored
Normal file
56
vendor/MSDisplayLink/Sources/MSDisplayLink/DisplayLink+SwiftUI.swift
vendored
Normal file
@@ -0,0 +1,56 @@
|
||||
//
|
||||
// DisplayLink+SwiftUI.swift
|
||||
// MSDisplayLink
|
||||
//
|
||||
// Created by 秋星桥 on 2024/8/14.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
@MainActor
|
||||
public struct DisplayLinkModifier: ViewModifier {
|
||||
let link: DisplayLink
|
||||
let context: DisplayLinkModifierContext
|
||||
|
||||
public init(scheduleToMainThread: Bool = true, _ callback: @escaping @Sendable (DisplayLinkCallbackContext) -> Void) {
|
||||
link = .init()
|
||||
context = .init(scheduleToMainThread: scheduleToMainThread, callback: callback)
|
||||
link.delegatingObject(context)
|
||||
}
|
||||
|
||||
public init(scheduleToMainThread: Bool = true, _ callback: @escaping @Sendable () -> Void) {
|
||||
self.init(scheduleToMainThread: scheduleToMainThread) { _ in callback() }
|
||||
}
|
||||
|
||||
public func body(content: Content) -> some View {
|
||||
content
|
||||
.onAppear { link.delegatingObject(context) }
|
||||
.onDisappear { link.delegatingObject(nil) }
|
||||
}
|
||||
}
|
||||
|
||||
class DisplayLinkModifierContext: ObservableObject, DisplayLinkDelegate, @unchecked Sendable {
|
||||
let scheduleToMainThread: Bool
|
||||
var callback: @Sendable (DisplayLinkCallbackContext) -> Void
|
||||
|
||||
init(scheduleToMainThread: Bool, callback: @escaping @Sendable (DisplayLinkCallbackContext) -> Void) {
|
||||
self.scheduleToMainThread = scheduleToMainThread
|
||||
self.callback = callback
|
||||
}
|
||||
|
||||
func synchronization(context: DisplayLinkCallbackContext) {
|
||||
if scheduleToMainThread {
|
||||
if Thread.isMainThread {
|
||||
callback(context)
|
||||
} else {
|
||||
DispatchQueue.main.async { self.callback(context) }
|
||||
}
|
||||
} else {
|
||||
if Thread.isMainThread {
|
||||
DispatchQueue.global().async { self.callback(context) }
|
||||
} else {
|
||||
callback(context)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
40
vendor/MSDisplayLink/Sources/MSDisplayLink/DisplayLink.swift
vendored
Normal file
40
vendor/MSDisplayLink/Sources/MSDisplayLink/DisplayLink.swift
vendored
Normal file
@@ -0,0 +1,40 @@
|
||||
//
|
||||
// DisplayLink.swift
|
||||
// MSDisplayLink
|
||||
//
|
||||
// Created by 秋星桥 on 2024/8/13.
|
||||
//
|
||||
|
||||
@preconcurrency import Combine
|
||||
import Foundation
|
||||
|
||||
public protocol DisplayLinkDelegate: AnyObject {
|
||||
func synchronization(context: DisplayLinkCallbackContext)
|
||||
}
|
||||
|
||||
public class DisplayLink: @unchecked Sendable {
|
||||
private weak var delegatingObject: DisplayLinkDelegate?
|
||||
|
||||
private var driver: DisplayLinkDriver?
|
||||
private var driverSubscription: Set<AnyCancellable> = .init()
|
||||
|
||||
public init() {
|
||||
let driver = DisplayLinkDriver()
|
||||
driver.synchronizationPublisher
|
||||
.sink { [weak self] output in self?.delegatingObject?.synchronization(context: output) }
|
||||
.store(in: &driverSubscription)
|
||||
self.driver = driver
|
||||
}
|
||||
|
||||
deinit { teardown() }
|
||||
|
||||
func teardown() {
|
||||
driverSubscription.forEach { $0.cancel() }
|
||||
driverSubscription.removeAll()
|
||||
driver = nil
|
||||
}
|
||||
|
||||
public func delegatingObject(_ object: DisplayLinkDelegate?) {
|
||||
delegatingObject = object
|
||||
}
|
||||
}
|
||||
23
vendor/MSDisplayLink/Sources/MSDisplayLink/DisplayLinkCallbackContext.swift
vendored
Normal file
23
vendor/MSDisplayLink/Sources/MSDisplayLink/DisplayLinkCallbackContext.swift
vendored
Normal file
@@ -0,0 +1,23 @@
|
||||
//
|
||||
// DisplayLinkCallbackContext.swift
|
||||
// MSDisplayLink
|
||||
//
|
||||
// Created by 秋星桥 on 2025/1/6.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
public struct DisplayLinkCallbackContext: Sendable {
|
||||
/// The time interval between screen refresh updates.
|
||||
public let duration: TimeInterval
|
||||
/// The time interval that represents when the last frame displayed.
|
||||
public let timestamp: TimeInterval
|
||||
/// The time interval that represents when the next frame displays.
|
||||
public let targetTimestamp: TimeInterval
|
||||
|
||||
public init(duration: TimeInterval, timestamp: TimeInterval, targetTimestamp: TimeInterval) {
|
||||
self.duration = duration
|
||||
self.timestamp = timestamp
|
||||
self.targetTimestamp = targetTimestamp
|
||||
}
|
||||
}
|
||||
64
vendor/MSDisplayLink/Sources/MSDisplayLink/DisplayLinkDriver+CA.swift
vendored
Normal file
64
vendor/MSDisplayLink/Sources/MSDisplayLink/DisplayLinkDriver+CA.swift
vendored
Normal file
@@ -0,0 +1,64 @@
|
||||
import Foundation
|
||||
|
||||
#if canImport(UIKit)
|
||||
import UIKit
|
||||
|
||||
typealias DisplayLinkDriverHelper = CADisplayLinkDriverHelper
|
||||
|
||||
class CADisplayLinkDriverHelper: DisplayLinkDriverHelperBase {
|
||||
nonisolated(unsafe) static let shared = CADisplayLinkDriverHelper()
|
||||
|
||||
private var displayLink: CADisplayLink?
|
||||
|
||||
override private init() {
|
||||
super.init()
|
||||
NotificationCenter.default.addObserver(
|
||||
self,
|
||||
selector: #selector(applicationDidEnterBackground(_:)),
|
||||
name: UIApplication.didEnterBackgroundNotification,
|
||||
object: nil
|
||||
)
|
||||
|
||||
NotificationCenter.default.addObserver(
|
||||
self,
|
||||
selector: #selector(applicationWillEnterForeground(_:)),
|
||||
name: UIApplication.willEnterForegroundNotification,
|
||||
object: nil
|
||||
)
|
||||
}
|
||||
|
||||
deinit {
|
||||
NotificationCenter.default.removeObserver(self)
|
||||
}
|
||||
|
||||
override func startDisplayLink() {
|
||||
assert(Thread.isMainThread)
|
||||
guard displayLink == nil else { return }
|
||||
displayLink = CADisplayLink(target: self, selector: #selector(displayLinkCallback(_:)))
|
||||
displayLink?.add(to: .main, forMode: .common)
|
||||
}
|
||||
|
||||
override func stopDisplayLink() {
|
||||
assert(Thread.isMainThread)
|
||||
displayLink?.invalidate()
|
||||
displayLink = nil
|
||||
}
|
||||
|
||||
@objc private func displayLinkCallback(_ displayLink: CADisplayLink) {
|
||||
let context = DisplayLinkCallbackContext(
|
||||
duration: displayLink.duration,
|
||||
timestamp: displayLink.timestamp,
|
||||
targetTimestamp: displayLink.targetTimestamp
|
||||
)
|
||||
autoreleasepool { dispatchUpdate(context: context) }
|
||||
}
|
||||
|
||||
@objc private func applicationDidEnterBackground(_: Notification) {
|
||||
stopDisplayLink()
|
||||
}
|
||||
|
||||
@objc private func applicationWillEnterForeground(_: Notification) {
|
||||
startDisplayLink()
|
||||
}
|
||||
}
|
||||
#endif
|
||||
43
vendor/MSDisplayLink/Sources/MSDisplayLink/DisplayLinkDriver+CV.swift
vendored
Normal file
43
vendor/MSDisplayLink/Sources/MSDisplayLink/DisplayLinkDriver+CV.swift
vendored
Normal file
@@ -0,0 +1,43 @@
|
||||
import Foundation
|
||||
|
||||
#if !canImport(UIKit) && canImport(AppKit)
|
||||
import AppKit
|
||||
|
||||
typealias DisplayLinkDriverHelper = CVDisplayLinkDriverHelper
|
||||
|
||||
class CVDisplayLinkDriverHelper: DisplayLinkDriverHelperBase {
|
||||
nonisolated(unsafe) static let shared = CVDisplayLinkDriverHelper()
|
||||
|
||||
private var displayLink: CVDisplayLink?
|
||||
|
||||
override func startDisplayLink() {
|
||||
assert(Thread.isMainThread)
|
||||
guard displayLink == nil else { return }
|
||||
CVDisplayLinkCreateWithActiveCGDisplays(&displayLink)
|
||||
guard let displayLink else { return }
|
||||
|
||||
CVDisplayLinkSetOutputCallback(displayLink, { _, inNow, inOutputTime, _, _, _ in
|
||||
let clockFrequency = CVGetHostClockFrequency()
|
||||
let context = DisplayLinkCallbackContext(
|
||||
duration: TimeInterval(inNow.pointee.videoRefreshPeriod) / TimeInterval(inNow.pointee.videoTimeScale),
|
||||
timestamp: TimeInterval(inNow.pointee.hostTime) / clockFrequency,
|
||||
targetTimestamp: TimeInterval(inOutputTime.pointee.hostTime) / clockFrequency
|
||||
)
|
||||
assert(!Thread.isMainThread)
|
||||
DispatchQueue.main.async {
|
||||
autoreleasepool {
|
||||
CVDisplayLinkDriverHelper.shared.dispatchUpdate(context: context)
|
||||
}
|
||||
}
|
||||
return kCVReturnSuccess
|
||||
}, nil)
|
||||
CVDisplayLinkStart(displayLink)
|
||||
}
|
||||
|
||||
override func stopDisplayLink() {
|
||||
assert(Thread.isMainThread)
|
||||
if let displayLink { CVDisplayLinkStop(displayLink) }
|
||||
displayLink = nil
|
||||
}
|
||||
}
|
||||
#endif
|
||||
59
vendor/MSDisplayLink/Sources/MSDisplayLink/DisplayLinkDriver+Helper.swift
vendored
Normal file
59
vendor/MSDisplayLink/Sources/MSDisplayLink/DisplayLinkDriver+Helper.swift
vendored
Normal file
@@ -0,0 +1,59 @@
|
||||
//
|
||||
// DisplayLinkDriver+Helper.swift
|
||||
//
|
||||
//
|
||||
// Created by 秋星桥 on 2024/8/29.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
class DisplayLinkDriverHelperBase: Identifiable {
|
||||
final let id: UUID = .init()
|
||||
|
||||
private(set) var referenceHolder: [WeakBox] = []
|
||||
|
||||
struct WeakBox { weak var object: DisplayLinkDriver? }
|
||||
|
||||
final func delegate(_ object: DisplayLinkDriver) {
|
||||
assert(Thread.isMainThread)
|
||||
var shouldStartDisplayLink = false
|
||||
defer { if shouldStartDisplayLink { startDisplayLink() } }
|
||||
|
||||
referenceHolder = referenceHolder
|
||||
.filter { $0.object != nil }
|
||||
.filter { $0.object?.id != object.id }
|
||||
+ [.init(object: object)]
|
||||
|
||||
shouldStartDisplayLink = !referenceHolder.isEmpty
|
||||
}
|
||||
|
||||
final func remove(_ object: DisplayLinkDriver) {
|
||||
assert(Thread.isMainThread)
|
||||
|
||||
referenceHolder = referenceHolder.filter { $0.object?.id != object.id }
|
||||
}
|
||||
|
||||
final func reclaimComputeResourceIfPossible() {
|
||||
assert(Thread.isMainThread)
|
||||
var shouldStop = false
|
||||
defer { if shouldStop { self.stopDisplayLink() } }
|
||||
referenceHolder = referenceHolder.filter { $0.object != nil }
|
||||
shouldStop = referenceHolder.isEmpty
|
||||
}
|
||||
|
||||
final func dispatchUpdate(context: DisplayLinkCallbackContext) {
|
||||
defer { self.reclaimComputeResourceIfPossible() }
|
||||
|
||||
for box in referenceHolder {
|
||||
box.object?.synchronize(context: context)
|
||||
}
|
||||
}
|
||||
|
||||
func startDisplayLink() {
|
||||
fatalError("Subclasses need to implement the `startDisplayLink()` method.")
|
||||
}
|
||||
|
||||
func stopDisplayLink() {
|
||||
fatalError("Subclasses need to implement the `stopDisplayLink()` method.")
|
||||
}
|
||||
}
|
||||
32
vendor/MSDisplayLink/Sources/MSDisplayLink/DisplayLinkDriver.swift
vendored
Normal file
32
vendor/MSDisplayLink/Sources/MSDisplayLink/DisplayLinkDriver.swift
vendored
Normal file
@@ -0,0 +1,32 @@
|
||||
//
|
||||
// DisplayLinkDriver.swift
|
||||
// MSDisplayLink
|
||||
//
|
||||
// Created by 秋星桥 on 2024/8/13.
|
||||
//
|
||||
|
||||
@preconcurrency import Combine
|
||||
import Foundation
|
||||
|
||||
class DisplayLinkDriver: Identifiable {
|
||||
let id: UUID = .init()
|
||||
|
||||
typealias SynchornizationPublisher = PassthroughSubject<
|
||||
DisplayLinkCallbackContext,
|
||||
Never
|
||||
>
|
||||
let synchronizationPublisher: SynchornizationPublisher
|
||||
|
||||
init() {
|
||||
synchronizationPublisher = .init()
|
||||
DisplayLinkDriverHelper.shared.delegate(self)
|
||||
}
|
||||
|
||||
deinit {
|
||||
DisplayLinkDriverHelper.shared.remove(self)
|
||||
}
|
||||
|
||||
func synchronize(context: DisplayLinkCallbackContext) {
|
||||
synchronizationPublisher.send(context)
|
||||
}
|
||||
}
|
||||
87
vendor/build/build-cssh.sh
vendored
Executable file
87
vendor/build/build-cssh.sh
vendored
Executable file
@@ -0,0 +1,87 @@
|
||||
#!/bin/bash
|
||||
# 交叉编译 libssh2 + mbedTLS → CSSHCore.xcframework(iOS device / simulator / macOS)。
|
||||
# mbedTLS 作为 libssh2 的 crypto backend(M0 选型;后续可换 OpenSSL,TXTransport 接口不变)。
|
||||
set -euo pipefail
|
||||
|
||||
VENDOR="$(cd "$(dirname "$0")/.." && pwd)"
|
||||
MBEDTLS="$VENDOR/mbedtls"
|
||||
LIBSSH2="$VENDOR/libssh2"
|
||||
OUT="$VENDOR/build/cssh"
|
||||
ARTIFACTS="$VENDOR/../artifacts"
|
||||
IOS_DEPLOY=17.0
|
||||
MAC_DEPLOY=14.0
|
||||
|
||||
build_slice() {
|
||||
local name="$1" sysname="$2" sysroot="$3" archs="$4" deploy="$5"
|
||||
local prefix="$OUT/$name/prefix"
|
||||
echo "==================== slice: $name ($archs @ $sysroot) ===================="
|
||||
rm -rf "$OUT/$name"; mkdir -p "$prefix"
|
||||
|
||||
local common=(
|
||||
-G Ninja
|
||||
-DCMAKE_BUILD_TYPE=Release
|
||||
-DCMAKE_SYSTEM_NAME="$sysname"
|
||||
-DCMAKE_OSX_SYSROOT="$sysroot"
|
||||
-DCMAKE_OSX_ARCHITECTURES="$archs"
|
||||
-DCMAKE_OSX_DEPLOYMENT_TARGET="$deploy"
|
||||
-DCMAKE_INSTALL_PREFIX="$prefix"
|
||||
)
|
||||
|
||||
# --- mbedTLS ---
|
||||
cmake -S "$MBEDTLS" -B "$OUT/$name/mbedtls" "${common[@]}" \
|
||||
-DENABLE_TESTING=OFF -DENABLE_PROGRAMS=OFF \
|
||||
-DUSE_STATIC_MBEDTLS_LIBRARY=ON -DUSE_SHARED_MBEDTLS_LIBRARY=OFF
|
||||
cmake --build "$OUT/$name/mbedtls" --target install
|
||||
|
||||
# --- libssh2 (mbedTLS backend) ---
|
||||
cmake -S "$LIBSSH2" -B "$OUT/$name/libssh2" "${common[@]}" \
|
||||
-DCRYPTO_BACKEND=mbedTLS \
|
||||
-DCMAKE_PREFIX_PATH="$prefix" \
|
||||
-DMBEDTLS_INCLUDE_DIR="$prefix/include" \
|
||||
-DMBEDCRYPTO_LIBRARY="$prefix/lib/libmbedcrypto.a" \
|
||||
-DBUILD_SHARED_LIBS=OFF -DBUILD_EXAMPLES=OFF -DBUILD_TESTING=OFF \
|
||||
-DENABLE_ZLIB_COMPRESSION=OFF
|
||||
cmake --build "$OUT/$name/libssh2" --target install
|
||||
|
||||
# --- 合并静态库:libssh2 + mbedtls* → 单一 libCSSHCore.a ---
|
||||
libtool -static -o "$OUT/$name/libCSSHCore.a" \
|
||||
"$prefix/lib/libssh2.a" \
|
||||
"$prefix/lib/libmbedtls.a" \
|
||||
"$prefix/lib/libmbedx509.a" \
|
||||
"$prefix/lib/libmbedcrypto.a"
|
||||
echo "[+] $name → $(lipo -archs "$OUT/$name/libCSSHCore.a")"
|
||||
}
|
||||
|
||||
pack_xcframework() {
|
||||
echo "==================== packing CSSHCore.xcframework (library-only) ===================="
|
||||
# 只打包静态库切片;头文件与 modulemap 由 TXTransport 包内的 CSSH(C) target 提供,
|
||||
# 避免 SwiftPM binaryTarget 自带 modulemap 复制到 include/ 时的冲突。
|
||||
rm -rf "$ARTIFACTS/CSSHCore.xcframework"; mkdir -p "$ARTIFACTS"
|
||||
local args=()
|
||||
for s in "$@"; do
|
||||
args+=(-library "$OUT/$s/libCSSHCore.a")
|
||||
done
|
||||
xcodebuild -create-xcframework "${args[@]}" -output "$ARTIFACTS/CSSHCore.xcframework"
|
||||
echo "[+] CSSHCore.xcframework:"; ls "$ARTIFACTS/CSSHCore.xcframework"
|
||||
|
||||
# 同步 libssh2 公共头到 CSSH C target(供 import CSSH)。
|
||||
local csshinc="$VENDOR/../packages/TXTransport/Sources/CSSH/include"
|
||||
mkdir -p "$csshinc"
|
||||
cp "$OUT/iossim/prefix/include/libssh2.h" \
|
||||
"$OUT/iossim/prefix/include/libssh2_publickey.h" \
|
||||
"$OUT/iossim/prefix/include/libssh2_sftp.h" "$csshinc/"
|
||||
echo "[+] synced libssh2 headers → $csshinc"
|
||||
}
|
||||
|
||||
case "${1:-all}" in
|
||||
iossim) build_slice iossim iOS iphonesimulator "arm64;x86_64" "$IOS_DEPLOY" ;;
|
||||
device) build_slice ios-arm64 iOS iphoneos "arm64" "$IOS_DEPLOY" ;;
|
||||
macos) build_slice macos Darwin macosx "arm64" "$MAC_DEPLOY" ;;
|
||||
all)
|
||||
build_slice iossim iOS iphonesimulator "arm64;x86_64" "$IOS_DEPLOY"
|
||||
build_slice ios-arm64 iOS iphoneos "arm64" "$IOS_DEPLOY"
|
||||
build_slice macos Darwin macosx "arm64" "$MAC_DEPLOY"
|
||||
pack_xcframework iossim ios-arm64 macos
|
||||
;;
|
||||
pack) shift; pack_xcframework "$@" ;;
|
||||
esac
|
||||
1
vendor/libghostty-spm/.gitattributes
vendored
Normal file
1
vendor/libghostty-spm/.gitattributes
vendored
Normal file
@@ -0,0 +1 @@
|
||||
*.patch whitespace=-trailing-space
|
||||
306
vendor/libghostty-spm/.github/workflows/build.yml
vendored
Normal file
306
vendor/libghostty-spm/.github/workflows/build.yml
vendored
Normal file
@@ -0,0 +1,306 @@
|
||||
name: Build GhosttyKit
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
package_version:
|
||||
description: "GhosttyKit package version to publish, for example 1.3.1."
|
||||
required: true
|
||||
type: string
|
||||
schedule:
|
||||
- cron: "23 6 * * 1"
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
concurrency:
|
||||
group: build-${{ github.ref_name || 'scheduled' }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
prepare:
|
||||
runs-on: macos-15
|
||||
outputs:
|
||||
build_needed: ${{ steps.release.outputs.build_needed }}
|
||||
ghostty_ref: ${{ steps.release.outputs.ghostty_ref }}
|
||||
resolved_sha: ${{ steps.release.outputs.resolved_sha }}
|
||||
package_version: ${{ steps.release.outputs.package_version }}
|
||||
storage_tag: ${{ steps.release.outputs.storage_tag }}
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v5
|
||||
with:
|
||||
fetch-depth: 0
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Resolve Release
|
||||
id: release
|
||||
run: |
|
||||
EVENT="${{ github.event_name }}"
|
||||
INPUT_PACKAGE_VERSION="${{ github.event.inputs.package_version }}"
|
||||
GHOSTTY_REF=$(tr -d '[:space:]' < Ghostty.ref)
|
||||
|
||||
if ! echo "$GHOSTTY_REF" | grep -Eq '^[0-9a-f]{40}$'; then
|
||||
echo "[!] Ghostty.ref must contain one full lowercase commit sha"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
git fetch --tags origin
|
||||
LATEST_PACKAGE_VERSION=$(
|
||||
git tag --list |
|
||||
python3 -c 'import re, sys; versions = [tuple(int(part) for part in tag.split(".")) for tag in (line.strip() for line in sys.stdin) if re.fullmatch(r"\d+\.\d+\.\d+", tag)]; print(".".join(map(str, max(versions))), end="") if versions else None'
|
||||
)
|
||||
|
||||
case "$EVENT" in
|
||||
workflow_dispatch)
|
||||
PACKAGE_VERSION="$INPUT_PACKAGE_VERSION"
|
||||
;;
|
||||
schedule)
|
||||
PACKAGE_VERSION=$(
|
||||
python3 -c 'import sys; version = sys.argv[1]; major, minor, patch = map(int, version.split(".")) if version else (1, 0, -1); print(f"{major}.{minor}.{patch + 1}", end="")' "$LATEST_PACKAGE_VERSION"
|
||||
)
|
||||
;;
|
||||
*)
|
||||
echo "[!] unsupported event: $EVENT"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
if ! echo "$PACKAGE_VERSION" | grep -Eq '^[0-9]+\.[0-9]+\.[0-9]+$'; then
|
||||
echo "[!] package_version must be semantic, for example 1.3.1: $PACKAGE_VERSION"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ -n "$LATEST_PACKAGE_VERSION" ] && ! python3 -c 'import sys; latest = tuple(map(int, sys.argv[1].split("."))); requested = tuple(map(int, sys.argv[2].split("."))); raise SystemExit(0 if requested > latest else 1)' "$LATEST_PACKAGE_VERSION" "$PACKAGE_VERSION"; then
|
||||
echo "[!] package_version must be newer than $LATEST_PACKAGE_VERSION: $PACKAGE_VERSION"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
STORAGE_TAG="storage.$PACKAGE_VERSION"
|
||||
if git rev-parse "refs/tags/$PACKAGE_VERSION" >/dev/null 2>&1; then
|
||||
echo "[!] package tag already exists: $PACKAGE_VERSION"
|
||||
exit 1
|
||||
fi
|
||||
if git rev-parse "refs/tags/$STORAGE_TAG" >/dev/null 2>&1; then
|
||||
echo "[!] storage tag already exists: $STORAGE_TAG"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ "$EVENT" = "schedule" ] && [ -n "$LATEST_PACKAGE_VERSION" ] && [ "$(git rev-list -n 1 "$LATEST_PACKAGE_VERSION")" = "$(git rev-parse HEAD)" ]; then
|
||||
echo "[*] main matches package tag $LATEST_PACKAGE_VERSION, skipping scheduled release"
|
||||
echo "build_needed=false" >> "$GITHUB_OUTPUT"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
RESOLVED_SHA=$(gh api "repos/ghostty-org/ghostty/commits/$GHOSTTY_REF" --jq '.sha')
|
||||
if [ "$RESOLVED_SHA" != "$GHOSTTY_REF" ]; then
|
||||
echo "[!] pinned Ghostty commit did not resolve exactly: $GHOSTTY_REF"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "[*] package_version=$PACKAGE_VERSION ghostty_ref=$GHOSTTY_REF"
|
||||
echo "build_needed=true" >> "$GITHUB_OUTPUT"
|
||||
echo "ghostty_ref=$GHOSTTY_REF" >> "$GITHUB_OUTPUT"
|
||||
echo "resolved_sha=$RESOLVED_SHA" >> "$GITHUB_OUTPUT"
|
||||
echo "package_version=$PACKAGE_VERSION" >> "$GITHUB_OUTPUT"
|
||||
echo "storage_tag=$STORAGE_TAG" >> "$GITHUB_OUTPUT"
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
build-target:
|
||||
needs: prepare
|
||||
if: needs.prepare.outputs.build_needed == 'true'
|
||||
runs-on: macos-15
|
||||
timeout-minutes: 60
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- target: aarch64-macos
|
||||
variant: macosx
|
||||
- target: arm64e-macos
|
||||
variant: macosx
|
||||
cpu: apple_a12
|
||||
- target: x86_64-macos
|
||||
variant: macosx
|
||||
- target: aarch64-ios
|
||||
variant: iphoneos
|
||||
- target: arm64e-ios
|
||||
variant: iphoneos
|
||||
cpu: apple_a12
|
||||
- target: aarch64-ios-simulator
|
||||
variant: iphonesimulator
|
||||
cpu: apple_a17
|
||||
- target: x86_64-ios-simulator
|
||||
variant: iphonesimulator
|
||||
- target: aarch64-ios-macabi
|
||||
variant: maccatalyst
|
||||
cpu: apple_a17
|
||||
- target: arm64e-ios-macabi
|
||||
variant: maccatalyst
|
||||
cpu: apple_a17
|
||||
- target: x86_64-ios-macabi
|
||||
variant: maccatalyst
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v5
|
||||
|
||||
- name: Setup Zig
|
||||
uses: mlugg/setup-zig@v2
|
||||
with:
|
||||
version: 0.15.2
|
||||
|
||||
- name: Clone Ghostty
|
||||
run: |
|
||||
git clone https://github.com/ghostty-org/ghostty ./References/ghostty-upstream
|
||||
git -C ./References/ghostty-upstream checkout "${{ needs.prepare.outputs.ghostty_ref }}"
|
||||
|
||||
- name: Build Target
|
||||
run: |
|
||||
export ZIG_CPU="${{ matrix.cpu }}"
|
||||
./Script/build-ghostty.sh \
|
||||
"$PWD/References/ghostty-upstream" \
|
||||
"${{ matrix.target }}" \
|
||||
"$PWD/build/out"
|
||||
|
||||
- name: Pack Artifact
|
||||
run: tar -czf "target--${{ matrix.variant }}--${{ matrix.target }}.tar.gz" -C ./build/out .
|
||||
|
||||
- name: Upload Artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: target--${{ matrix.variant }}--${{ matrix.target }}
|
||||
path: target--${{ matrix.variant }}--${{ matrix.target }}.tar.gz
|
||||
compression-level: 0
|
||||
retention-days: 1
|
||||
|
||||
merge-test-release:
|
||||
needs: [prepare, build-target]
|
||||
if: needs.prepare.outputs.build_needed == 'true'
|
||||
runs-on: macos-26
|
||||
timeout-minutes: 60
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v5
|
||||
with:
|
||||
fetch-depth: 0
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Set up Xcode
|
||||
run: sudo xcode-select -s /Applications/Xcode.app
|
||||
|
||||
- name: Download All Artifacts
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
pattern: target-*
|
||||
path: ./build/tarballs
|
||||
merge-multiple: true
|
||||
|
||||
- name: Assemble Variants
|
||||
run: |
|
||||
mkdir -p ./build/targets ./build/dest
|
||||
|
||||
for tarball in ./build/tarballs/target--*.tar.gz; do
|
||||
BASENAME=$(basename "$tarball" .tar.gz)
|
||||
VARIANT=$(echo "$BASENAME" | awk -F'--' '{print $2}')
|
||||
TARGET=$(echo "$BASENAME" | awk -F'--' '{print $3}')
|
||||
echo "[*] unpacking $BASENAME → variant=$VARIANT target=$TARGET"
|
||||
mkdir -p "./build/targets/$VARIANT/$TARGET"
|
||||
tar -xzf "$tarball" -C "./build/targets/$VARIANT/$TARGET"
|
||||
done
|
||||
|
||||
for variant_dir in ./build/targets/*/; do
|
||||
VARIANT=$(basename "$variant_dir")
|
||||
mkdir -p "./build/dest/$VARIANT/lib" "./build/dest/$VARIANT/include"
|
||||
|
||||
LIBS=()
|
||||
FIRST_HEADERS=
|
||||
for target_dir in "$variant_dir"*/; do
|
||||
LIBS+=("$target_dir/lib/libghostty.a")
|
||||
if [ -z "$FIRST_HEADERS" ]; then
|
||||
FIRST_HEADERS="$target_dir/include"
|
||||
fi
|
||||
done
|
||||
|
||||
cp -R "$FIRST_HEADERS/." "./build/dest/$VARIANT/include/"
|
||||
|
||||
if [ "${#LIBS[@]}" -eq 1 ]; then
|
||||
cp "${LIBS[0]}" "./build/dest/$VARIANT/lib/libghostty.a"
|
||||
echo "[*] $VARIANT: single arch"
|
||||
else
|
||||
lipo -create "${LIBS[@]}" -output "./build/dest/$VARIANT/lib/libghostty.a"
|
||||
echo "[*] $VARIANT: lipo ${#LIBS[@]} archs"
|
||||
fi
|
||||
done
|
||||
|
||||
- name: Merge XCFramework
|
||||
run: |
|
||||
./Script/merge-xcframework.sh \
|
||||
"$PWD/build/dest" \
|
||||
"$PWD/BinaryTarget/GhosttyKit.xcframework" \
|
||||
"$PWD/build/GhosttyKit.xcframework.zip"
|
||||
|
||||
- name: Test XCFramework Consumer
|
||||
run: ./Script/test-xcframework-consumer.sh "$PWD/BinaryTarget/GhosttyKit.xcframework"
|
||||
|
||||
- name: Use Local Binary Target
|
||||
run: cp Package.local.swift Package.swift
|
||||
|
||||
- name: Build Package Matrix
|
||||
run: ./Script/test.sh
|
||||
|
||||
- name: Run Swift Tests
|
||||
run: swift test
|
||||
|
||||
- name: Prepare Release
|
||||
run: |
|
||||
STORAGE_TAG="${{ needs.prepare.outputs.storage_tag }}"
|
||||
DOWNLOAD_URL="https://github.com/${{ github.repository }}/releases/download/$STORAGE_TAG/GhosttyKit.xcframework.zip"
|
||||
echo "[*] predicted download url: $DOWNLOAD_URL"
|
||||
|
||||
mv ./build/GhosttyKit.xcframework.zip /tmp/GhosttyKit.xcframework.zip
|
||||
git clean -fdx -f
|
||||
git reset --hard
|
||||
mv /tmp/GhosttyKit.xcframework.zip ./GhosttyKit.xcframework.zip
|
||||
./Script/build-manifest.sh GhosttyKit.xcframework.zip "$DOWNLOAD_URL"
|
||||
|
||||
- name: Commit & Push
|
||||
run: |
|
||||
git config user.name "github-actions[bot]"
|
||||
git config user.email "github-actions[bot]@users.noreply.github.com"
|
||||
git add -A
|
||||
git commit -m "chore: autopublish $(date -u +%Y-%m-%dT%H:%M:%SZ)" || echo "No changes to commit"
|
||||
git push
|
||||
|
||||
- name: Create Storage Release
|
||||
uses: softprops/action-gh-release@v2
|
||||
with:
|
||||
tag_name: ${{ needs.prepare.outputs.storage_tag }}
|
||||
target_commitish: ${{ github.ref_name }}
|
||||
body: |
|
||||
GhosttyKit XCFramework
|
||||
|
||||
Package version: ${{ needs.prepare.outputs.package_version }}
|
||||
Ghostty commit: ${{ needs.prepare.outputs.resolved_sha }}
|
||||
draft: false
|
||||
prerelease: false
|
||||
files: |
|
||||
GhosttyKit.xcframework.zip
|
||||
|
||||
- name: Create Release
|
||||
uses: softprops/action-gh-release@v2
|
||||
with:
|
||||
tag_name: ${{ needs.prepare.outputs.package_version }}
|
||||
target_commitish: ${{ github.ref_name }}
|
||||
body: |
|
||||
GhosttyKit ${{ needs.prepare.outputs.package_version }}
|
||||
|
||||
Package version: ${{ needs.prepare.outputs.package_version }}
|
||||
Ghostty commit: ${{ needs.prepare.outputs.resolved_sha }}
|
||||
draft: false
|
||||
prerelease: false
|
||||
|
||||
- name: Verify Release Consistency
|
||||
run: ./Script/verify-release.sh "${{ needs.prepare.outputs.package_version }}" "${{ needs.prepare.outputs.storage_tag }}"
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
43
vendor/libghostty-spm/.github/workflows/pages.yml
vendored
Normal file
43
vendor/libghostty-spm/.github/workflows/pages.yml
vendored
Normal file
@@ -0,0 +1,43 @@
|
||||
name: Publish Docs
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
paths:
|
||||
- docs/**
|
||||
- .github/workflows/pages.yml
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
pages: write
|
||||
id-token: write
|
||||
|
||||
concurrency:
|
||||
group: pages
|
||||
cancel-in-progress: false
|
||||
|
||||
jobs:
|
||||
deploy:
|
||||
name: Deploy GitHub Pages
|
||||
runs-on: ubuntu-latest
|
||||
environment:
|
||||
name: github-pages
|
||||
url: ${{ steps.deployment.outputs.page_url }}
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v5
|
||||
|
||||
- name: Configure Pages
|
||||
uses: actions/configure-pages@v5
|
||||
with:
|
||||
enablement: true
|
||||
|
||||
- name: Upload Pages Artifact
|
||||
uses: actions/upload-pages-artifact@v3
|
||||
with:
|
||||
path: docs
|
||||
|
||||
- name: Deploy Pages
|
||||
id: deployment
|
||||
uses: actions/deploy-pages@v4
|
||||
38
vendor/libghostty-spm/.github/workflows/pr.yml
vendored
Normal file
38
vendor/libghostty-spm/.github/workflows/pr.yml
vendored
Normal file
@@ -0,0 +1,38 @@
|
||||
name: PR Build & Test
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
branches: [main]
|
||||
types: [opened, synchronize, reopened]
|
||||
|
||||
# Zero-scope GITHUB_TOKEN: PR runs untrusted contributor code, so the
|
||||
# token gets no permissions at all. Public repo, so checkout works
|
||||
# without auth. No secrets referenced anywhere in this workflow.
|
||||
permissions: {}
|
||||
|
||||
concurrency:
|
||||
group: pr-${{ github.event.pull_request.number }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
build-test:
|
||||
name: Build & Test
|
||||
runs-on: macos-26
|
||||
timeout-minutes: 30
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v5
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Set up Xcode
|
||||
run: sudo xcode-select -s /Applications/Xcode.app
|
||||
|
||||
- name: Swift Version
|
||||
run: swift --version
|
||||
|
||||
- name: Build Package Matrix
|
||||
run: ./Script/test.sh
|
||||
|
||||
- name: Swift Test
|
||||
run: swift test
|
||||
186
vendor/libghostty-spm/.github/workflows/ui-tests.yml
vendored
Normal file
186
vendor/libghostty-spm/.github/workflows/ui-tests.yml
vendored
Normal file
@@ -0,0 +1,186 @@
|
||||
name: UI Tests
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
branches: [main]
|
||||
types: [opened, synchronize, reopened]
|
||||
workflow_dispatch:
|
||||
|
||||
permissions: {}
|
||||
|
||||
concurrency:
|
||||
group: ui-tests-${{ github.event.pull_request.number || github.ref_name }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
ios-iphone:
|
||||
name: iPhone Simulator UI Tests
|
||||
runs-on: macos-26
|
||||
timeout-minutes: 30
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v5
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Set up Xcode
|
||||
run: xcodebuild -version
|
||||
|
||||
- name: Run iPhone UI Tests
|
||||
run: |
|
||||
set -euo pipefail
|
||||
rm -rf "$RUNNER_TEMP/libghostty-ui-ios.xcresult"
|
||||
RUNTIME=$(xcrun simctl list runtimes available | awk '/^iOS .*com\.apple\.CoreSimulator\.SimRuntime\.iOS/ { runtime=$NF } END { print runtime }')
|
||||
[ -n "$RUNTIME" ] || { echo "no available iOS simulator runtime"; exit 1; }
|
||||
DEVICE=$(xcrun simctl list devicetypes | sed -n '/iPhone Air/s/.*(\(com\.apple\.CoreSimulator\.SimDeviceType\.[^)]*\)).*/\1/p' | head -1)
|
||||
if [ -z "$DEVICE" ]; then
|
||||
DEVICE=$(xcrun simctl list devicetypes | sed -n '/iPhone 16/s/.*(\(com\.apple\.CoreSimulator\.SimDeviceType\.[^)]*\)).*/\1/p' | head -1)
|
||||
fi
|
||||
if [ -z "$DEVICE" ]; then
|
||||
DEVICE=$(xcrun simctl list devicetypes | sed -n '/iPhone/s/.*(\(com\.apple\.CoreSimulator\.SimDeviceType\.[^)]*\)).*/\1/p' | head -1)
|
||||
fi
|
||||
[ -n "$DEVICE" ] || { echo "no available iPhone simulator device type"; exit 1; }
|
||||
UDID=$(xcrun simctl create "libghostty-ui-iPhone-${GITHUB_RUN_ID}" "$DEVICE" "$RUNTIME")
|
||||
trap 'xcrun simctl shutdown "$UDID" >/dev/null 2>&1 || true; xcrun simctl delete "$UDID" >/dev/null 2>&1 || true' EXIT
|
||||
# Hosted runner cleanup: avoid stale simulator windows stealing UI automation focus.
|
||||
xcrun simctl shutdown all >/dev/null 2>&1 || true
|
||||
xcrun simctl boot "$UDID"
|
||||
xcrun simctl bootstatus "$UDID" -b
|
||||
xcodebuild test \
|
||||
-project Example/MobileGhosttyApp.xcodeproj \
|
||||
-scheme MobileGhosttyApp \
|
||||
-destination "id=$UDID" \
|
||||
-resultBundlePath "$RUNNER_TEMP/libghostty-ui-ios.xcresult" \
|
||||
CODE_SIGNING_ALLOWED=NO \
|
||||
CODE_SIGNING_REQUIRED=NO \
|
||||
CODE_SIGN_IDENTITY=""
|
||||
|
||||
- name: Upload iPhone Result Bundle
|
||||
if: always()
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: libghostty-ui-ios-xcresult
|
||||
path: ${{ runner.temp }}/libghostty-ui-ios.xcresult
|
||||
retention-days: 7
|
||||
|
||||
ios-ipad:
|
||||
name: iPad Simulator UI Tests
|
||||
runs-on: macos-26
|
||||
timeout-minutes: 30
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v5
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Set up Xcode
|
||||
run: xcodebuild -version
|
||||
|
||||
- name: Run iPad UI Tests
|
||||
run: |
|
||||
set -euo pipefail
|
||||
rm -rf "$RUNNER_TEMP/libghostty-ui-ipad.xcresult"
|
||||
RUNTIME=$(xcrun simctl list runtimes available | awk '/^iOS .*com\.apple\.CoreSimulator\.SimRuntime\.iOS/ { runtime=$NF } END { print runtime }')
|
||||
[ -n "$RUNTIME" ] || { echo "no available iOS simulator runtime"; exit 1; }
|
||||
DEVICE=$(xcrun simctl list devicetypes | sed -n '/iPad Air 13-inch (M3)/s/.*(\(com\.apple\.CoreSimulator\.SimDeviceType\.[^)]*\)).*/\1/p' | head -1)
|
||||
if [ -z "$DEVICE" ]; then
|
||||
DEVICE=$(xcrun simctl list devicetypes | sed -n '/iPad Air 13-inch/s/.*(\(com\.apple\.CoreSimulator\.SimDeviceType\.[^)]*\)).*/\1/p' | head -1)
|
||||
fi
|
||||
if [ -z "$DEVICE" ]; then
|
||||
DEVICE=$(xcrun simctl list devicetypes | sed -n '/iPad Pro 13-inch/s/.*(\(com\.apple\.CoreSimulator\.SimDeviceType\.[^)]*\)).*/\1/p' | head -1)
|
||||
fi
|
||||
if [ -z "$DEVICE" ]; then
|
||||
DEVICE=$(xcrun simctl list devicetypes | sed -n '/iPad/s/.*(\(com\.apple\.CoreSimulator\.SimDeviceType\.[^)]*\)).*/\1/p' | head -1)
|
||||
fi
|
||||
[ -n "$DEVICE" ] || { echo "no available iPad simulator device type"; exit 1; }
|
||||
UDID=$(xcrun simctl create "libghostty-ui-iPad-${GITHUB_RUN_ID}" "$DEVICE" "$RUNTIME")
|
||||
trap 'xcrun simctl shutdown "$UDID" >/dev/null 2>&1 || true; xcrun simctl delete "$UDID" >/dev/null 2>&1 || true' EXIT
|
||||
# Hosted runner cleanup: avoid stale simulator windows stealing UI automation focus.
|
||||
xcrun simctl shutdown all >/dev/null 2>&1 || true
|
||||
xcrun simctl boot "$UDID"
|
||||
xcrun simctl bootstatus "$UDID" -b
|
||||
xcodebuild test \
|
||||
-project Example/MobileGhosttyApp.xcodeproj \
|
||||
-scheme MobileGhosttyApp \
|
||||
-destination "id=$UDID" \
|
||||
-resultBundlePath "$RUNNER_TEMP/libghostty-ui-ipad.xcresult" \
|
||||
CODE_SIGNING_ALLOWED=NO \
|
||||
CODE_SIGNING_REQUIRED=NO \
|
||||
CODE_SIGN_IDENTITY=""
|
||||
|
||||
- name: Upload iPad Result Bundle
|
||||
if: always()
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: libghostty-ui-ipad-xcresult
|
||||
path: ${{ runner.temp }}/libghostty-ui-ipad.xcresult
|
||||
retention-days: 7
|
||||
|
||||
mac-catalyst:
|
||||
name: Mac Catalyst UI Tests
|
||||
runs-on: macos-26
|
||||
timeout-minutes: 30
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v5
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Set up Xcode
|
||||
run: xcodebuild -version
|
||||
|
||||
- name: Run Mac Catalyst UI Tests
|
||||
run: |
|
||||
set -euo pipefail
|
||||
rm -rf "$RUNNER_TEMP/libghostty-ui-maccatalyst.xcresult"
|
||||
xcodebuild test \
|
||||
-project Example/MobileGhosttyApp.xcodeproj \
|
||||
-scheme MobileGhosttyApp \
|
||||
-destination 'platform=macOS,variant=Mac Catalyst' \
|
||||
-resultBundlePath "$RUNNER_TEMP/libghostty-ui-maccatalyst.xcresult" \
|
||||
MACOSX_DEPLOYMENT_TARGET=15.0 \
|
||||
CODE_SIGNING_ALLOWED=NO \
|
||||
CODE_SIGNING_REQUIRED=NO \
|
||||
CODE_SIGN_IDENTITY=""
|
||||
|
||||
- name: Upload Mac Catalyst Result Bundle
|
||||
if: always()
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: libghostty-ui-maccatalyst-xcresult
|
||||
path: ${{ runner.temp }}/libghostty-ui-maccatalyst.xcresult
|
||||
retention-days: 7
|
||||
|
||||
macos-appkit:
|
||||
name: macOS AppKit UI Tests
|
||||
runs-on: macos-26
|
||||
timeout-minutes: 30
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v5
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Set up Xcode
|
||||
run: xcodebuild -version
|
||||
|
||||
- name: Run macOS AppKit UI Tests
|
||||
run: |
|
||||
set -euo pipefail
|
||||
rm -rf "$RUNNER_TEMP/libghostty-ui-macos.xcresult"
|
||||
xcodebuild test \
|
||||
-project Example/GhosttyTerminalApp.xcodeproj \
|
||||
-scheme GhosttyTerminalApp \
|
||||
-destination 'platform=macOS' \
|
||||
-resultBundlePath "$RUNNER_TEMP/libghostty-ui-macos.xcresult" \
|
||||
CODE_SIGNING_ALLOWED=NO \
|
||||
CODE_SIGNING_REQUIRED=NO \
|
||||
CODE_SIGN_IDENTITY=""
|
||||
|
||||
- name: Upload macOS AppKit Result Bundle
|
||||
if: always()
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: libghostty-ui-macos-xcresult
|
||||
path: ${{ runner.temp }}/libghostty-ui-macos.xcresult
|
||||
retention-days: 7
|
||||
55
vendor/libghostty-spm/.gitignore
vendored
Normal file
55
vendor/libghostty-spm/.gitignore
vendored
Normal file
@@ -0,0 +1,55 @@
|
||||
# Build
|
||||
.build/
|
||||
build/
|
||||
*.a
|
||||
*.xcframework
|
||||
*.xcframework.zip
|
||||
binpack*
|
||||
BinaryTarget/
|
||||
|
||||
# Xcode
|
||||
*.xcodeproj
|
||||
xcuserdata/
|
||||
*.xcuserstate
|
||||
.swiftpm/
|
||||
|
||||
# Swift Package Manager
|
||||
Package.resolved
|
||||
|
||||
# Obj-C / Swift
|
||||
*.hmap
|
||||
|
||||
# App Packaging
|
||||
*.ipa
|
||||
*.dSYM
|
||||
*.dSYM.zip
|
||||
|
||||
# Playgrounds
|
||||
timeline.xctimeline
|
||||
playground.xcworkspace
|
||||
|
||||
# macOS
|
||||
.DS_Store
|
||||
__MACOSX/
|
||||
._*
|
||||
.AppleDouble
|
||||
.LSOverride
|
||||
Icon[]
|
||||
.DocumentRevisions-V100
|
||||
.fseventsd
|
||||
.Spotlight-V100
|
||||
.TemporaryItems
|
||||
.Trashes
|
||||
.VolumeIcon.icns
|
||||
.com.apple.timemachine.donotpresent
|
||||
.AppleDB
|
||||
.AppleDesktop
|
||||
Network Trash Folder
|
||||
Temporary Items
|
||||
.apdisk
|
||||
|
||||
# Project
|
||||
/temp
|
||||
/Packages
|
||||
/References
|
||||
/references
|
||||
0
vendor/libghostty-spm/.root
vendored
Normal file
0
vendor/libghostty-spm/.root
vendored
Normal file
159
vendor/libghostty-spm/AGENTS.md
vendored
Normal file
159
vendor/libghostty-spm/AGENTS.md
vendored
Normal file
@@ -0,0 +1,159 @@
|
||||
# CLAUDE.md
|
||||
|
||||
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
||||
|
||||
## Project Overview
|
||||
|
||||
SPM package wrapping Ghostty terminal emulator C library for Apple platforms (macOS 13+, iOS 15+, Mac Catalyst 15+). Four library products:
|
||||
|
||||
- **GhosttyKit** — minimal re-export of the libghostty C API (`@_exported import libghostty`)
|
||||
- **GhosttyTerminal** — Swift wrapper: native views, SwiftUI integration, input handling, display link, host-managed I/O
|
||||
- **GhosttyTheme** — 485 terminal color themes from iTerm2-Color-Schemes (MIT License, depends on GhosttyTerminal)
|
||||
- **ShellCraftKit** — sandboxed shell emulation framework (depends on GhosttyTerminal)
|
||||
|
||||
Binary target: pre-built `libghostty` XCFramework. Dependency: MSDisplayLink ^2.1.0.
|
||||
|
||||
## Build & Test Commands
|
||||
|
||||
```bash
|
||||
# Build the SPM package
|
||||
swift build
|
||||
|
||||
# Run tests
|
||||
swift test
|
||||
|
||||
# Multi-destination build verification (macOS, iOS, iOS Simulator, Mac Catalyst)
|
||||
./Script/test.sh
|
||||
|
||||
# Build full XCFramework from Ghostty source (requires zig)
|
||||
./build.sh
|
||||
./build.sh --platforms macos,ios --source /path/to/ghostty --skip-tests
|
||||
|
||||
# Generate Package.swift for release
|
||||
./Script/build-manifest.sh
|
||||
|
||||
# Regenerate GhosttyTheme Swift files from iTerm2-Color-Schemes
|
||||
./Script/generate-themes.sh
|
||||
```
|
||||
|
||||
## Architecture
|
||||
|
||||
```
|
||||
GhosttyKit (C API re-export)
|
||||
└─ libghostty.a (Zig → static lib) + ghostty.h
|
||||
|
||||
GhosttyTerminal (Swift wrapper, ~40 files)
|
||||
├─ Configuration/ Config structs, themes, color schemes, ghostty.conf rendering
|
||||
├─ Controller/ TerminalController — app lifecycle, config, surface creation
|
||||
├─ InMemory/ Sandbox-safe I/O backend (no PTY), C callback bridge
|
||||
├─ Metrics/ Grid size, viewport dimensions, input/scroll modifiers
|
||||
├─ Platform/AppKit/ macOS NSView: input, IME, key events
|
||||
├─ Platform/UIKit/ iOS UIView: UITextInput, keyboard, touch/gesture, IME, input accessory bar
|
||||
├─ State/ ObservableObject TerminalViewState (SwiftUI state container)
|
||||
├─ Surface/ Metal rendering bridge, display link, surface lifecycle
|
||||
└─ View/ SwiftUI TerminalSurfaceView + platform representables
|
||||
|
||||
GhosttyTheme (485 terminal color themes)
|
||||
├─ GhosttyThemeDefinition — theme data model (name, colors, palette)
|
||||
├─ GhosttyThemeCatalog — static catalog, search, lookup by name
|
||||
├─ +TerminalConfiguration — bridge to TerminalConfiguration/TerminalTheme, isDark helper
|
||||
└─ Themes/ — auto-generated Swift files (A-Z) from iTerm2-Color-Schemes
|
||||
|
||||
ShellCraftKit (~5 files)
|
||||
├─ Definition/ ShellDefinition, SandboxShell, ShellCommand protocol
|
||||
└─ Session/ ShellSession + Bridge + Engine
|
||||
```
|
||||
|
||||
Key types: `TerminalViewState` (ObservableObject, SwiftUI entry point), `TerminalSurfaceView` (SwiftUI view), `TerminalView` (platform typealias: UITerminalView / AppTerminalView), `TerminalController`, `InMemoryTerminalSession`, `GhosttyThemeDefinition`, `GhosttyThemeCatalog`.
|
||||
|
||||
### Platform Branching
|
||||
|
||||
Use `#if canImport(UIKit)` FIRST, then `#else #if canImport(AppKit)` — Catalyst imports both UIKit and AppKit.
|
||||
|
||||
### Host-Managed I/O
|
||||
|
||||
All example apps run in App Sandbox. Use `GHOSTTY_SURFACE_IO_BACKEND_HOST_MANAGED` for non-PTY I/O. Never disable sandbox or spawn subprocesses.
|
||||
|
||||
### iOS Input Architecture (UITextInput)
|
||||
|
||||
`UITerminalView` conforms to `UITextInput` (which includes `UIKeyInput`) to receive both software keyboard and hardware keyboard input on iOS/Catalyst. The input chain:
|
||||
|
||||
1. **Hardware keys** → `pressesBegan`/`pressesEnded` in `+Keyboard.swift` → builds `ghostty_input_key_s` → `surface.sendKeyEvent()`. Sets `hardwareKeyHandled = true` to suppress the duplicate `insertText`/`deleteBackward` that UIKit would otherwise deliver.
|
||||
2. **Software keyboard** → UIKit calls `insertText(_:)` / `deleteBackward()` via UIKeyInput. Guarded by `hardwareKeyHandled` flag to avoid double-processing hardware key presses.
|
||||
3. **Input accessory bar** (iOS only, excludes Catalyst) → `TerminalInputAccessoryView` provides a toolbar above the software keyboard with Esc, Tab, arrow keys, modifier keys (Ctrl/Alt/Cmd), symbol keys, and Paste. Modifier keys support **sticky states**: tap to arm (consumed after next key), double-tap to lock (persists until toggled off). Sticky modifier state is tracked by `TerminalStickyModifierState`. Actions are dispatched via `UITerminalView+InputAccessory.swift`. Button colors are configurable via `TerminalInputAccessoryStyle` (regular/active background and foreground), exposed as `UITerminalView.inputAccessoryStyle`.
|
||||
4. **IME / marked text** → `setMarkedText` / `unmarkText` delegate to `TerminalTextInputHandler`, which calls `surface.preedit()` for inline composition preview. Committed text goes through `insertText`. Sticky modifiers are respected during IME composition.
|
||||
5. **Text positioning** → `TerminalTextPosition` / `TerminalTextRange` (UITextPosition/UITextRange subclasses) provide minimal cursor geometry. `caretRect`/`firstRect` use `surface.imePoint()` for IME candidate window placement.
|
||||
|
||||
Files in `Platform/UIKit/`:
|
||||
|
||||
- `UITerminalView.swift` — main view, `canBecomeFirstResponder`, coordinator setup
|
||||
- `UITerminalView+UITextInput.swift` — full UITextInput conformance (UIKeyInput, marked text, positions, geometry)
|
||||
- `UITerminalView+Keyboard.swift` — hardware key handling via UIPress, modifier translation
|
||||
- `UITerminalView+InputAccessory.swift` — input accessory bar integration, key actions, sticky modifier dispatch
|
||||
- `UITerminalView+Interaction.swift` — touch scrolling, momentum scroll via CADisplayLink, Catalyst pointer/mouse
|
||||
- `UITerminalView+Lifecycle.swift` — display scale, sublayer frames, focus, color scheme
|
||||
- `TerminalInputAccessoryView.swift` — input accessory bar UIView (blur background, scrollable button layout)
|
||||
- `TerminalInputAccessoryStyle.swift` — configurable button colors for the accessory bar (regular/active background and foreground)
|
||||
- `TerminalInputBarKey.swift` — enum defining accessory bar key types (esc, tab, arrows, symbols, paste)
|
||||
- `TerminalStickyModifierState.swift` — modifier key state machine (inactive/armed/locked, double-tap locking)
|
||||
- `TerminalTextInputHandler@UIKit.swift` — IME state machine (marked text, preedit bridge, sticky modifier support)
|
||||
- `TerminalTextPosition.swift` — TerminalTextPosition / TerminalTextRange subclasses
|
||||
|
||||
The macOS equivalent uses `NSTextInputClient` in `AppTerminalView+NSTextInputClient.swift` with a parallel `TerminalTextInputHandler@AppKit.swift`.
|
||||
|
||||
### iOS Long-Press Text Selection
|
||||
|
||||
Long-press ≥0.5s on `UITerminalView` (single-finger, iOS only — Catalyst excluded) triggers `TerminalSurfaceTextSelectionRequestDelegate.terminalDidRequestTextSelection(_:)`. The host receives a `TerminalTextSelectionRequest` (viewport text snapshot + UTF-16 `NSRange?` for pre-selection + source point) and is expected to present a host UI (e.g. UITextView sheet). Word detection uses `ghostty_surface_quicklook_word` (Apple-only); `TerminalSelectionAnchor.resolveRange` maps the result to an `NSRange` via NSString UTF-16 calculations. Same-row duplicate occurrences are disambiguated by `pointX / cellWidthPoints`; callers must convert `cellPixels / displayScale → points` so ghostty's `tl_px_x/y` host-point units match. Prefix CJK full-width characters can shift cell-vs-UTF-16 columns and degrade disambiguation (ASCII-only correct, best-effort otherwise). The recognizer is gated by `gestureRecognizerShouldBegin` to stay inactive when no host has opted in. MVP supports only the `inMemory` backend.
|
||||
|
||||
In iPhone UI tests, synthesize ordinary terminal taps as explicitly short presses and verify `hasKeyboardFocus` before `typeText`; a loaded hosted runner can stretch `tap()` long enough for the selection recognizer to present its sheet. Keep the ordinary XCTest tap and typing path on iPad, where short presses do not reliably publish keyboard focus through accessibility.
|
||||
|
||||
### Manifest Sync
|
||||
|
||||
When changing SwiftPM products, targets, or test dependencies, update all three together:
|
||||
|
||||
- `Package.swift` — production manifest (remote XCFramework URL + checksum)
|
||||
- `Package.local.swift` — local development (path-based binary target)
|
||||
- `Package.swift.template` — CI template with `__DOWNLOAD_URL__` / `__CHECKSUM__` placeholders
|
||||
|
||||
### Release Versioning
|
||||
|
||||
- Bare semantic-version tags are GhosttyKit Swift package versions, independent of Ghostty upstream versions.
|
||||
- `Ghostty.ref` pins release builds to one immutable upstream commit; update it in a reviewed change instead of inferring an upstream ref from the package version.
|
||||
- `storage.<package-version>` owns the XCFramework asset referenced by the matching package tag.
|
||||
- arm64e release slices must contain real ARM64E Mach-O members and adapt pointer-authenticated callback, Block, Objective-C IMP, and system callback boundaries; never satisfy the architecture by changing only a fat-archive label.
|
||||
|
||||
## Swift Code Style
|
||||
|
||||
- **4-space indentation**, opening brace on same line
|
||||
- PascalCase types, camelCase properties/methods
|
||||
- PascalCase files for types, `+` for extensions (e.g., `AppTerminalView+Input.swift`)
|
||||
- **ObservableObject/@Published** for SwiftUI state that must support iOS 15 / Mac Catalyst 15
|
||||
- **Swift concurrency**: async/await, Task, actor, @MainActor
|
||||
- Early returns, guard statements, single responsibility per type/extension
|
||||
- Value types over reference types, composition over inheritance
|
||||
- Dependency injection over singletons
|
||||
- Avoid protocol-oriented design unless necessary
|
||||
- Split files frequently — keep files small and focused (~40-100 lines typical)
|
||||
- Don't extract methods unnecessarily — avoid premature abstraction
|
||||
|
||||
## Shell Script Style
|
||||
|
||||
- Shebang: `#!/bin/zsh`, failure handling: `set -euo pipefail`
|
||||
- Output: `[+]` success, `[-]` failure, lowercase messages
|
||||
- Minimal comments, no color output, assume tools available
|
||||
- Don't add if-checks when pipefail handles failures
|
||||
|
||||
## GhosttyKit Design Requirements
|
||||
|
||||
### Wrapper Design
|
||||
|
||||
- GhosttyTerminal must expose **all** functionality from `ghostty.h`
|
||||
- Clean Swift APIs mapping to C API: config, app lifecycle, surfaces, input, clipboard, inspector, splits, mouse, IME, text selection
|
||||
- Proper Swift patterns: enums for C enums, structs for C structs, closures for callbacks
|
||||
|
||||
### Example App Requirements
|
||||
|
||||
- Apps run in **App Sandbox** — must NOT spawn subprocesses (non-negotiable)
|
||||
- Use mock terminal IO with real GhosttyTerminal surface/view layer
|
||||
- Use host-managed I/O backend, never disable sandbox for PTY workarounds
|
||||
- Keep echo terminal as self-contained module separate from GhosttyKit integration
|
||||
1
vendor/libghostty-spm/CLAUDE.md
vendored
Symbolic link
1
vendor/libghostty-spm/CLAUDE.md
vendored
Symbolic link
@@ -0,0 +1 @@
|
||||
AGENTS.md
|
||||
13
vendor/libghostty-spm/Example/GhosttyTerminalApp.xcworkspace/contents.xcworkspacedata
generated
vendored
Normal file
13
vendor/libghostty-spm/Example/GhosttyTerminalApp.xcworkspace/contents.xcworkspacedata
generated
vendored
Normal file
@@ -0,0 +1,13 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Workspace
|
||||
version = "1.0">
|
||||
<FileRef
|
||||
location = "group:GhosttyTerminalApp.xcodeproj">
|
||||
</FileRef>
|
||||
<FileRef
|
||||
location = "group:MobileGhosttyApp.xcodeproj">
|
||||
</FileRef>
|
||||
<FileRef
|
||||
location = "group:../">
|
||||
</FileRef>
|
||||
</Workspace>
|
||||
@@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>IDEWorkspaceSharedSettings_AutocreateContextsIfNeeded</key>
|
||||
<false/>
|
||||
</dict>
|
||||
</plist>
|
||||
52
vendor/libghostty-spm/Example/GhosttyTerminalApp/AppDelegate.swift
vendored
Normal file
52
vendor/libghostty-spm/Example/GhosttyTerminalApp/AppDelegate.swift
vendored
Normal file
@@ -0,0 +1,52 @@
|
||||
import Cocoa
|
||||
import GhosttyTerminal
|
||||
|
||||
final class AppDelegate: NSObject, NSApplicationDelegate {
|
||||
private let defaultContentSize = NSSize(width: 720, height: 480)
|
||||
private let minimumContentSize = NSSize(width: 480, height: 320)
|
||||
private var window: NSWindow?
|
||||
|
||||
func applicationDidFinishLaunching(_: Notification) {
|
||||
TerminalDebugLog.enable(.standard)
|
||||
|
||||
let window = NSWindow(
|
||||
contentRect: NSRect(origin: .zero, size: defaultContentSize),
|
||||
styleMask: [.titled, .closable, .miniaturizable, .resizable],
|
||||
backing: .buffered,
|
||||
defer: false
|
||||
)
|
||||
window.title = "GhosttyKit Sandbox Demo"
|
||||
window.isOpaque = true
|
||||
window.backgroundColor = .windowBackgroundColor
|
||||
window.titlebarAppearsTransparent = false
|
||||
window.contentMinSize = minimumContentSize
|
||||
window.contentViewController = ViewController()
|
||||
window.center()
|
||||
window.makeKeyAndOrderFront(nil)
|
||||
repairRestoredWindowSizeIfNeeded(window)
|
||||
NSApp.activate(ignoringOtherApps: true)
|
||||
self.window = window
|
||||
}
|
||||
|
||||
func applicationSupportsSecureRestorableState(_: NSApplication) -> Bool {
|
||||
true
|
||||
}
|
||||
|
||||
func applicationShouldTerminateAfterLastWindowClosed(_: NSApplication) -> Bool {
|
||||
true
|
||||
}
|
||||
|
||||
private func repairRestoredWindowSizeIfNeeded(_ window: NSWindow) {
|
||||
DispatchQueue.main.async { [defaultContentSize, minimumContentSize] in
|
||||
let contentRect = window.contentRect(forFrameRect: window.frame)
|
||||
guard contentRect.width < minimumContentSize.width
|
||||
|| contentRect.height < minimumContentSize.height
|
||||
else {
|
||||
return
|
||||
}
|
||||
|
||||
window.setContentSize(defaultContentSize)
|
||||
window.center()
|
||||
}
|
||||
}
|
||||
}
|
||||
11
vendor/libghostty-spm/Example/GhosttyTerminalApp/AppIcon.icon/Assets/singaporedollarsign.svg
vendored
Normal file
11
vendor/libghostty-spm/Example/GhosttyTerminalApp/AppIcon.icon/Assets/singaporedollarsign.svg
vendored
Normal file
@@ -0,0 +1,11 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!--Generator: Apple Native CoreSVG 341-->
|
||||
<!DOCTYPE svg
|
||||
PUBLIC "-//W3C//DTD SVG 1.1//EN"
|
||||
"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 403.625 361.875">
|
||||
<g>
|
||||
<rect height="361.875" opacity="0" width="403.625" x="0" y="0"/>
|
||||
<path d="M93.5 332.625C146.625 332.625 185.75 302.875 185.75 249.25C185.75 201.25 154 181.875 110.625 168.625L86 161.25C56.125 152.25 31.5 137.5 31.5 105.125C31.5 70.375 59.625 51.5 90.875 51.5C121.125 51.5 148.25 68 154.25 100.75C155.75 107.75 159.875 111.375 166.875 111.375C173.875 111.375 179.25 107 179.25 99C179.25 96.375 178.875 93.25 178.125 90.25C168.875 49.625 132.375 27.25 91.25 27.25C42.75 27.25 4.625 58.625 4.625 106.5C4.625 151.75 34 173.5 77.5 186.625L102.25 194.25C134.25 203.875 158.75 218.375 158.75 251.75C158.75 287.25 130.625 308.375 94.625 308.375C58.625 308.375 31 289.5 24.75 255C22.875 247.375 18.75 244.25 12.125 244.25C4.875 244.25 0 249.5 0 257C0 261.75 0.625 265.75 1.75 269.625C11.875 309.75 49.25 332.625 93.5 332.625ZM314.375 322.625C362.875 322.625 403.625 297.125 403.625 248C403.625 204 372.25 186 334.125 171.75L306.625 161.5C281.75 152.5 254.5 137.75 254.5 108C254.5 77.25 282.5 60.875 313.5 60.875C345.75 60.875 367.625 78.25 373.25 110.25C375 117.25 379.25 121.625 386.5 121.625C394.25 121.625 399 116.875 399 109.25C399 107.25 398.875 103.75 398.125 100.125C390.25 58.5 354.75 37.375 313.5 37.375C268.125 37.375 228.125 63.375 228.125 109.375C228.125 149.5 257.75 170.25 295.875 184.5L323.375 194.875C352.375 205.375 377.5 218.75 377.5 250.125C377.5 282.875 350.25 299.125 314.5 299.125C278.625 299.125 253.875 282 248.875 246.75C247.625 239.75 242.875 235.125 235.625 235.125C228.625 235.125 223 239.75 223 247.875C223 249.75 223.25 251.875 223.5 253.75C231.125 300.625 269.375 322.625 314.375 322.625ZM313.25 361.75C318.25 361.75 322.375 357.625 322.375 352.75L322.375 9C322.375 4.125 318.25 0 313.25 0C308.25 0 304.125 4.125 304.125 9L304.125 352.75C304.125 357.625 308.25 361.75 313.25 361.75Z" fill="white" fill-opacity="0.85"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.1 KiB |
30
vendor/libghostty-spm/Example/GhosttyTerminalApp/AppIcon.icon/icon.json
vendored
Normal file
30
vendor/libghostty-spm/Example/GhosttyTerminalApp/AppIcon.icon/icon.json
vendored
Normal file
@@ -0,0 +1,30 @@
|
||||
{
|
||||
"fill" : {
|
||||
"automatic-gradient" : "srgb:1.00000,0.54098,0.84731,1.00000"
|
||||
},
|
||||
"groups" : [
|
||||
{
|
||||
"layers" : [
|
||||
{
|
||||
"glass" : false,
|
||||
"image-name" : "singaporedollarsign.svg",
|
||||
"name" : "singaporedollarsign"
|
||||
}
|
||||
],
|
||||
"shadow" : {
|
||||
"kind" : "neutral",
|
||||
"opacity" : 0.5
|
||||
},
|
||||
"translucency" : {
|
||||
"enabled" : true,
|
||||
"value" : 0.5
|
||||
}
|
||||
}
|
||||
],
|
||||
"supported-platforms" : {
|
||||
"circles" : [
|
||||
"watchOS"
|
||||
],
|
||||
"squares" : "shared"
|
||||
}
|
||||
}
|
||||
10
vendor/libghostty-spm/Example/GhosttyTerminalApp/Entitlement.entitlements
vendored
Normal file
10
vendor/libghostty-spm/Example/GhosttyTerminalApp/Entitlement.entitlements
vendored
Normal file
@@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>com.apple.security.app-sandbox</key>
|
||||
<true/>
|
||||
<key>com.apple.security.files.user-selected.read-only</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</plist>
|
||||
108
vendor/libghostty-spm/Example/GhosttyTerminalApp/ViewController.swift
vendored
Normal file
108
vendor/libghostty-spm/Example/GhosttyTerminalApp/ViewController.swift
vendored
Normal file
@@ -0,0 +1,108 @@
|
||||
import Cocoa
|
||||
import GhosttyTerminal
|
||||
import ShellCraftKit
|
||||
|
||||
private final class AppearanceAwareView: NSView {
|
||||
var onAppearanceChange: (() -> Void)?
|
||||
|
||||
override func viewDidChangeEffectiveAppearance() {
|
||||
super.viewDidChangeEffectiveAppearance()
|
||||
onAppearanceChange?()
|
||||
}
|
||||
}
|
||||
|
||||
final class ViewController: NSViewController {
|
||||
private lazy var terminalView: TerminalView = .init(
|
||||
frame: NSRect(x: 0, y: 0, width: 720, height: 480)
|
||||
)
|
||||
|
||||
private lazy var shellSession: ShellSession = .init(shell: defaultSandboxShell)
|
||||
|
||||
private lazy var controller: TerminalController = .init { builder in
|
||||
builder.withBackgroundOpacity(0)
|
||||
builder.withCustom("keybind", "super+k=text:\\x0c")
|
||||
}
|
||||
|
||||
override func loadView() {
|
||||
let container = AppearanceAwareView()
|
||||
container.onAppearanceChange = { [weak self] in
|
||||
self?.applyWindowBackgroundColor()
|
||||
}
|
||||
view = container
|
||||
}
|
||||
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
configureView()
|
||||
configureTerminalView()
|
||||
}
|
||||
|
||||
override func viewDidAppear() {
|
||||
super.viewDidAppear()
|
||||
activateTerminal()
|
||||
}
|
||||
|
||||
override func viewDidLayout() {
|
||||
super.viewDidLayout()
|
||||
terminalView.fitToSize()
|
||||
}
|
||||
|
||||
private func configureView() {
|
||||
view.wantsLayer = true
|
||||
applyWindowBackgroundColor()
|
||||
}
|
||||
|
||||
private func applyWindowBackgroundColor() {
|
||||
// `NSColor.windowBackgroundColor.cgColor` snapshots whichever
|
||||
// appearance is current at the call site, so drawing it naively
|
||||
// caches yesterday's light/dark value on the layer. Resolve it
|
||||
// under the view's effective appearance so the layer follows
|
||||
// system toggles.
|
||||
view.effectiveAppearance.performAsCurrentDrawingAppearance {
|
||||
view.layer?.backgroundColor = NSColor.windowBackgroundColor.cgColor
|
||||
}
|
||||
}
|
||||
|
||||
private func configureTerminalView() {
|
||||
terminalView.delegate = self
|
||||
terminalView.setAccessibilityElement(true)
|
||||
terminalView.setAccessibilityIdentifier("terminal.surface")
|
||||
terminalView.setAccessibilityLabel("Terminal Surface")
|
||||
terminalView.configuration = TerminalSurfaceOptions(
|
||||
backend: .inMemory(shellSession.terminalSession)
|
||||
)
|
||||
terminalView.controller = controller
|
||||
terminalView.translatesAutoresizingMaskIntoConstraints = false
|
||||
view.addSubview(terminalView)
|
||||
|
||||
NSLayoutConstraint.activate([
|
||||
terminalView.topAnchor.constraint(equalTo: view.topAnchor),
|
||||
terminalView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
|
||||
terminalView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
|
||||
terminalView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
|
||||
])
|
||||
}
|
||||
|
||||
private func activateTerminal() {
|
||||
view.window?.makeFirstResponder(terminalView)
|
||||
shellSession.start()
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Terminal Callbacks
|
||||
|
||||
extension ViewController:
|
||||
TerminalSurfaceTitleDelegate,
|
||||
TerminalSurfaceResizeDelegate,
|
||||
TerminalSurfaceCloseDelegate
|
||||
{
|
||||
func terminalDidChangeTitle(_ title: String) {
|
||||
view.window?.title = title
|
||||
}
|
||||
|
||||
func terminalDidResize(columns _: Int, rows _: Int) {}
|
||||
|
||||
func terminalDidClose(processAlive _: Bool) {
|
||||
view.window?.close()
|
||||
}
|
||||
}
|
||||
17
vendor/libghostty-spm/Example/GhosttyTerminalApp/main.swift
vendored
Normal file
17
vendor/libghostty-spm/Example/GhosttyTerminalApp/main.swift
vendored
Normal file
@@ -0,0 +1,17 @@
|
||||
//
|
||||
// main.swift
|
||||
// GhosttyTerminalApp
|
||||
//
|
||||
// Created by qaq on 19/3/2026.
|
||||
//
|
||||
|
||||
import AppKit
|
||||
|
||||
MainActor.assumeIsolated {
|
||||
let delegate = AppDelegate()
|
||||
let app = NSApplication.shared
|
||||
app.setActivationPolicy(.regular)
|
||||
app.delegate = delegate
|
||||
app.run()
|
||||
fatalError()
|
||||
}
|
||||
178
vendor/libghostty-spm/Example/GhosttyTerminalAppUITests/GhosttyTerminalAppUITests.swift
vendored
Normal file
178
vendor/libghostty-spm/Example/GhosttyTerminalAppUITests/GhosttyTerminalAppUITests.swift
vendored
Normal file
@@ -0,0 +1,178 @@
|
||||
import XCTest
|
||||
import AppKit
|
||||
|
||||
final class GhosttyTerminalAppUITests: XCTestCase {
|
||||
private var app: XCUIApplication!
|
||||
private var systemAlertMonitor: NSObjectProtocol?
|
||||
|
||||
override func setUpWithError() throws {
|
||||
continueAfterFailure = false
|
||||
app = XCUIApplication()
|
||||
app.launchArguments = ["--ui-testing"]
|
||||
systemAlertMonitor = installSystemAlertHandler()
|
||||
app.launch()
|
||||
}
|
||||
|
||||
override func tearDownWithError() throws {
|
||||
capture("final-state")
|
||||
app = nil
|
||||
}
|
||||
|
||||
func testTerminalUserOperations() throws {
|
||||
let terminal = try requireTerminalInteractionTarget()
|
||||
|
||||
capture("01-launch")
|
||||
clickTerminal(in: terminal)
|
||||
capture("02-focus")
|
||||
|
||||
typeTerminalText("echo mac-single\n", in: terminal)
|
||||
capture("03-single-line-input")
|
||||
|
||||
typeTerminalText("echo mac first line\n", in: terminal)
|
||||
typeTerminalText("echo mac second line\n", in: terminal)
|
||||
capture("04-multiple-lines")
|
||||
|
||||
typeTerminalText("中文键盘测试,标点和全角字符。\n", in: terminal)
|
||||
capture("05-chinese-input")
|
||||
|
||||
typeTerminalText("日本語キーボードテスト、かなと漢字。\n", in: terminal)
|
||||
capture("06-japanese-input")
|
||||
|
||||
typeTerminalText("Mixed input: English 中文 日本語 123\n", in: terminal)
|
||||
capture("07-multilingual-input")
|
||||
|
||||
terminal.swipeUp()
|
||||
capture("08-swipe-up")
|
||||
terminal.swipeDown()
|
||||
capture("09-swipe-down")
|
||||
|
||||
app.typeKey("=", modifierFlags: .command)
|
||||
capture("10-keyboard-zoom-in")
|
||||
app.typeKey("-", modifierFlags: .command)
|
||||
capture("11-keyboard-zoom-out")
|
||||
|
||||
typeTerminalText("clear\n", in: terminal)
|
||||
capture("12-clear-command")
|
||||
|
||||
assertNoCopyMenuWithoutSelection(in: terminal)
|
||||
typeTerminalText("echo selection anchor\n", in: terminal)
|
||||
dragPointerSelection(in: terminal)
|
||||
capture("13-pointer-selection")
|
||||
openCopyMenuAndCopySelection(in: terminal, screenshotName: "14-pointer-copy-menu")
|
||||
longPressTerminal(in: terminal)
|
||||
capture("15-long-press")
|
||||
}
|
||||
|
||||
private func requireTerminalInteractionTarget() throws -> XCUIElement {
|
||||
let terminal = app.descendants(matching: .any)["terminal.surface"].firstMatch
|
||||
if terminal.waitForExistence(timeout: 4), terminal.isHittable {
|
||||
return terminal
|
||||
}
|
||||
|
||||
let window = app.windows.firstMatch
|
||||
XCTAssertTrue(window.waitForExistence(timeout: 8))
|
||||
XCTAssertTrue(window.isHittable)
|
||||
return window
|
||||
}
|
||||
|
||||
private func clickTerminal(in element: XCUIElement) {
|
||||
element.coordinate(withNormalizedOffset: CGVector(dx: 0.5, dy: 0.55)).click()
|
||||
}
|
||||
|
||||
private func installSystemAlertHandler() -> NSObjectProtocol {
|
||||
addUIInterruptionMonitor(withDescription: "System alert") { alert in
|
||||
let preferredButtons = [
|
||||
"OK", "Ok", "好", "确定", "允许", "Allow", "继续", "Continue",
|
||||
"关闭", "Close", "Dismiss",
|
||||
]
|
||||
for title in preferredButtons {
|
||||
let button = alert.buttons[title].firstMatch
|
||||
if button.exists {
|
||||
button.click()
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
private func typeTerminalText(_ text: String, in element: XCUIElement) {
|
||||
clickTerminal(in: element)
|
||||
element.typeText(text)
|
||||
}
|
||||
|
||||
private func longPressTerminal(in element: XCUIElement) {
|
||||
element.coordinate(withNormalizedOffset: CGVector(dx: 0.5, dy: 0.55)).press(forDuration: 0.7)
|
||||
}
|
||||
|
||||
private func dragPointerSelection(in element: XCUIElement) {
|
||||
log("pointer-selection-coordinates", "start=(0.008, 0.10), end=(0.42, 0.10), rightClick=(0.20, 0.10)")
|
||||
let start = element.coordinate(withNormalizedOffset: CGVector(dx: 0.008, dy: 0.10))
|
||||
let end = element.coordinate(withNormalizedOffset: CGVector(dx: 0.42, dy: 0.10))
|
||||
start.press(forDuration: 0.1, thenDragTo: end)
|
||||
}
|
||||
|
||||
private func openCopyMenuAndCopySelection(in element: XCUIElement, screenshotName: String) {
|
||||
NSPasteboard.general.clearContents()
|
||||
disableSystemAlertMonitorBeforeContextMenu()
|
||||
element.coordinate(withNormalizedOffset: CGVector(dx: 0.20, dy: 0.10)).rightClick()
|
||||
let copy = copyMenuItem()
|
||||
if !copy.waitForExistence(timeout: 3) {
|
||||
capture("\(screenshotName)-missing")
|
||||
XCTFail("Copy menu item not found after pointer selection right click. Hierarchy: \(app.debugDescription)")
|
||||
return
|
||||
}
|
||||
copy.click()
|
||||
capture(screenshotName)
|
||||
let actual = copiedPasteboardText(timeout: 2)
|
||||
log("pointer-selection-pasteboard", actual ?? "<nil>")
|
||||
XCTAssertEqual(actual, "selection anchor")
|
||||
}
|
||||
|
||||
private func assertNoCopyMenuWithoutSelection(in element: XCUIElement) {
|
||||
disableSystemAlertMonitorBeforeContextMenu()
|
||||
element.coordinate(withNormalizedOffset: CGVector(dx: 0.20, dy: 0.10)).rightClick()
|
||||
XCTAssertFalse(
|
||||
copyMenuItem().waitForExistence(timeout: 0.5),
|
||||
"Copy menu item appeared without an active terminal selection."
|
||||
)
|
||||
}
|
||||
|
||||
private func disableSystemAlertMonitorBeforeContextMenu() {
|
||||
if let systemAlertMonitor {
|
||||
removeUIInterruptionMonitor(systemAlertMonitor)
|
||||
self.systemAlertMonitor = nil
|
||||
}
|
||||
}
|
||||
|
||||
private func copyMenuItem() -> XCUIElement {
|
||||
app.menuItems["Copy"].firstMatch
|
||||
}
|
||||
|
||||
private func copiedPasteboardText(timeout: TimeInterval) -> String? {
|
||||
let deadline = Date().addingTimeInterval(timeout)
|
||||
repeat {
|
||||
if let string = NSPasteboard.general.string(forType: .string) {
|
||||
return string
|
||||
}
|
||||
RunLoop.current.run(mode: .default, before: Date().addingTimeInterval(0.05))
|
||||
} while Date() < deadline
|
||||
return nil
|
||||
}
|
||||
|
||||
private func log(_ name: String, _ value: String) {
|
||||
let attachment = XCTAttachment(string: value)
|
||||
attachment.name = name
|
||||
attachment.lifetime = .keepAlways
|
||||
add(attachment)
|
||||
}
|
||||
|
||||
private func capture(_ name: String) {
|
||||
guard let app else { return }
|
||||
let attachment = XCTAttachment(screenshot: app.screenshot())
|
||||
attachment.name = name
|
||||
attachment.lifetime = .keepAlways
|
||||
add(attachment)
|
||||
}
|
||||
}
|
||||
29
vendor/libghostty-spm/Example/MobileGhosttyApp/AppDelegate.swift
vendored
Normal file
29
vendor/libghostty-spm/Example/MobileGhosttyApp/AppDelegate.swift
vendored
Normal file
@@ -0,0 +1,29 @@
|
||||
import GhosttyTerminal
|
||||
import UIKit
|
||||
|
||||
@main
|
||||
final class AppDelegate: UIResponder, UIApplicationDelegate {
|
||||
func application(
|
||||
_: UIApplication,
|
||||
didFinishLaunchingWithOptions _: [UIApplication.LaunchOptionsKey: Any]? = nil
|
||||
) -> Bool {
|
||||
TerminalDebugLog.enable(.standard)
|
||||
TerminalDebugLog.sink = { message in
|
||||
NSLog("%@", message)
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func application(
|
||||
_: UIApplication,
|
||||
configurationForConnecting connectingSceneSession: UISceneSession,
|
||||
options _: UIScene.ConnectionOptions
|
||||
) -> UISceneConfiguration {
|
||||
let config = UISceneConfiguration(
|
||||
name: "Default Configuration",
|
||||
sessionRole: connectingSceneSession.role
|
||||
)
|
||||
config.delegateClass = SceneDelegate.self
|
||||
return config
|
||||
}
|
||||
}
|
||||
11
vendor/libghostty-spm/Example/MobileGhosttyApp/AppIcon.icon/Assets/singaporedollarsign.svg
vendored
Normal file
11
vendor/libghostty-spm/Example/MobileGhosttyApp/AppIcon.icon/Assets/singaporedollarsign.svg
vendored
Normal file
@@ -0,0 +1,11 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!--Generator: Apple Native CoreSVG 341-->
|
||||
<!DOCTYPE svg
|
||||
PUBLIC "-//W3C//DTD SVG 1.1//EN"
|
||||
"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 403.625 361.875">
|
||||
<g>
|
||||
<rect height="361.875" opacity="0" width="403.625" x="0" y="0"/>
|
||||
<path d="M93.5 332.625C146.625 332.625 185.75 302.875 185.75 249.25C185.75 201.25 154 181.875 110.625 168.625L86 161.25C56.125 152.25 31.5 137.5 31.5 105.125C31.5 70.375 59.625 51.5 90.875 51.5C121.125 51.5 148.25 68 154.25 100.75C155.75 107.75 159.875 111.375 166.875 111.375C173.875 111.375 179.25 107 179.25 99C179.25 96.375 178.875 93.25 178.125 90.25C168.875 49.625 132.375 27.25 91.25 27.25C42.75 27.25 4.625 58.625 4.625 106.5C4.625 151.75 34 173.5 77.5 186.625L102.25 194.25C134.25 203.875 158.75 218.375 158.75 251.75C158.75 287.25 130.625 308.375 94.625 308.375C58.625 308.375 31 289.5 24.75 255C22.875 247.375 18.75 244.25 12.125 244.25C4.875 244.25 0 249.5 0 257C0 261.75 0.625 265.75 1.75 269.625C11.875 309.75 49.25 332.625 93.5 332.625ZM314.375 322.625C362.875 322.625 403.625 297.125 403.625 248C403.625 204 372.25 186 334.125 171.75L306.625 161.5C281.75 152.5 254.5 137.75 254.5 108C254.5 77.25 282.5 60.875 313.5 60.875C345.75 60.875 367.625 78.25 373.25 110.25C375 117.25 379.25 121.625 386.5 121.625C394.25 121.625 399 116.875 399 109.25C399 107.25 398.875 103.75 398.125 100.125C390.25 58.5 354.75 37.375 313.5 37.375C268.125 37.375 228.125 63.375 228.125 109.375C228.125 149.5 257.75 170.25 295.875 184.5L323.375 194.875C352.375 205.375 377.5 218.75 377.5 250.125C377.5 282.875 350.25 299.125 314.5 299.125C278.625 299.125 253.875 282 248.875 246.75C247.625 239.75 242.875 235.125 235.625 235.125C228.625 235.125 223 239.75 223 247.875C223 249.75 223.25 251.875 223.5 253.75C231.125 300.625 269.375 322.625 314.375 322.625ZM313.25 361.75C318.25 361.75 322.375 357.625 322.375 352.75L322.375 9C322.375 4.125 318.25 0 313.25 0C308.25 0 304.125 4.125 304.125 9L304.125 352.75C304.125 357.625 308.25 361.75 313.25 361.75Z" fill="white" fill-opacity="0.85"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.1 KiB |
30
vendor/libghostty-spm/Example/MobileGhosttyApp/AppIcon.icon/icon.json
vendored
Normal file
30
vendor/libghostty-spm/Example/MobileGhosttyApp/AppIcon.icon/icon.json
vendored
Normal file
@@ -0,0 +1,30 @@
|
||||
{
|
||||
"fill" : {
|
||||
"automatic-gradient" : "srgb:1.00000,0.54098,0.84731,1.00000"
|
||||
},
|
||||
"groups" : [
|
||||
{
|
||||
"layers" : [
|
||||
{
|
||||
"glass" : false,
|
||||
"image-name" : "singaporedollarsign.svg",
|
||||
"name" : "singaporedollarsign"
|
||||
}
|
||||
],
|
||||
"shadow" : {
|
||||
"kind" : "neutral",
|
||||
"opacity" : 0.5
|
||||
},
|
||||
"translucency" : {
|
||||
"enabled" : true,
|
||||
"value" : 0.5
|
||||
}
|
||||
}
|
||||
],
|
||||
"supported-platforms" : {
|
||||
"circles" : [
|
||||
"watchOS"
|
||||
],
|
||||
"squares" : "shared"
|
||||
}
|
||||
}
|
||||
29
vendor/libghostty-spm/Example/MobileGhosttyApp/ApplicationExitController.swift
vendored
Normal file
29
vendor/libghostty-spm/Example/MobileGhosttyApp/ApplicationExitController.swift
vendored
Normal file
@@ -0,0 +1,29 @@
|
||||
import UIKit
|
||||
|
||||
@MainActor
|
||||
enum ApplicationExitController {
|
||||
private static var hasRequestedExit = false
|
||||
|
||||
static func requestExit() {
|
||||
guard !hasRequestedExit else {
|
||||
return
|
||||
}
|
||||
|
||||
hasRequestedExit = true
|
||||
suspendIfAvailable()
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) {
|
||||
exit(EXIT_SUCCESS)
|
||||
}
|
||||
}
|
||||
|
||||
private static func suspendIfAvailable() {
|
||||
let selector = NSSelectorFromString("suspend")
|
||||
let application = UIApplication.shared
|
||||
|
||||
guard application.responds(to: selector) else {
|
||||
return
|
||||
}
|
||||
|
||||
_ = application.perform(selector)
|
||||
}
|
||||
}
|
||||
21
vendor/libghostty-spm/Example/MobileGhosttyApp/SceneDelegate.swift
vendored
Normal file
21
vendor/libghostty-spm/Example/MobileGhosttyApp/SceneDelegate.swift
vendored
Normal file
@@ -0,0 +1,21 @@
|
||||
import UIKit
|
||||
|
||||
final class SceneDelegate: UIResponder, UIWindowSceneDelegate {
|
||||
var window: UIWindow?
|
||||
|
||||
func scene(
|
||||
_ scene: UIScene,
|
||||
willConnectTo _: UISceneSession,
|
||||
options _: UIScene.ConnectionOptions
|
||||
) {
|
||||
guard let windowScene = scene as? UIWindowScene else {
|
||||
return
|
||||
}
|
||||
|
||||
let window = UIWindow(windowScene: windowScene)
|
||||
window.backgroundColor = .systemBackground
|
||||
window.rootViewController = UINavigationController(rootViewController: ViewController())
|
||||
window.makeKeyAndVisible()
|
||||
self.window = window
|
||||
}
|
||||
}
|
||||
86
vendor/libghostty-spm/Example/MobileGhosttyApp/TerminalSelectionViewController.swift
vendored
Normal file
86
vendor/libghostty-spm/Example/MobileGhosttyApp/TerminalSelectionViewController.swift
vendored
Normal file
@@ -0,0 +1,86 @@
|
||||
import UIKit
|
||||
|
||||
final class TerminalSelectionViewController: UIViewController {
|
||||
private let pendingText: String
|
||||
private let pendingAnchorRange: NSRange?
|
||||
|
||||
var onDone: (() -> Void)?
|
||||
|
||||
private lazy var textView: UITextView = {
|
||||
let view = UITextView()
|
||||
view.isEditable = false
|
||||
view.isSelectable = true
|
||||
view.accessibilityIdentifier = "terminal.selectionTextView"
|
||||
view.alwaysBounceVertical = true
|
||||
view.font = .monospacedSystemFont(ofSize: 14, weight: .regular)
|
||||
view.backgroundColor = .clear
|
||||
view.textColor = .label
|
||||
view.textContainerInset = .init(top: 12, left: 12, bottom: 12, right: 12)
|
||||
view.translatesAutoresizingMaskIntoConstraints = false
|
||||
return view
|
||||
}()
|
||||
|
||||
init(text: String, anchorRange: NSRange?) {
|
||||
pendingText = text
|
||||
pendingAnchorRange = anchorRange
|
||||
super.init(nibName: nil, bundle: nil)
|
||||
}
|
||||
|
||||
@available(*, unavailable)
|
||||
required init?(coder _: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
view.backgroundColor = .clear
|
||||
|
||||
textView.text = pendingText
|
||||
view.addSubview(textView)
|
||||
|
||||
// Pin to view edges (not safe area) so scrolled content extends
|
||||
// under the transparent navigation bar / home indicator the way
|
||||
// iOS sheets expect. UIKit's contentInsetAdjustmentBehavior keeps
|
||||
// the initial scroll position below the nav bar via
|
||||
// adjustedContentInset.
|
||||
NSLayoutConstraint.activate([
|
||||
textView.topAnchor.constraint(equalTo: view.topAnchor),
|
||||
textView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
|
||||
textView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
|
||||
textView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
|
||||
])
|
||||
|
||||
navigationItem.rightBarButtonItem = UIBarButtonItem(
|
||||
barButtonSystemItem: .done,
|
||||
target: self,
|
||||
action: #selector(handleDone)
|
||||
)
|
||||
navigationItem.rightBarButtonItem?.accessibilityIdentifier = "terminal.selectionDoneButton"
|
||||
}
|
||||
|
||||
override func viewWillAppear(_ animated: Bool) {
|
||||
super.viewWillAppear(animated)
|
||||
navigationController?.view.backgroundColor = .clear
|
||||
navigationController?.navigationBar.standardAppearance.configureWithTransparentBackground()
|
||||
navigationController?.navigationBar.scrollEdgeAppearance = navigationController?.navigationBar.standardAppearance
|
||||
}
|
||||
|
||||
override func viewDidAppear(_ animated: Bool) {
|
||||
super.viewDidAppear(animated)
|
||||
textView.becomeFirstResponder()
|
||||
|
||||
let nsText = textView.text as NSString
|
||||
if let range = pendingAnchorRange, NSMaxRange(range) <= nsText.length {
|
||||
textView.selectedRange = range
|
||||
textView.scrollRangeToVisible(range)
|
||||
} else {
|
||||
textView.selectAll(nil)
|
||||
}
|
||||
}
|
||||
|
||||
@objc private func handleDone() {
|
||||
dismiss(animated: true) { [weak self] in
|
||||
self?.onDone?()
|
||||
}
|
||||
}
|
||||
}
|
||||
262
vendor/libghostty-spm/Example/MobileGhosttyApp/ViewController.swift
vendored
Normal file
262
vendor/libghostty-spm/Example/MobileGhosttyApp/ViewController.swift
vendored
Normal file
@@ -0,0 +1,262 @@
|
||||
import GhosttyTerminal
|
||||
import GhosttyTheme
|
||||
import ShellCraftKit
|
||||
import UIKit
|
||||
|
||||
final class ViewController: UIViewController {
|
||||
private static let lightThemeKey = "SelectedTheme.light"
|
||||
private static let darkThemeKey = "SelectedTheme.dark"
|
||||
|
||||
private lazy var terminalView: TerminalView = .init(frame: .zero)
|
||||
private lazy var shellSession: ShellSession = .init(shell: defaultSandboxShell)
|
||||
private lazy var controller: TerminalController = .init(
|
||||
theme: Self.savedTerminalTheme()
|
||||
) { builder in
|
||||
builder.withBackgroundOpacity(0)
|
||||
}
|
||||
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
title = "Terminal"
|
||||
view.backgroundColor = .systemBackground
|
||||
view.isOpaque = true
|
||||
configureTerminalView()
|
||||
configureThemeMenu()
|
||||
applyBackgroundForCurrentAppearance()
|
||||
}
|
||||
|
||||
override func viewDidAppear(_ animated: Bool) {
|
||||
super.viewDidAppear(animated)
|
||||
activateTerminal()
|
||||
}
|
||||
|
||||
override func viewDidLayoutSubviews() {
|
||||
super.viewDidLayoutSubviews()
|
||||
terminalView.fitToSize()
|
||||
}
|
||||
|
||||
override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
|
||||
super.traitCollectionDidChange(previousTraitCollection)
|
||||
guard traitCollection.hasDifferentColorAppearance(comparedTo: previousTraitCollection) else {
|
||||
return
|
||||
}
|
||||
controller.setTheme(Self.savedTerminalTheme())
|
||||
applyBackgroundForCurrentAppearance()
|
||||
}
|
||||
|
||||
private func configureTerminalView() {
|
||||
terminalView.delegate = self
|
||||
terminalView.isAccessibilityElement = true
|
||||
terminalView.accessibilityIdentifier = "terminal.surface"
|
||||
terminalView.accessibilityLabel = "Terminal Surface"
|
||||
terminalView.configuration = TerminalSurfaceOptions(
|
||||
backend: .inMemory(shellSession.terminalSession)
|
||||
)
|
||||
terminalView.controller = controller
|
||||
terminalView.backgroundColor = .clear
|
||||
terminalView.isOpaque = false
|
||||
terminalView.translatesAutoresizingMaskIntoConstraints = false
|
||||
view.addSubview(terminalView)
|
||||
|
||||
NSLayoutConstraint.activate([
|
||||
terminalView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor),
|
||||
terminalView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
|
||||
terminalView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
|
||||
terminalView.bottomAnchor.constraint(equalTo: view.keyboardLayoutGuide.topAnchor),
|
||||
])
|
||||
}
|
||||
|
||||
private func activateTerminal() {
|
||||
terminalView.becomeFirstResponder()
|
||||
shellSession.start()
|
||||
}
|
||||
|
||||
// MARK: - Persistence
|
||||
|
||||
private static func savedTerminalTheme() -> TerminalTheme {
|
||||
let lightConfig = savedThemeDefinition(forKey: lightThemeKey)?
|
||||
.toTerminalConfiguration() ?? .alabaster
|
||||
let darkConfig = savedThemeDefinition(forKey: darkThemeKey)?
|
||||
.toTerminalConfiguration() ?? .afterglow
|
||||
return TerminalTheme(light: lightConfig, dark: darkConfig)
|
||||
}
|
||||
|
||||
private static func savedThemeDefinition(
|
||||
forKey key: String
|
||||
) -> GhosttyThemeDefinition? {
|
||||
guard let name = UserDefaults.standard.string(forKey: key) else {
|
||||
return nil
|
||||
}
|
||||
return GhosttyThemeCatalog.theme(named: name)
|
||||
}
|
||||
|
||||
private var isDarkMode: Bool {
|
||||
traitCollection.userInterfaceStyle == .dark
|
||||
}
|
||||
|
||||
private func saveTheme(_ theme: GhosttyThemeDefinition) {
|
||||
let key = isDarkMode ? Self.darkThemeKey : Self.lightThemeKey
|
||||
UserDefaults.standard.set(theme.name, forKey: key)
|
||||
}
|
||||
|
||||
private func applyBackgroundForCurrentAppearance() {
|
||||
let key = isDarkMode ? Self.darkThemeKey : Self.lightThemeKey
|
||||
guard let theme = Self.savedThemeDefinition(forKey: key) else { return }
|
||||
if let bgColor = UIColor(hexString: theme.background) {
|
||||
view.backgroundColor = bgColor
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Theme Menu
|
||||
|
||||
private func configureThemeMenu() {
|
||||
navigationItem.rightBarButtonItem = UIBarButtonItem(
|
||||
image: UIImage(systemName: "paintpalette"),
|
||||
menu: buildThemeMenu()
|
||||
)
|
||||
navigationItem.rightBarButtonItem?.accessibilityIdentifier = "terminal.themeButton"
|
||||
}
|
||||
|
||||
private func buildThemeMenu() -> UIMenu {
|
||||
let popular = buildSubmenu(
|
||||
title: "Popular",
|
||||
themes: [
|
||||
"Dracula", "Catppuccin Mocha", "Catppuccin Latte",
|
||||
"Nord", "Solarized Dark", "Solarized Light",
|
||||
"Gruvbox Dark", "Gruvbox Light", "Tokyo Night",
|
||||
"One Half Dark", "One Half Light", "Rose Pine",
|
||||
"Monokai Pro", "GitHub Dark", "GitHub Light",
|
||||
]
|
||||
)
|
||||
|
||||
let dark = UIMenu(
|
||||
title: "Dark",
|
||||
image: UIImage(systemName: "moon.fill"),
|
||||
children: alphabeticalSubmenus(
|
||||
themes: GhosttyThemeCatalog.allThemes.filter(\.isDark)
|
||||
)
|
||||
)
|
||||
|
||||
let light = UIMenu(
|
||||
title: "Light",
|
||||
image: UIImage(systemName: "sun.max.fill"),
|
||||
children: alphabeticalSubmenus(
|
||||
themes: GhosttyThemeCatalog.allThemes.filter { !$0.isDark }
|
||||
)
|
||||
)
|
||||
|
||||
return UIMenu(title: "Theme", children: [popular, dark, light])
|
||||
}
|
||||
|
||||
private func buildSubmenu(
|
||||
title: String,
|
||||
themes names: [String]
|
||||
) -> UIMenu {
|
||||
let actions = names.compactMap { name -> UIAction? in
|
||||
guard let theme = GhosttyThemeCatalog.theme(named: name) else {
|
||||
return nil
|
||||
}
|
||||
return themeAction(for: theme)
|
||||
}
|
||||
return UIMenu(
|
||||
title: title,
|
||||
image: UIImage(systemName: "star.fill"),
|
||||
children: actions
|
||||
)
|
||||
}
|
||||
|
||||
private func alphabeticalSubmenus(
|
||||
themes: [GhosttyThemeDefinition]
|
||||
) -> [UIMenu] {
|
||||
var grouped: [String: [GhosttyThemeDefinition]] = [:]
|
||||
for theme in themes {
|
||||
let letter = String(theme.name.prefix(1)).uppercased()
|
||||
let key = letter.first?.isLetter == true ? letter : "#"
|
||||
grouped[key, default: []].append(theme)
|
||||
}
|
||||
|
||||
return grouped.keys.sorted().map { key in
|
||||
UIMenu(
|
||||
title: key,
|
||||
children: grouped[key]!.map { themeAction(for: $0) }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private func themeAction(for theme: GhosttyThemeDefinition) -> UIAction {
|
||||
UIAction(title: theme.name) { [weak self] _ in
|
||||
self?.applyTheme(theme)
|
||||
}
|
||||
}
|
||||
|
||||
private func applyTheme(_ theme: GhosttyThemeDefinition) {
|
||||
saveTheme(theme)
|
||||
controller.setTheme(Self.savedTerminalTheme())
|
||||
|
||||
if let bgColor = UIColor(hexString: theme.background) {
|
||||
view.backgroundColor = bgColor
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Terminal Callbacks
|
||||
|
||||
extension ViewController:
|
||||
TerminalSurfaceTitleDelegate,
|
||||
TerminalSurfaceResizeDelegate,
|
||||
TerminalSurfaceCloseDelegate,
|
||||
TerminalSurfaceTextSelectionRequestDelegate,
|
||||
UIAdaptivePresentationControllerDelegate
|
||||
{
|
||||
func terminalDidChangeTitle(_ title: String) {
|
||||
self.title = title
|
||||
}
|
||||
|
||||
func terminalDidResize(columns _: Int, rows _: Int) {}
|
||||
|
||||
func terminalDidClose(processAlive _: Bool) {
|
||||
ApplicationExitController.requestExit()
|
||||
}
|
||||
|
||||
func terminalDidRequestTextSelection(_ request: TerminalTextSelectionRequest) {
|
||||
let selectionVC = TerminalSelectionViewController(
|
||||
text: request.text,
|
||||
anchorRange: request.anchorRange
|
||||
)
|
||||
selectionVC.onDone = { [weak self] in
|
||||
self?.terminalView.becomeFirstResponder()
|
||||
}
|
||||
let nav = UINavigationController(rootViewController: selectionVC)
|
||||
nav.modalPresentationStyle = .pageSheet
|
||||
nav.sheetPresentationController?.detents = [.medium(), .large()]
|
||||
nav.sheetPresentationController?.prefersGrabberVisible = true
|
||||
nav.presentationController?.delegate = self
|
||||
present(nav, animated: true)
|
||||
}
|
||||
|
||||
/// Covers the user-gesture (grabber swipe) dismiss path only —
|
||||
/// programmatic dismiss does not trigger this callback, so the Done
|
||||
/// button restores focus via `onDone` instead.
|
||||
func presentationControllerDidDismiss(_: UIPresentationController) {
|
||||
terminalView.becomeFirstResponder()
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - UIColor Hex
|
||||
|
||||
private extension UIColor {
|
||||
convenience init?(hexString: String) {
|
||||
let hex = hexString.hasPrefix("#") ? String(hexString.dropFirst()) : hexString
|
||||
guard hex.count == 6,
|
||||
let r = UInt8(hex.prefix(2), radix: 16),
|
||||
let g = UInt8(hex.dropFirst(2).prefix(2), radix: 16),
|
||||
let b = UInt8(hex.dropFirst(4).prefix(2), radix: 16)
|
||||
else { return nil }
|
||||
self.init(
|
||||
red: CGFloat(r) / 255,
|
||||
green: CGFloat(g) / 255,
|
||||
blue: CGFloat(b) / 255,
|
||||
alpha: 1
|
||||
)
|
||||
}
|
||||
}
|
||||
449
vendor/libghostty-spm/Example/MobileGhosttyAppUITests/MobileGhosttyAppUITests.swift
vendored
Normal file
449
vendor/libghostty-spm/Example/MobileGhosttyAppUITests/MobileGhosttyAppUITests.swift
vendored
Normal file
@@ -0,0 +1,449 @@
|
||||
import XCTest
|
||||
|
||||
#if targetEnvironment(macCatalyst)
|
||||
import AppKit
|
||||
#endif
|
||||
|
||||
#if canImport(UIKit)
|
||||
import UIKit
|
||||
#endif
|
||||
|
||||
final class MobileGhosttyAppUITests: XCTestCase {
|
||||
private var app: XCUIApplication!
|
||||
|
||||
override func setUpWithError() throws {
|
||||
continueAfterFailure = false
|
||||
app = XCUIApplication()
|
||||
app.launchArguments = ["--ui-testing"]
|
||||
installSystemAlertHandler()
|
||||
#if !targetEnvironment(macCatalyst)
|
||||
if UIDevice.current.userInterfaceIdiom == .pad {
|
||||
XCUIDevice.shared.orientation = .landscapeLeft
|
||||
}
|
||||
#endif
|
||||
app.launch()
|
||||
}
|
||||
|
||||
override func tearDownWithError() throws {
|
||||
capture("final-state")
|
||||
app = nil
|
||||
}
|
||||
|
||||
func testTerminalUserOperations() throws {
|
||||
let terminal = try requireTerminalInteractionTarget()
|
||||
|
||||
capture("01-launch")
|
||||
typeTerminalText("echo ui-single\n", in: terminal)
|
||||
capture("02-single-line-input")
|
||||
|
||||
typeTerminalText("echo first line\n", in: terminal)
|
||||
typeTerminalText("echo second line\n", in: terminal)
|
||||
capture("03-multiple-lines")
|
||||
|
||||
typeTerminalText("中文键盘测试,标点和全角字符。\n", in: terminal)
|
||||
capture("04-chinese-input")
|
||||
|
||||
typeTerminalText("日本語キーボードテスト、かなと漢字。\n", in: terminal)
|
||||
capture("05-japanese-input")
|
||||
|
||||
typeTerminalText("Mixed input: English 中文 日本語 123\n", in: terminal)
|
||||
capture("06-multilingual-input")
|
||||
|
||||
tapTerminal(in: terminal)
|
||||
capture("07-tap-dismiss-keyboard")
|
||||
tapTerminal(in: terminal)
|
||||
capture("08-tap-refocus")
|
||||
|
||||
typeTerminalText("help\n", in: terminal)
|
||||
capture("09-help-output")
|
||||
|
||||
terminal.swipeUp()
|
||||
capture("10-swipe-up")
|
||||
terminal.swipeDown()
|
||||
capture("11-swipe-down")
|
||||
|
||||
#if targetEnvironment(macCatalyst)
|
||||
app.typeKey("=", modifierFlags: .command)
|
||||
#else
|
||||
terminal.pinch(withScale: 1.25, velocity: 1.0)
|
||||
#endif
|
||||
capture("12-zoom-in")
|
||||
// Use the keyboard zoom-out path on iOS because XCTest's second
|
||||
// pinch in one test session can report an invalid coordinate.
|
||||
app.typeKey("-", modifierFlags: .command)
|
||||
capture("13-zoom-out")
|
||||
|
||||
typeTerminalText("clear\n", in: terminal)
|
||||
capture("14-clear-command")
|
||||
|
||||
let pointerSelectionCommand: String
|
||||
#if targetEnvironment(macCatalyst)
|
||||
pointerSelectionCommand = "echo \(catalystPointerSelectionPrefix)\(expectedPointerSelection)\n"
|
||||
#else
|
||||
pointerSelectionCommand = isIPad
|
||||
? "echo \(iPadPointerSelectionPrefix)\(expectedPointerSelection)\n"
|
||||
: "echo \(expectedPointerSelection)\n"
|
||||
#endif
|
||||
typeTerminalText(pointerSelectionCommand, in: terminal)
|
||||
#if targetEnvironment(macCatalyst)
|
||||
dragPointerSelection(in: terminal)
|
||||
capture("15-pointer-selection-catalyst")
|
||||
openCopyMenuAndCopySelection(in: terminal, screenshotName: "16-pointer-copy-menu-catalyst")
|
||||
longPressTerminal(in: terminal)
|
||||
capture("17-long-press-catalyst")
|
||||
#else
|
||||
if isIPad {
|
||||
longPressTerminal(in: terminal, offset: CGVector(dx: 0.35, dy: 0.18))
|
||||
XCTAssertTrue(selectionTextView().waitForExistence(timeout: 4))
|
||||
capture("15-long-press-selection")
|
||||
dismissSelectionSheet()
|
||||
|
||||
hideSoftwareKeyboardIfVisible()
|
||||
capture("16-ipad-keyboard-hidden-before-pointer")
|
||||
dragIPadPointerSelection(in: terminal)
|
||||
capture("17-ipad-pointer-selection")
|
||||
openCopyMenuAndCopySelection(
|
||||
in: terminal,
|
||||
screenshotName: "18-ipad-pointer-copy-menu",
|
||||
rightClickOffset: CGVector(dx: 0.10, dy: 0.035)
|
||||
)
|
||||
} else {
|
||||
longPressTerminal(in: terminal, offset: CGVector(dx: 0.35, dy: 0.18))
|
||||
XCTAssertTrue(selectionTextView().waitForExistence(timeout: 4))
|
||||
capture("15-long-press-selection")
|
||||
}
|
||||
#endif
|
||||
|
||||
#if !targetEnvironment(macCatalyst)
|
||||
if isIPad {
|
||||
tapTerminal(in: terminal)
|
||||
tapAccessoryButton("Tab", screenshotName: "16-accessory-tab")
|
||||
tapAccessoryButton("Esc", screenshotName: "17-accessory-esc")
|
||||
tapAccessoryButton("Right", screenshotName: "18-accessory-right")
|
||||
}
|
||||
#endif
|
||||
|
||||
#if targetEnvironment(macCatalyst)
|
||||
openThemeMenuAndSelectPopularTheme()
|
||||
capture("19-theme-menu-selection")
|
||||
#else
|
||||
if isIPad {
|
||||
openThemeMenuAndSelectPopularTheme()
|
||||
capture("19-theme-menu-selection")
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
private func installSystemAlertHandler() {
|
||||
addUIInterruptionMonitor(withDescription: "System alert") { alert in
|
||||
let preferredButtons = [
|
||||
"OK", "Ok", "好", "确定", "允许", "Allow", "继续", "Continue",
|
||||
"关闭", "Close", "Dismiss",
|
||||
]
|
||||
for title in preferredButtons {
|
||||
let button = alert.buttons[title].firstMatch
|
||||
if button.exists {
|
||||
self.activateInterruptionButton(button)
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
let firstButton = alert.buttons.firstMatch
|
||||
guard firstButton.exists else { return false }
|
||||
if firstButton.identifier == "InputSource" ||
|
||||
firstButton.label.hasPrefix("com.apple.inputmethod.")
|
||||
{
|
||||
self.app.typeKey(.escape, modifierFlags: [])
|
||||
return true
|
||||
}
|
||||
self.activateInterruptionButton(firstButton)
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
private func activateInterruptionButton(_ button: XCUIElement) {
|
||||
#if targetEnvironment(macCatalyst)
|
||||
button.click()
|
||||
#else
|
||||
button.tap()
|
||||
#endif
|
||||
}
|
||||
|
||||
private func requireTerminalInteractionTarget() throws -> XCUIElement {
|
||||
let terminal = app.descendants(matching: .any)["terminal.surface"].firstMatch
|
||||
if terminal.waitForExistence(timeout: 4), terminal.isHittable {
|
||||
return terminal
|
||||
}
|
||||
|
||||
let window = app.windows.firstMatch
|
||||
XCTAssertTrue(window.waitForExistence(timeout: 8))
|
||||
XCTAssertTrue(window.isHittable)
|
||||
return window
|
||||
}
|
||||
|
||||
private func tapTerminal(in element: XCUIElement) {
|
||||
let coordinate = element.coordinate(withNormalizedOffset: terminalInteractionOffset)
|
||||
#if targetEnvironment(macCatalyst)
|
||||
coordinate.click()
|
||||
#else
|
||||
if isIPad {
|
||||
coordinate.tap()
|
||||
} else {
|
||||
coordinate.press(forDuration: 0.01)
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
private func typeTerminalText(_ text: String, in element: XCUIElement) {
|
||||
#if targetEnvironment(macCatalyst)
|
||||
element.coordinate(withNormalizedOffset: terminalInteractionOffset).click()
|
||||
app.typeText(text)
|
||||
#else
|
||||
if !isIPad, !prepareTerminalForTyping(element) {
|
||||
return
|
||||
}
|
||||
element.typeText(text)
|
||||
#endif
|
||||
}
|
||||
|
||||
#if !targetEnvironment(macCatalyst)
|
||||
private func prepareTerminalForTyping(_ element: XCUIElement) -> Bool {
|
||||
if selectionTextView().exists {
|
||||
dismissSelectionSheet()
|
||||
}
|
||||
if waitForKeyboardFocus(in: element, timeout: 0.5) {
|
||||
return true
|
||||
}
|
||||
|
||||
for _ in 0 ..< 2 {
|
||||
tapTerminal(in: element)
|
||||
if selectionTextView().waitForExistence(timeout: 0.25) {
|
||||
dismissSelectionSheet()
|
||||
}
|
||||
if waitForKeyboardFocus(in: element, timeout: 2) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
XCTFail("Terminal did not acquire keyboard focus before typing")
|
||||
return false
|
||||
}
|
||||
|
||||
private func waitForKeyboardFocus(
|
||||
in element: XCUIElement,
|
||||
timeout: TimeInterval
|
||||
) -> Bool {
|
||||
let expectation = XCTNSPredicateExpectation(
|
||||
predicate: NSPredicate(format: "hasKeyboardFocus == true"),
|
||||
object: element
|
||||
)
|
||||
return XCTWaiter.wait(for: [expectation], timeout: timeout) == .completed
|
||||
}
|
||||
#endif
|
||||
|
||||
private func longPressTerminal(in element: XCUIElement, offset: CGVector? = nil) {
|
||||
element.coordinate(withNormalizedOffset: offset ?? terminalInteractionOffset).press(forDuration: 0.7)
|
||||
}
|
||||
|
||||
#if targetEnvironment(macCatalyst)
|
||||
private func dragPointerSelection(in element: XCUIElement) {
|
||||
log("pointer-selection-coordinates", "start=(0.008, 0.045), end=(0.42, 0.045), rightClick=(0.20, 0.045)")
|
||||
let start = element.coordinate(withNormalizedOffset: CGVector(dx: 0.008, dy: 0.045))
|
||||
let end = element.coordinate(withNormalizedOffset: CGVector(dx: 0.42, dy: 0.045))
|
||||
start.press(forDuration: 0.1, thenDragTo: end)
|
||||
}
|
||||
#else
|
||||
private var isIPad: Bool {
|
||||
UIDevice.current.userInterfaceIdiom == .pad
|
||||
}
|
||||
|
||||
private func dragIPadPointerSelection(in element: XCUIElement) {
|
||||
log(
|
||||
"ipad-pointer-selection-coordinates",
|
||||
"frame=\(element.frame) command=echo \(iPadPointerSelectionPrefix)\(expectedPointerSelection) start=(0.015, 0.035), end=(0.205, 0.035), rightClick=(0.10, 0.035)"
|
||||
)
|
||||
let start = element.coordinate(withNormalizedOffset: CGVector(dx: 0.015, dy: 0.035))
|
||||
let end = element.coordinate(withNormalizedOffset: CGVector(dx: 0.205, dy: 0.035))
|
||||
start.click(forDuration: 0.1, thenDragTo: end)
|
||||
}
|
||||
|
||||
private func hideSoftwareKeyboardIfVisible() {
|
||||
let hideKeyboard = app.buttons["Hide keyboard"].firstMatch
|
||||
if hideKeyboard.waitForExistence(timeout: 1), hideKeyboard.isHittable {
|
||||
hideKeyboard.tap()
|
||||
XCTAssertFalse(hideKeyboard.waitForExistence(timeout: 2))
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
private func openCopyMenuAndCopySelection(
|
||||
in element: XCUIElement,
|
||||
screenshotName: String,
|
||||
rightClickOffset: CGVector? = nil
|
||||
) {
|
||||
UIPasteboard.general.string = nil
|
||||
let offset = rightClickOffset ?? CGVector(dx: 0.20, dy: 0.045)
|
||||
log("pointer-copy-menu-coordinate", "frame=\(element.frame) rightClick=(\(offset.dx), \(offset.dy))")
|
||||
element.coordinate(withNormalizedOffset: offset).rightClick()
|
||||
let copy = copyMenuItem()
|
||||
if !copy.waitForExistence(timeout: 3) {
|
||||
capture("\(screenshotName)-missing")
|
||||
XCTFail("Copy menu item not found after pointer selection right click. Hierarchy: \(app.debugDescription)")
|
||||
return
|
||||
}
|
||||
capture(screenshotName)
|
||||
activateCopyMenuItem(copy)
|
||||
let actual = copiedSelectionText(in: element, timeout: 2)
|
||||
log("pointer-selection-pasteboard", actual ?? "<nil>")
|
||||
XCTAssertEqual(actual, expectedPointerSelection)
|
||||
}
|
||||
|
||||
private func activateCopyMenuItem(_ copy: XCUIElement) {
|
||||
#if targetEnvironment(macCatalyst)
|
||||
copy.click()
|
||||
#else
|
||||
copy.tap()
|
||||
#endif
|
||||
}
|
||||
|
||||
private func copyMenuItem() -> XCUIElement {
|
||||
#if targetEnvironment(macCatalyst)
|
||||
app.menuItems["Copy"].firstMatch
|
||||
#else
|
||||
app.buttons["Copy"].firstMatch
|
||||
#endif
|
||||
}
|
||||
|
||||
private func copiedSelectionText(in element: XCUIElement, timeout: TimeInterval) -> String? {
|
||||
let deadline = Date().addingTimeInterval(timeout)
|
||||
repeat {
|
||||
if let string = copiedSelectionTextSnapshot(in: element, timeout: 0.25) {
|
||||
return string
|
||||
}
|
||||
RunLoop.current.run(mode: .default, before: Date().addingTimeInterval(0.05))
|
||||
} while Date() < deadline
|
||||
return nil
|
||||
}
|
||||
|
||||
private func copiedSelectionTextSnapshot(in element: XCUIElement, timeout: TimeInterval) -> String? {
|
||||
#if !targetEnvironment(macCatalyst)
|
||||
return element.value as? String
|
||||
#else
|
||||
_ = timeout
|
||||
return UIPasteboard.general.string
|
||||
#endif
|
||||
}
|
||||
|
||||
private func log(_ name: String, _ value: String) {
|
||||
let attachment = XCTAttachment(string: value)
|
||||
attachment.name = name
|
||||
attachment.lifetime = .keepAlways
|
||||
add(attachment)
|
||||
}
|
||||
|
||||
#if targetEnvironment(macCatalyst)
|
||||
private var isIPad: Bool {
|
||||
false
|
||||
}
|
||||
#endif
|
||||
|
||||
private var terminalInteractionOffset: CGVector {
|
||||
CGVector(dx: 0.5, dy: 0.55)
|
||||
}
|
||||
|
||||
private func tapAccessoryButton(_ label: String, screenshotName: String) {
|
||||
dismissKeyboardOnboardingIfVisible()
|
||||
let button = app.buttons[label]
|
||||
guard button.waitForExistence(timeout: 2), button.isHittable else {
|
||||
capture("\(screenshotName)-not-visible")
|
||||
return
|
||||
}
|
||||
button.tap()
|
||||
capture(screenshotName)
|
||||
}
|
||||
|
||||
private func dismissKeyboardOnboardingIfVisible() {
|
||||
let continueButton = app.buttons["Continue"].firstMatch
|
||||
guard continueButton.waitForExistence(timeout: 1), continueButton.isHittable else {
|
||||
return
|
||||
}
|
||||
#if targetEnvironment(macCatalyst)
|
||||
continueButton.click()
|
||||
#else
|
||||
continueButton.tap()
|
||||
#endif
|
||||
}
|
||||
|
||||
private func openThemeMenuAndSelectPopularTheme() {
|
||||
let themeButton = app.buttons["terminal.themeButton"].firstMatch
|
||||
guard themeButton.waitForExistence(timeout: 2), themeButton.isHittable else {
|
||||
capture("theme-button-not-visible")
|
||||
return
|
||||
}
|
||||
themeButton.tap()
|
||||
capture("theme-menu-open")
|
||||
|
||||
let popular = app.buttons["Popular"].firstMatch
|
||||
if popular.waitForExistence(timeout: 1), popular.isHittable {
|
||||
popular.tap()
|
||||
capture("theme-menu-popular")
|
||||
}
|
||||
|
||||
let dracula = app.buttons["Dracula"].firstMatch
|
||||
if dracula.waitForExistence(timeout: 2), dracula.isHittable {
|
||||
dracula.tap()
|
||||
} else {
|
||||
capture("theme-dracula-not-visible")
|
||||
dismissOpenMenu()
|
||||
}
|
||||
}
|
||||
|
||||
private func dismissOpenMenu() {
|
||||
#if targetEnvironment(macCatalyst)
|
||||
app.typeKey(.escape, modifierFlags: [])
|
||||
#else
|
||||
let window = app.windows.firstMatch
|
||||
guard window.exists else { return }
|
||||
window.coordinate(withNormalizedOffset: CGVector(dx: 0.5, dy: 0.9)).tap()
|
||||
#endif
|
||||
}
|
||||
|
||||
private func dismissSelectionSheet() {
|
||||
let done = app.buttons["terminal.selectionDoneButton"].firstMatch
|
||||
if done.waitForExistence(timeout: 2), done.isHittable {
|
||||
done.tap()
|
||||
XCTAssertTrue(selectionTextView().waitForNonExistence(timeout: 3))
|
||||
}
|
||||
}
|
||||
|
||||
private func selectionTextView() -> XCUIElement {
|
||||
app.textViews["terminal.selectionTextView"].firstMatch
|
||||
}
|
||||
|
||||
private func capture(_ name: String) {
|
||||
guard let app else { return }
|
||||
let attachment = XCTAttachment(screenshot: app.screenshot())
|
||||
attachment.name = name
|
||||
attachment.lifetime = .keepAlways
|
||||
add(attachment)
|
||||
}
|
||||
|
||||
private var expectedPointerSelection: String {
|
||||
"selection anchor"
|
||||
}
|
||||
|
||||
private var iPadPointerSelectionPrefix: String {
|
||||
// iPadOS/XCTest clamps the indirect pointer drag start a couple of
|
||||
// cells inside the view edge. The prefix keeps the single fixed drag
|
||||
// selecting the same expected terminal text without retries.
|
||||
"xx"
|
||||
}
|
||||
|
||||
#if targetEnvironment(macCatalyst)
|
||||
private var catalystPointerSelectionPrefix: String {
|
||||
// Mac Catalyst clamps the left-edge pointer drag inside the first
|
||||
// text cell on GitHub runners. The prefix keeps the copied text
|
||||
// anchored to the same expected selection.
|
||||
"x"
|
||||
}
|
||||
#endif
|
||||
}
|
||||
1
vendor/libghostty-spm/Ghostty.ref
vendored
Normal file
1
vendor/libghostty-spm/Ghostty.ref
vendored
Normal file
@@ -0,0 +1 @@
|
||||
35e1a0160c4f6797e1bb1ef8e7a2b8c6b114ab58
|
||||
21
vendor/libghostty-spm/LICENSE
vendored
Normal file
21
vendor/libghostty-spm/LICENSE
vendored
Normal file
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2026 @Lakr233
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
55
vendor/libghostty-spm/Package.local.swift
vendored
Normal file
55
vendor/libghostty-spm/Package.local.swift
vendored
Normal file
@@ -0,0 +1,55 @@
|
||||
// swift-tools-version: 6.0
|
||||
import PackageDescription
|
||||
|
||||
let package = Package(
|
||||
name: "GhosttyKit",
|
||||
platforms: [
|
||||
.iOS(.v15),
|
||||
.macOS(.v13),
|
||||
.macCatalyst(.v15),
|
||||
],
|
||||
products: [
|
||||
.library(name: "GhosttyKit", targets: ["GhosttyKit"]),
|
||||
.library(name: "GhosttyTerminal", targets: ["GhosttyTerminal"]),
|
||||
.library(name: "ShellCraftKit", targets: ["ShellCraftKit"]),
|
||||
.library(name: "GhosttyTheme", targets: ["GhosttyTheme"]),
|
||||
],
|
||||
dependencies: [
|
||||
.package(url: "https://github.com/Lakr233/MSDisplayLink.git", from: "2.1.0"),
|
||||
],
|
||||
targets: [
|
||||
.target(
|
||||
name: "GhosttyKit",
|
||||
dependencies: ["libghostty"],
|
||||
path: "Sources/GhosttyKit",
|
||||
linkerSettings: [
|
||||
.linkedLibrary("c++"),
|
||||
.linkedFramework("Carbon", .when(platforms: [.macOS])),
|
||||
]
|
||||
),
|
||||
.target(
|
||||
name: "GhosttyTerminal",
|
||||
dependencies: ["GhosttyKit", "MSDisplayLink"],
|
||||
path: "Sources/GhosttyTerminal"
|
||||
),
|
||||
.target(
|
||||
name: "ShellCraftKit",
|
||||
dependencies: ["GhosttyTerminal"],
|
||||
path: "Sources/ShellCraftKit"
|
||||
),
|
||||
.target(
|
||||
name: "GhosttyTheme",
|
||||
dependencies: ["GhosttyTerminal"],
|
||||
path: "Sources/GhosttyTheme",
|
||||
exclude: ["LICENSE"]
|
||||
),
|
||||
.binaryTarget(
|
||||
name: "libghostty",
|
||||
path: "BinaryTarget/GhosttyKit.xcframework"
|
||||
),
|
||||
.testTarget(
|
||||
name: "GhosttyKitTest",
|
||||
dependencies: ["GhosttyKit", "GhosttyTerminal", "GhosttyTheme", "ShellCraftKit"]
|
||||
),
|
||||
]
|
||||
)
|
||||
59
vendor/libghostty-spm/Package.swift
vendored
Normal file
59
vendor/libghostty-spm/Package.swift
vendored
Normal file
@@ -0,0 +1,59 @@
|
||||
// swift-tools-version: 6.0
|
||||
import PackageDescription
|
||||
|
||||
// terminalX vendored 变体:改为 local-path 依赖,使 SPM 零网络解析
|
||||
// (构建环境的认证代理不被 SwiftPM 支持)。
|
||||
// 上游原始清单见 Package.swift.template / Package.local.swift。
|
||||
// libghostty xcframework (storage.1.3.2) 已预置于 BinaryTarget/。
|
||||
let package = Package(
|
||||
name: "GhosttyKit",
|
||||
platforms: [
|
||||
.iOS(.v15),
|
||||
.macOS(.v13),
|
||||
.macCatalyst(.v15),
|
||||
],
|
||||
products: [
|
||||
.library(name: "GhosttyKit", targets: ["GhosttyKit"]),
|
||||
.library(name: "GhosttyTerminal", targets: ["GhosttyTerminal"]),
|
||||
.library(name: "ShellCraftKit", targets: ["ShellCraftKit"]),
|
||||
.library(name: "GhosttyTheme", targets: ["GhosttyTheme"]),
|
||||
],
|
||||
dependencies: [
|
||||
.package(path: "../MSDisplayLink"),
|
||||
],
|
||||
targets: [
|
||||
.target(
|
||||
name: "GhosttyKit",
|
||||
dependencies: ["libghostty"],
|
||||
path: "Sources/GhosttyKit",
|
||||
linkerSettings: [
|
||||
.linkedLibrary("c++"),
|
||||
.linkedFramework("Carbon", .when(platforms: [.macOS])),
|
||||
]
|
||||
),
|
||||
.target(
|
||||
name: "GhosttyTerminal",
|
||||
dependencies: ["GhosttyKit", "MSDisplayLink"],
|
||||
path: "Sources/GhosttyTerminal"
|
||||
),
|
||||
.target(
|
||||
name: "ShellCraftKit",
|
||||
dependencies: ["GhosttyTerminal"],
|
||||
path: "Sources/ShellCraftKit"
|
||||
),
|
||||
.target(
|
||||
name: "GhosttyTheme",
|
||||
dependencies: ["GhosttyTerminal"],
|
||||
path: "Sources/GhosttyTheme",
|
||||
exclude: ["LICENSE"]
|
||||
),
|
||||
.binaryTarget(
|
||||
name: "libghostty",
|
||||
path: "BinaryTarget/GhosttyKit.xcframework"
|
||||
),
|
||||
.testTarget(
|
||||
name: "GhosttyKitTest",
|
||||
dependencies: ["GhosttyKit", "GhosttyTerminal", "GhosttyTheme", "ShellCraftKit"]
|
||||
),
|
||||
]
|
||||
)
|
||||
56
vendor/libghostty-spm/Package.swift.template
vendored
Normal file
56
vendor/libghostty-spm/Package.swift.template
vendored
Normal file
@@ -0,0 +1,56 @@
|
||||
// swift-tools-version: 6.0
|
||||
import PackageDescription
|
||||
|
||||
let package = Package(
|
||||
name: "GhosttyKit",
|
||||
platforms: [
|
||||
.iOS(.v15),
|
||||
.macOS(.v13),
|
||||
.macCatalyst(.v15),
|
||||
],
|
||||
products: [
|
||||
.library(name: "GhosttyKit", targets: ["GhosttyKit"]),
|
||||
.library(name: "GhosttyTerminal", targets: ["GhosttyTerminal"]),
|
||||
.library(name: "ShellCraftKit", targets: ["ShellCraftKit"]),
|
||||
.library(name: "GhosttyTheme", targets: ["GhosttyTheme"]),
|
||||
],
|
||||
dependencies: [
|
||||
.package(url: "https://github.com/Lakr233/MSDisplayLink.git", from: "2.1.0"),
|
||||
],
|
||||
targets: [
|
||||
.target(
|
||||
name: "GhosttyKit",
|
||||
dependencies: ["libghostty"],
|
||||
path: "Sources/GhosttyKit",
|
||||
linkerSettings: [
|
||||
.linkedLibrary("c++"),
|
||||
.linkedFramework("Carbon", .when(platforms: [.macOS])),
|
||||
]
|
||||
),
|
||||
.target(
|
||||
name: "GhosttyTerminal",
|
||||
dependencies: ["GhosttyKit", "MSDisplayLink"],
|
||||
path: "Sources/GhosttyTerminal"
|
||||
),
|
||||
.target(
|
||||
name: "ShellCraftKit",
|
||||
dependencies: ["GhosttyTerminal"],
|
||||
path: "Sources/ShellCraftKit"
|
||||
),
|
||||
.target(
|
||||
name: "GhosttyTheme",
|
||||
dependencies: ["GhosttyTerminal"],
|
||||
path: "Sources/GhosttyTheme",
|
||||
exclude: ["LICENSE"]
|
||||
),
|
||||
.binaryTarget(
|
||||
name: "libghostty",
|
||||
url: "__DOWNLOAD_URL__",
|
||||
checksum: "__CHECKSUM__"
|
||||
),
|
||||
.testTarget(
|
||||
name: "GhosttyKitTest",
|
||||
dependencies: ["GhosttyKit", "GhosttyTerminal", "GhosttyTheme", "ShellCraftKit"]
|
||||
),
|
||||
]
|
||||
)
|
||||
47
vendor/libghostty-spm/Patches/arm64e/ghostty/0001-arm64e-boundaries.patch
vendored
Normal file
47
vendor/libghostty-spm/Patches/arm64e/ghostty/0001-arm64e-boundaries.patch
vendored
Normal file
@@ -0,0 +1,47 @@
|
||||
diff --git a/build.zig.zon b/build.zig.zon
|
||||
index f80cd53b3..e6141add7 100644
|
||||
--- a/build.zig.zon
|
||||
+++ b/build.zig.zon
|
||||
@@ -26,10 +26,7 @@
|
||||
.lazy = true,
|
||||
},
|
||||
.zig_objc = .{
|
||||
- // mitchellh/zig-objc
|
||||
- .url = "https://deps.files.ghostty.org/zig_objc-f356ed02833f0f1b8e84d50bed9e807bf7cdc0ae.tar.gz",
|
||||
- .hash = "zig_objc-0.0.0-Ir_Sp5gTAQCvxxR7oVIrPXxXwsfKgVP7_wqoOQrZjFeK",
|
||||
- .lazy = true,
|
||||
+ .path = ".libghostty-spm/zig_objc",
|
||||
},
|
||||
.zig_js = .{
|
||||
// mitchellh/zig-js
|
||||
diff --git a/pkg/macos/video/display_link.zig b/pkg/macos/video/display_link.zig
|
||||
index 4866531f2..18e9e40ec 100644
|
||||
--- a/pkg/macos/video/display_link.zig
|
||||
+++ b/pkg/macos/video/display_link.zig
|
||||
@@ -2,6 +2,8 @@ const std = @import("std");
|
||||
const assert = std.debug.assert;
|
||||
const Allocator = std.mem.Allocator;
|
||||
const c = @import("c.zig").c;
|
||||
+extern "c" fn ghostty_arm64e_sign_function(*const anyopaque) *const anyopaque;
|
||||
+
|
||||
|
||||
pub const DisplayLink = opaque {
|
||||
pub const Error = error{
|
||||
@@ -58,7 +60,7 @@ pub const DisplayLink = opaque {
|
||||
) Error!void {
|
||||
if (c.CVDisplayLinkSetOutputCallback(
|
||||
@ptrCast(self),
|
||||
- @ptrCast(&(struct {
|
||||
+ @ptrCast(@alignCast(ghostty_arm64e_sign_function(@ptrCast(&(struct {
|
||||
fn callback(
|
||||
displayLink: *DisplayLink,
|
||||
inNow: *const c.CVTimeStamp,
|
||||
@@ -78,7 +80,7 @@ pub const DisplayLink = opaque {
|
||||
);
|
||||
return c.kCVReturnSuccess;
|
||||
}
|
||||
- }).callback),
|
||||
+ }).callback)))),
|
||||
userinfo,
|
||||
) != c.kCVReturnSuccess)
|
||||
return error.InvalidOperation;
|
||||
64
vendor/libghostty-spm/Patches/arm64e/zig-objc/0001-arm64e-boundaries.patch
vendored
Normal file
64
vendor/libghostty-spm/Patches/arm64e/zig-objc/0001-arm64e-boundaries.patch
vendored
Normal file
@@ -0,0 +1,64 @@
|
||||
diff --git a/src/block.zig b/src/block.zig
|
||||
index bd4b17d..4d43262 100644
|
||||
--- a/src/block.zig
|
||||
+++ b/src/block.zig
|
||||
@@ -4,6 +4,7 @@ const assert = std.debug.assert;
|
||||
const Allocator = std.mem.Allocator;
|
||||
const objc = @import("main.zig");
|
||||
|
||||
+extern "c" fn ghostty_arm64e_prepare_block(*anyopaque) void;
|
||||
// We have to use the raw C allocator for all heap allocation in here
|
||||
// because the objc runtime expects `malloc` to be used. If you don't use
|
||||
// malloc you'll get segfaults because the objc runtime will try to free
|
||||
@@ -42,7 +43,7 @@ pub fn Block(
|
||||
const Self = @This();
|
||||
const captures_info = @typeInfo(Captures).@"struct";
|
||||
const InvokeFn = FnType(anyopaque);
|
||||
- const descriptor: Descriptor = .{
|
||||
+ var descriptor: Descriptor = .{
|
||||
.reserved = 0,
|
||||
.size = @sizeOf(Context),
|
||||
.copy_helper = &descCopyHelper,
|
||||
@@ -90,6 +91,7 @@ pub fn Block(
|
||||
@field(ctx, field.name) = @field(captures, field.name);
|
||||
}
|
||||
|
||||
+ ghostty_arm64e_prepare_block(@ptrCast(&ctx));
|
||||
return ctx;
|
||||
}
|
||||
|
||||
diff --git a/src/class.zig b/src/class.zig
|
||||
index e0c989c..697fe31 100644
|
||||
--- a/src/class.zig
|
||||
+++ b/src/class.zig
|
||||
@@ -7,5 +7,7 @@ const objc = @import("main.zig");
|
||||
const MsgSend = @import("msg_send.zig").MsgSend;
|
||||
|
||||
+extern "c" fn ghostty_arm64e_sign_function(*const anyopaque) *const anyopaque;
|
||||
+
|
||||
pub const Class = struct {
|
||||
value: c.Class,
|
||||
|
||||
@@ -65,7 +67,12 @@ pub const Class = struct {
|
||||
assert(fn_info.params.len >= 2);
|
||||
assert(fn_info.params[0].type == c.id);
|
||||
assert(fn_info.params[1].type == c.SEL);
|
||||
- _ = c.class_replaceMethod(self.value, objc.sel(name).value, @ptrCast(&imp), null);
|
||||
+ _ = c.class_replaceMethod(
|
||||
+ self.value,
|
||||
+ objc.sel(name).value,
|
||||
+ @ptrCast(@alignCast(ghostty_arm64e_sign_function(@ptrCast(&imp)))),
|
||||
+ null,
|
||||
+ );
|
||||
}
|
||||
|
||||
// allows adding new methods; returns true on success.
|
||||
@@ -81,7 +88,7 @@ pub const Class = struct {
|
||||
return boolResult(c.class_addMethod(
|
||||
self.value,
|
||||
objc.sel(name).value,
|
||||
- @ptrCast(&imp),
|
||||
+ @ptrCast(@alignCast(ghostty_arm64e_sign_function(@ptrCast(&imp)))),
|
||||
&encoding,
|
||||
));
|
||||
}
|
||||
61
vendor/libghostty-spm/Patches/ghostty/0001-darwin-libghostty-install.sh
vendored
Executable file
61
vendor/libghostty-spm/Patches/ghostty/0001-darwin-libghostty-install.sh
vendored
Executable file
@@ -0,0 +1,61 @@
|
||||
#!/bin/zsh
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
SOURCE_DIR=${1:-}
|
||||
|
||||
if [ -z "$SOURCE_DIR" ]; then
|
||||
echo "[-] missing source_dir"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
BUILD_ZIG="$SOURCE_DIR/build.zig"
|
||||
MARKER="libghostty static install for Darwin"
|
||||
|
||||
if [ ! -f "$BUILD_ZIG" ]; then
|
||||
echo "[-] build.zig not found: $BUILD_ZIG"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if grep -Fq "$MARKER" "$BUILD_ZIG"; then
|
||||
echo "[+] patch already applied: 0001-darwin-libghostty-install"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if grep -Fq "const lib_shared = try buildpkg.GhosttyLib.initShared" "$BUILD_ZIG"; then
|
||||
sed -i '' \
|
||||
'/We shouldn'\''t have this guard but we don'\''t currently/,/^ }$/c\
|
||||
// libghostty static install for Darwin:\
|
||||
// upstream only wires this for non-Darwin today, but we need the\
|
||||
// static archive for our own XCFramework assembly pipeline.\
|
||||
lib_shared.installHeader();\
|
||||
if (config.target.result.os.tag.isDarwin()) {\
|
||||
lib_static.install("libghostty.a");\
|
||||
} else if (config.target.result.os.tag == .windows) {\
|
||||
lib_shared.install("ghostty-internal.dll");\
|
||||
lib_static.install("ghostty-internal-static.lib");\
|
||||
} else {\
|
||||
lib_shared.install("ghostty-internal.so");\
|
||||
lib_static.install("ghostty-internal.a");\
|
||||
}' \
|
||||
"$BUILD_ZIG"
|
||||
else
|
||||
sed -i '' \
|
||||
'/We shouldn'\''t have this guard but we don'\''t currently/,/^ }$/c\
|
||||
// libghostty static install for Darwin:\
|
||||
// upstream only wires this for non-Darwin today, but we need the\
|
||||
// static archive for our own XCFramework assembly pipeline.\
|
||||
libghostty_shared.installHeader(); // Only need one header\
|
||||
if (!config.target.result.os.tag.isDarwin()) {\
|
||||
libghostty_shared.install("libghostty.so");\
|
||||
}\
|
||||
libghostty_static.install("libghostty.a");' \
|
||||
"$BUILD_ZIG"
|
||||
fi
|
||||
|
||||
if ! grep -Fq "$MARKER" "$BUILD_ZIG"; then
|
||||
echo "[-] failed to apply patch: 0001-darwin-libghostty-install"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "[+] applied patch: 0001-darwin-libghostty-install"
|
||||
536
vendor/libghostty-spm/Patches/ghostty/0002-host-managed-io-modern.patch
vendored
Normal file
536
vendor/libghostty-spm/Patches/ghostty/0002-host-managed-io-modern.patch
vendored
Normal file
@@ -0,0 +1,536 @@
|
||||
diff --git a/include/ghostty.h b/include/ghostty.h
|
||||
index 72bbb57a8..d9e399220 100644
|
||||
--- a/include/ghostty.h
|
||||
+++ b/include/ghostty.h
|
||||
@@ -464,10 +464,26 @@ typedef enum {
|
||||
GHOSTTY_SURFACE_CONTEXT_SPLIT = 2,
|
||||
} ghostty_surface_context_e;
|
||||
|
||||
+typedef enum {
|
||||
+ GHOSTTY_SURFACE_IO_BACKEND_EXEC = 0,
|
||||
+ GHOSTTY_SURFACE_IO_BACKEND_HOST_MANAGED = 1,
|
||||
+} ghostty_surface_io_backend_e;
|
||||
+
|
||||
+typedef void (*ghostty_surface_receive_buffer_cb)(void*, const uint8_t*, size_t);
|
||||
+typedef void (*ghostty_surface_receive_resize_cb)(void*,
|
||||
+ uint16_t,
|
||||
+ uint16_t,
|
||||
+ uint32_t,
|
||||
+ uint32_t);
|
||||
+
|
||||
typedef struct {
|
||||
ghostty_platform_e platform_tag;
|
||||
ghostty_platform_u platform;
|
||||
void* userdata;
|
||||
+ ghostty_surface_io_backend_e backend;
|
||||
+ void* receive_userdata;
|
||||
+ ghostty_surface_receive_buffer_cb receive_buffer;
|
||||
+ ghostty_surface_receive_resize_cb receive_resize;
|
||||
double scale_factor;
|
||||
float font_size;
|
||||
const char* working_directory;
|
||||
@@ -1109,6 +1125,8 @@ GHOSTTY_API ghostty_surface_config_s ghostty_surface_inherited_config(ghostty_su
|
||||
GHOSTTY_API void ghostty_surface_update_config(ghostty_surface_t, ghostty_config_t);
|
||||
GHOSTTY_API bool ghostty_surface_needs_confirm_quit(ghostty_surface_t);
|
||||
GHOSTTY_API bool ghostty_surface_process_exited(ghostty_surface_t);
|
||||
+GHOSTTY_API void ghostty_surface_write_buffer(ghostty_surface_t, const uint8_t*, uintptr_t);
|
||||
+GHOSTTY_API void ghostty_surface_process_exit(ghostty_surface_t, uint32_t, uint64_t);
|
||||
GHOSTTY_API void ghostty_surface_refresh(ghostty_surface_t);
|
||||
GHOSTTY_API void ghostty_surface_draw(ghostty_surface_t);
|
||||
GHOSTTY_API void ghostty_surface_set_content_scale(ghostty_surface_t, double, double);
|
||||
diff --git a/src/Surface.zig b/src/Surface.zig
|
||||
index 2c4edc497..b35c4465e 100644
|
||||
--- a/src/Surface.zig
|
||||
+++ b/src/Surface.zig
|
||||
@@ -628,40 +628,48 @@ pub fn init(
|
||||
// This separate block ({}) is important because our errdefers must
|
||||
// be scoped here to be valid.
|
||||
{
|
||||
- var env = rt_surface.defaultTermioEnv() catch |err| env: {
|
||||
- // If an error occurs, we don't want to block surface startup.
|
||||
- log.warn("error getting env map for surface err={}", .{err});
|
||||
- break :env internal_os.getEnvMap(alloc) catch
|
||||
- std.process.EnvMap.init(alloc);
|
||||
- };
|
||||
- errdefer env.deinit();
|
||||
+ const backend: termio.Backend = switch (rt_surface.termioBackend()) {
|
||||
+ .exec => backend: {
|
||||
+ var env = rt_surface.defaultTermioEnv() catch |err| env: {
|
||||
+ log.warn("error getting env map for surface err={}", .{err});
|
||||
+ break :env internal_os.getEnvMap(alloc) catch
|
||||
+ std.process.EnvMap.init(alloc);
|
||||
+ };
|
||||
+ errdefer env.deinit();
|
||||
|
||||
- // don't leak GHOSTTY_LOG to any subprocesses
|
||||
- env.remove("GHOSTTY_LOG");
|
||||
+ env.remove("GHOSTTY_LOG");
|
||||
|
||||
- var buf: [18]u8 = undefined;
|
||||
- try env.put(
|
||||
- "GHOSTTY_SURFACE_ID",
|
||||
- std.fmt.bufPrint(&buf, "0x{x:0>16}", .{self.id}) catch unreachable,
|
||||
- );
|
||||
+ var buf: [18]u8 = undefined;
|
||||
+ try env.put(
|
||||
+ "GHOSTTY_SURFACE_ID",
|
||||
+ std.fmt.bufPrint(&buf, "0x{x:0>16}", .{self.id}) catch unreachable,
|
||||
+ );
|
||||
|
||||
- // Initialize our IO backend
|
||||
- var io_exec = try termio.Exec.init(alloc, .{
|
||||
- .command = command,
|
||||
- .env = env,
|
||||
- .env_override = config.env,
|
||||
- .shell_integration = config.@"shell-integration",
|
||||
- .shell_integration_features = config.@"shell-integration-features",
|
||||
- .cursor_blink = config.@"cursor-style-blink",
|
||||
- .working_directory = if (config.@"working-directory") |wd| wd.value() else null,
|
||||
- .resources_dir = global_state.resources_dir.host(),
|
||||
- .term = config.term,
|
||||
- .rt_pre_exec_info = .init(config),
|
||||
- .rt_post_fork_info = .init(config),
|
||||
- });
|
||||
- errdefer io_exec.deinit();
|
||||
+ var io_exec = try termio.Exec.init(alloc, .{
|
||||
+ .command = command,
|
||||
+ .env = env,
|
||||
+ .env_override = config.env,
|
||||
+ .shell_integration = config.@"shell-integration",
|
||||
+ .shell_integration_features = config.@"shell-integration-features",
|
||||
+ .cursor_blink = config.@"cursor-style-blink",
|
||||
+ .working_directory = if (config.@"working-directory") |wd| wd.value() else null,
|
||||
+ .resources_dir = global_state.resources_dir.host(),
|
||||
+ .term = config.term,
|
||||
+ .rt_pre_exec_info = .init(config),
|
||||
+ .rt_post_fork_info = .init(config),
|
||||
+ });
|
||||
+ errdefer io_exec.deinit();
|
||||
+
|
||||
+ break :backend .{ .exec = io_exec };
|
||||
+ },
|
||||
+
|
||||
+ .host_managed => .{
|
||||
+ .host_managed = termio.HostManaged.init(
|
||||
+ try rt_surface.hostManagedConfig(),
|
||||
+ ),
|
||||
+ },
|
||||
+ };
|
||||
|
||||
- // Initialize our IO mailbox
|
||||
var io_mailbox = try termio.Mailbox.initSPSC(alloc);
|
||||
errdefer io_mailbox.deinit(alloc);
|
||||
|
||||
@@ -669,7 +677,7 @@ pub fn init(
|
||||
.size = size,
|
||||
.full_config = config,
|
||||
.config = try termio.Termio.DerivedConfig.init(alloc, config),
|
||||
- .backend = .{ .exec = io_exec },
|
||||
+ .backend = backend,
|
||||
.mailbox = io_mailbox,
|
||||
.renderer_state = &self.renderer_state,
|
||||
.renderer_wakeup = render_thread.wakeup,
|
||||
@@ -1311,9 +1319,11 @@ fn childExitedAbnormally(
|
||||
const alloc = arena.allocator();
|
||||
|
||||
// Build up our command for the error message
|
||||
- const command = try std.mem.join(alloc, " ", switch (self.io.backend) {
|
||||
- .exec => |*exec| exec.subprocess.args,
|
||||
- });
|
||||
+ const command = switch (self.io.backend) {
|
||||
+ .exec => |*exec| try std.mem.join(alloc, " ", exec.subprocess.args),
|
||||
+ .host_managed => try alloc.dupe(u8, "host-managed session"),
|
||||
+ };
|
||||
+ defer alloc.free(command);
|
||||
const runtime_str = try std.fmt.allocPrint(alloc, "{d} ms", .{info.runtime_ms});
|
||||
|
||||
self.renderer_state.mutex.lock();
|
||||
diff --git a/src/apprt/embedded.zig b/src/apprt/embedded.zig
|
||||
index 7310159cc..0180884fc 100644
|
||||
--- a/src/apprt/embedded.zig
|
||||
+++ b/src/apprt/embedded.zig
|
||||
@@ -15,6 +15,7 @@ const input = @import("../input.zig");
|
||||
const internal_os = @import("../os/main.zig");
|
||||
const renderer = @import("../renderer.zig");
|
||||
const terminal = @import("../terminal/main.zig");
|
||||
+const termio = @import("../termio.zig");
|
||||
const CoreApp = @import("../App.zig");
|
||||
const CoreInspector = @import("../inspector/main.zig").Inspector;
|
||||
const CoreSurface = @import("../Surface.zig");
|
||||
@@ -411,6 +412,8 @@ pub const Surface = struct {
|
||||
app: *App,
|
||||
platform: Platform,
|
||||
userdata: ?*anyopaque = null,
|
||||
+ termio_backend: TermioBackend = .exec,
|
||||
+ host_io: HostIO = .{},
|
||||
core_surface: CoreSurface,
|
||||
content_scale: apprt.ContentScale,
|
||||
size: apprt.SurfaceSize,
|
||||
@@ -421,6 +424,17 @@ pub const Surface = struct {
|
||||
/// that getTitle works without the implementer needing to save it.
|
||||
title: ?[:0]const u8 = null,
|
||||
|
||||
+ pub const TermioBackend = enum(c_int) {
|
||||
+ exec = 0,
|
||||
+ host_managed = 1,
|
||||
+ };
|
||||
+
|
||||
+ pub const HostIO = struct {
|
||||
+ userdata: ?*anyopaque = null,
|
||||
+ receive_buffer: ?termio.HostManaged.Config.Write = null,
|
||||
+ receive_resize: ?termio.HostManaged.Config.Resize = null,
|
||||
+ };
|
||||
+
|
||||
/// Surface initialization options.
|
||||
pub const Options = extern struct {
|
||||
/// The platform that this surface is being initialized for and
|
||||
@@ -431,6 +445,20 @@ pub const Surface = struct {
|
||||
/// Userdata passed to some of the callbacks.
|
||||
userdata: ?*anyopaque = null,
|
||||
|
||||
+ /// The backend used to satisfy terminal IO for this surface.
|
||||
+ backend: TermioBackend = .exec,
|
||||
+
|
||||
+ /// Userdata passed to host-managed IO callbacks.
|
||||
+ receive_userdata: ?*anyopaque = null,
|
||||
+
|
||||
+ /// Called when Ghostty emits bytes that should be sent to the host
|
||||
+ /// transport.
|
||||
+ receive_buffer: ?termio.HostManaged.Config.Write = null,
|
||||
+
|
||||
+ /// Called when the terminal size changes and the host transport
|
||||
+ /// should be updated.
|
||||
+ receive_resize: ?termio.HostManaged.Config.Resize = null,
|
||||
+
|
||||
/// The scale factor of the screen.
|
||||
scale_factor: f64 = 1,
|
||||
|
||||
@@ -469,6 +497,12 @@ pub const Surface = struct {
|
||||
.app = app,
|
||||
.platform = try .init(opts.platform_tag, opts.platform),
|
||||
.userdata = opts.userdata,
|
||||
+ .termio_backend = opts.backend,
|
||||
+ .host_io = .{
|
||||
+ .userdata = opts.receive_userdata orelse opts.userdata,
|
||||
+ .receive_buffer = opts.receive_buffer,
|
||||
+ .receive_resize = opts.receive_resize,
|
||||
+ },
|
||||
.core_surface = undefined,
|
||||
.content_scale = .{
|
||||
.x = @floatCast(opts.scale_factor),
|
||||
@@ -634,6 +668,19 @@ pub const Surface = struct {
|
||||
return &self.core_surface;
|
||||
}
|
||||
|
||||
+ pub fn termioBackend(self: *const Surface) TermioBackend {
|
||||
+ return self.termio_backend;
|
||||
+ }
|
||||
+
|
||||
+ pub fn hostManagedConfig(self: *const Surface) !termio.HostManaged.Config {
|
||||
+ const write = self.host_io.receive_buffer orelse return error.HostManagedWriteRequired;
|
||||
+ return .{
|
||||
+ .userdata = self.host_io.userdata,
|
||||
+ .write = write,
|
||||
+ .resize = self.host_io.receive_resize,
|
||||
+ };
|
||||
+ }
|
||||
+
|
||||
pub fn rtApp(self: *const Surface) *App {
|
||||
return self.app;
|
||||
}
|
||||
@@ -1600,6 +1647,28 @@ pub const CAPI = struct {
|
||||
return surface.core_surface.child_exited;
|
||||
}
|
||||
|
||||
+ export fn ghostty_surface_write_buffer(
|
||||
+ surface: *Surface,
|
||||
+ ptr: [*]const u8,
|
||||
+ len: usize,
|
||||
+ ) void {
|
||||
+ if (len == 0) return;
|
||||
+ surface.core_surface.io.processOutput(ptr[0..len]);
|
||||
+ }
|
||||
+
|
||||
+ export fn ghostty_surface_process_exit(
|
||||
+ surface: *Surface,
|
||||
+ exit_code: u32,
|
||||
+ runtime_ms: u64,
|
||||
+ ) void {
|
||||
+ _ = surface.core_surface.io.surface_mailbox.push(.{
|
||||
+ .child_exited = .{
|
||||
+ .exit_code = exit_code,
|
||||
+ .runtime_ms = runtime_ms,
|
||||
+ },
|
||||
+ }, .{ .forever = {} });
|
||||
+ }
|
||||
+
|
||||
/// Returns true if the surface has a selection.
|
||||
export fn ghostty_surface_has_selection(surface: *Surface) bool {
|
||||
return surface.core_surface.hasSelection();
|
||||
diff --git a/src/termio.zig b/src/termio.zig
|
||||
index b16885109..6931fcb58 100644
|
||||
--- a/src/termio.zig
|
||||
+++ b/src/termio.zig
|
||||
@@ -23,6 +23,7 @@ const message = @import("termio/message.zig");
|
||||
pub const backend = @import("termio/backend.zig");
|
||||
pub const mailbox = @import("termio/mailbox.zig");
|
||||
pub const Exec = @import("termio/Exec.zig");
|
||||
+pub const HostManaged = @import("termio/HostManaged.zig");
|
||||
pub const Options = @import("termio/Options.zig");
|
||||
pub const Termio = @import("termio/Termio.zig");
|
||||
pub const Thread = @import("termio/Thread.zig");
|
||||
diff --git a/src/termio/HostManaged.zig b/src/termio/HostManaged.zig
|
||||
new file mode 100644
|
||||
index 000000000..0d1821350
|
||||
--- /dev/null
|
||||
+++ b/src/termio/HostManaged.zig
|
||||
@@ -0,0 +1,136 @@
|
||||
+const HostManaged = @This();
|
||||
+
|
||||
+const std = @import("std");
|
||||
+const Allocator = std.mem.Allocator;
|
||||
+const renderer = @import("../renderer.zig");
|
||||
+const terminal = @import("../terminal/main.zig");
|
||||
+const termio = @import("../termio.zig");
|
||||
+
|
||||
+userdata: ?*anyopaque = null,
|
||||
+write: Config.Write,
|
||||
+resize_cb: ?Config.Resize = null,
|
||||
+grid_size: renderer.GridSize = .{
|
||||
+ .columns = 0,
|
||||
+ .rows = 0,
|
||||
+},
|
||||
+screen_size: renderer.ScreenSize = .{
|
||||
+ .width = 0,
|
||||
+ .height = 0,
|
||||
+},
|
||||
+
|
||||
+pub const Config = struct {
|
||||
+ pub const Write = *const fn (?*anyopaque, [*]const u8, usize) callconv(.c) void;
|
||||
+ pub const Resize = *const fn (?*anyopaque, u16, u16, u32, u32) callconv(.c) void;
|
||||
+
|
||||
+ userdata: ?*anyopaque = null,
|
||||
+ write: Write,
|
||||
+ resize: ?Resize = null,
|
||||
+};
|
||||
+
|
||||
+pub fn init(cfg: Config) HostManaged {
|
||||
+ return .{
|
||||
+ .userdata = cfg.userdata,
|
||||
+ .write = cfg.write,
|
||||
+ .resize_cb = cfg.resize,
|
||||
+ };
|
||||
+}
|
||||
+
|
||||
+pub fn deinit(self: *HostManaged) void {
|
||||
+ _ = self;
|
||||
+}
|
||||
+
|
||||
+pub fn initTerminal(self: *HostManaged, term: *terminal.Terminal) void {
|
||||
+ self.grid_size = .{
|
||||
+ .columns = term.cols,
|
||||
+ .rows = term.rows,
|
||||
+ };
|
||||
+ self.screen_size = .{
|
||||
+ .width = term.width_px,
|
||||
+ .height = term.height_px,
|
||||
+ };
|
||||
+}
|
||||
+
|
||||
+pub fn threadEnter(
|
||||
+ self: *HostManaged,
|
||||
+ alloc: Allocator,
|
||||
+ io: *termio.Termio,
|
||||
+ td: *termio.Termio.ThreadData,
|
||||
+) !void {
|
||||
+ _ = alloc;
|
||||
+ _ = io;
|
||||
+
|
||||
+ td.backend = .{ .host_managed = .{} };
|
||||
+ self.notifyResize();
|
||||
+}
|
||||
+
|
||||
+pub fn threadExit(self: *HostManaged, td: *termio.Termio.ThreadData) void {
|
||||
+ _ = self;
|
||||
+ _ = td;
|
||||
+}
|
||||
+
|
||||
+pub fn focusGained(
|
||||
+ self: *HostManaged,
|
||||
+ td: *termio.Termio.ThreadData,
|
||||
+ focused: bool,
|
||||
+) !void {
|
||||
+ _ = self;
|
||||
+ _ = td;
|
||||
+ _ = focused;
|
||||
+}
|
||||
+
|
||||
+pub fn resize(
|
||||
+ self: *HostManaged,
|
||||
+ grid_size: renderer.GridSize,
|
||||
+ screen_size: renderer.ScreenSize,
|
||||
+) !void {
|
||||
+ self.grid_size = grid_size;
|
||||
+ self.screen_size = screen_size;
|
||||
+ self.notifyResize();
|
||||
+}
|
||||
+
|
||||
+pub fn queueWrite(
|
||||
+ self: *HostManaged,
|
||||
+ alloc: Allocator,
|
||||
+ td: *termio.Termio.ThreadData,
|
||||
+ data: []const u8,
|
||||
+ linefeed: bool,
|
||||
+) !void {
|
||||
+ _ = alloc;
|
||||
+ _ = td;
|
||||
+ _ = linefeed;
|
||||
+
|
||||
+ if (data.len == 0) return;
|
||||
+ self.write(self.userdata, data.ptr, data.len);
|
||||
+}
|
||||
+
|
||||
+pub fn childExitedAbnormally(
|
||||
+ self: *HostManaged,
|
||||
+ gpa: Allocator,
|
||||
+ t: *terminal.Terminal,
|
||||
+ exit_code: u32,
|
||||
+ runtime_ms: u64,
|
||||
+) !void {
|
||||
+ _ = self;
|
||||
+ _ = gpa;
|
||||
+ _ = t;
|
||||
+ _ = exit_code;
|
||||
+ _ = runtime_ms;
|
||||
+}
|
||||
+
|
||||
+fn notifyResize(self: *HostManaged) void {
|
||||
+ const resize_cb = self.resize_cb orelse return;
|
||||
+ resize_cb(
|
||||
+ self.userdata,
|
||||
+ self.grid_size.columns,
|
||||
+ self.grid_size.rows,
|
||||
+ self.screen_size.width,
|
||||
+ self.screen_size.height,
|
||||
+ );
|
||||
+}
|
||||
+
|
||||
+pub const ThreadData = struct {
|
||||
+ pub fn deinit(self: *ThreadData, alloc: Allocator) void {
|
||||
+ _ = self;
|
||||
+ _ = alloc;
|
||||
+ }
|
||||
+};
|
||||
diff --git a/src/termio/backend.zig b/src/termio/backend.zig
|
||||
index c29009acb..9857cbd7a 100644
|
||||
--- a/src/termio/backend.zig
|
||||
+++ b/src/termio/backend.zig
|
||||
@@ -11,28 +11,34 @@ const ProcessInfo = @import("../pty.zig").ProcessInfo;
|
||||
const WRITE_REQ_PREALLOC = std.math.pow(usize, 2, 5);
|
||||
|
||||
/// The kinds of backends.
|
||||
-pub const Kind = enum { exec };
|
||||
+pub const Kind = enum { exec, host_managed };
|
||||
|
||||
/// Configuration for the various backend types.
|
||||
pub const Config = union(Kind) {
|
||||
/// Exec uses posix exec to run a command with a pty.
|
||||
exec: termio.Exec.Config,
|
||||
+
|
||||
+ /// Host managed forwards terminal IO through embedder callbacks.
|
||||
+ host_managed: termio.HostManaged.Config,
|
||||
};
|
||||
|
||||
/// Backend implementations. A backend is responsible for owning the pty
|
||||
/// behavior and providing read/write capabilities.
|
||||
pub const Backend = union(Kind) {
|
||||
exec: termio.Exec,
|
||||
+ host_managed: termio.HostManaged,
|
||||
|
||||
pub fn deinit(self: *Backend) void {
|
||||
switch (self.*) {
|
||||
.exec => |*exec| exec.deinit(),
|
||||
+ .host_managed => |*host_managed| host_managed.deinit(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn initTerminal(self: *Backend, t: *terminal.Terminal) void {
|
||||
switch (self.*) {
|
||||
.exec => |*exec| exec.initTerminal(t),
|
||||
+ .host_managed => |*host_managed| host_managed.initTerminal(t),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -44,12 +50,14 @@ pub const Backend = union(Kind) {
|
||||
) !void {
|
||||
switch (self.*) {
|
||||
.exec => |*exec| try exec.threadEnter(alloc, io, td),
|
||||
+ .host_managed => |*host_managed| try host_managed.threadEnter(alloc, io, td),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn threadExit(self: *Backend, td: *termio.Termio.ThreadData) void {
|
||||
switch (self.*) {
|
||||
.exec => |*exec| exec.threadExit(td),
|
||||
+ .host_managed => |*host_managed| host_managed.threadExit(td),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -60,6 +68,7 @@ pub const Backend = union(Kind) {
|
||||
) !void {
|
||||
switch (self.*) {
|
||||
.exec => |*exec| try exec.focusGained(td, focused),
|
||||
+ .host_managed => |*host_managed| try host_managed.focusGained(td, focused),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -70,6 +79,7 @@ pub const Backend = union(Kind) {
|
||||
) !void {
|
||||
switch (self.*) {
|
||||
.exec => |*exec| try exec.resize(grid_size, screen_size),
|
||||
+ .host_managed => |*host_managed| try host_managed.resize(grid_size, screen_size),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -82,6 +92,7 @@ pub const Backend = union(Kind) {
|
||||
) !void {
|
||||
switch (self.*) {
|
||||
.exec => |*exec| try exec.queueWrite(alloc, td, data, linefeed),
|
||||
+ .host_managed => |*host_managed| try host_managed.queueWrite(alloc, td, data, linefeed),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -99,6 +110,12 @@ pub const Backend = union(Kind) {
|
||||
exit_code,
|
||||
runtime_ms,
|
||||
),
|
||||
+ .host_managed => |*host_managed| try host_managed.childExitedAbnormally(
|
||||
+ gpa,
|
||||
+ t,
|
||||
+ exit_code,
|
||||
+ runtime_ms,
|
||||
+ ),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -108,6 +125,7 @@ pub const Backend = union(Kind) {
|
||||
pub fn getProcessInfo(self: *Backend, comptime info: ProcessInfo) ?ProcessInfo.Type(info) {
|
||||
return switch (self.*) {
|
||||
.exec => |*exec| exec.getProcessInfo(info),
|
||||
+ .host_managed => null,
|
||||
};
|
||||
}
|
||||
};
|
||||
@@ -115,10 +133,12 @@ pub const Backend = union(Kind) {
|
||||
/// Termio thread data. See termio.ThreadData for docs.
|
||||
pub const ThreadData = union(Kind) {
|
||||
exec: termio.Exec.ThreadData,
|
||||
+ host_managed: termio.HostManaged.ThreadData,
|
||||
|
||||
pub fn deinit(self: *ThreadData, alloc: Allocator) void {
|
||||
switch (self.*) {
|
||||
.exec => |*exec| exec.deinit(alloc),
|
||||
+ .host_managed => |*host_managed| host_managed.deinit(alloc),
|
||||
}
|
||||
}
|
||||
|
||||
518
vendor/libghostty-spm/Patches/ghostty/0002-host-managed-io.patch
vendored
Normal file
518
vendor/libghostty-spm/Patches/ghostty/0002-host-managed-io.patch
vendored
Normal file
@@ -0,0 +1,518 @@
|
||||
diff --git a/include/ghostty.h b/include/ghostty.h
|
||||
index 40ff55c..4c4f01b 100644
|
||||
--- a/include/ghostty.h
|
||||
+++ b/include/ghostty.h
|
||||
@@ -437,10 +437,26 @@ typedef enum {
|
||||
GHOSTTY_SURFACE_CONTEXT_SPLIT = 2,
|
||||
} ghostty_surface_context_e;
|
||||
|
||||
+typedef enum {
|
||||
+ GHOSTTY_SURFACE_IO_BACKEND_EXEC = 0,
|
||||
+ GHOSTTY_SURFACE_IO_BACKEND_HOST_MANAGED = 1,
|
||||
+} ghostty_surface_io_backend_e;
|
||||
+
|
||||
+typedef void (*ghostty_surface_receive_buffer_cb)(void*, const uint8_t*, size_t);
|
||||
+typedef void (*ghostty_surface_receive_resize_cb)(void*,
|
||||
+ uint16_t,
|
||||
+ uint16_t,
|
||||
+ uint32_t,
|
||||
+ uint32_t);
|
||||
+
|
||||
typedef struct {
|
||||
ghostty_platform_e platform_tag;
|
||||
ghostty_platform_u platform;
|
||||
void* userdata;
|
||||
+ ghostty_surface_io_backend_e backend;
|
||||
+ void* receive_userdata;
|
||||
+ ghostty_surface_receive_buffer_cb receive_buffer;
|
||||
+ ghostty_surface_receive_resize_cb receive_resize;
|
||||
double scale_factor;
|
||||
float font_size;
|
||||
const char* working_directory;
|
||||
@@ -1080,6 +1096,8 @@ ghostty_surface_config_s ghostty_surface_inherited_config(ghostty_surface_t, gho
|
||||
void ghostty_surface_update_config(ghostty_surface_t, ghostty_config_t);
|
||||
bool ghostty_surface_needs_confirm_quit(ghostty_surface_t);
|
||||
bool ghostty_surface_process_exited(ghostty_surface_t);
|
||||
+void ghostty_surface_write_buffer(ghostty_surface_t, const uint8_t*, uintptr_t);
|
||||
+void ghostty_surface_process_exit(ghostty_surface_t, uint32_t, uint64_t);
|
||||
void ghostty_surface_refresh(ghostty_surface_t);
|
||||
void ghostty_surface_draw(ghostty_surface_t);
|
||||
void ghostty_surface_set_content_scale(ghostty_surface_t, double, double);
|
||||
diff --git a/src/Surface.zig b/src/Surface.zig
|
||||
index 4d66622..145d113 100644
|
||||
--- a/src/Surface.zig
|
||||
+++ b/src/Surface.zig
|
||||
@@ -620,34 +620,42 @@ pub fn init(
|
||||
// This separate block ({}) is important because our errdefers must
|
||||
// be scoped here to be valid.
|
||||
{
|
||||
- var env = rt_surface.defaultTermioEnv() catch |err| env: {
|
||||
- // If an error occurs, we don't want to block surface startup.
|
||||
- log.warn("error getting env map for surface err={}", .{err});
|
||||
- break :env internal_os.getEnvMap(alloc) catch
|
||||
- std.process.EnvMap.init(alloc);
|
||||
+ const backend: termio.Backend = switch (rt_surface.termioBackend()) {
|
||||
+ .exec => backend: {
|
||||
+ var env = rt_surface.defaultTermioEnv() catch |err| env: {
|
||||
+ log.warn("error getting env map for surface err={}", .{err});
|
||||
+ break :env internal_os.getEnvMap(alloc) catch
|
||||
+ std.process.EnvMap.init(alloc);
|
||||
+ };
|
||||
+ errdefer env.deinit();
|
||||
+
|
||||
+ env.remove("GHOSTTY_LOG");
|
||||
+
|
||||
+ var io_exec = try termio.Exec.init(alloc, .{
|
||||
+ .command = command,
|
||||
+ .env = env,
|
||||
+ .env_override = config.env,
|
||||
+ .shell_integration = config.@"shell-integration",
|
||||
+ .shell_integration_features = config.@"shell-integration-features",
|
||||
+ .cursor_blink = config.@"cursor-style-blink",
|
||||
+ .working_directory = if (config.@"working-directory") |wd| wd.value() else null,
|
||||
+ .resources_dir = global_state.resources_dir.host(),
|
||||
+ .term = config.term,
|
||||
+ .rt_pre_exec_info = .init(config),
|
||||
+ .rt_post_fork_info = .init(config),
|
||||
+ });
|
||||
+ errdefer io_exec.deinit();
|
||||
+
|
||||
+ break :backend .{ .exec = io_exec };
|
||||
+ },
|
||||
+
|
||||
+ .host_managed => .{
|
||||
+ .host_managed = termio.HostManaged.init(
|
||||
+ try rt_surface.hostManagedConfig(),
|
||||
+ ),
|
||||
+ },
|
||||
};
|
||||
- errdefer env.deinit();
|
||||
-
|
||||
- // don't leak GHOSTTY_LOG to any subprocesses
|
||||
- env.remove("GHOSTTY_LOG");
|
||||
-
|
||||
- // Initialize our IO backend
|
||||
- var io_exec = try termio.Exec.init(alloc, .{
|
||||
- .command = command,
|
||||
- .env = env,
|
||||
- .env_override = config.env,
|
||||
- .shell_integration = config.@"shell-integration",
|
||||
- .shell_integration_features = config.@"shell-integration-features",
|
||||
- .cursor_blink = config.@"cursor-style-blink",
|
||||
- .working_directory = if (config.@"working-directory") |wd| wd.value() else null,
|
||||
- .resources_dir = global_state.resources_dir.host(),
|
||||
- .term = config.term,
|
||||
- .rt_pre_exec_info = .init(config),
|
||||
- .rt_post_fork_info = .init(config),
|
||||
- });
|
||||
- errdefer io_exec.deinit();
|
||||
|
||||
- // Initialize our IO mailbox
|
||||
var io_mailbox = try termio.Mailbox.initSPSC(alloc);
|
||||
errdefer io_mailbox.deinit(alloc);
|
||||
|
||||
@@ -655,7 +663,7 @@ pub fn init(
|
||||
.size = size,
|
||||
.full_config = config,
|
||||
.config = try termio.Termio.DerivedConfig.init(alloc, config),
|
||||
- .backend = .{ .exec = io_exec },
|
||||
+ .backend = backend,
|
||||
.mailbox = io_mailbox,
|
||||
.renderer_state = &self.renderer_state,
|
||||
.renderer_wakeup = render_thread.wakeup,
|
||||
@@ -1289,9 +1297,11 @@ fn childExitedAbnormally(
|
||||
const alloc = arena.allocator();
|
||||
|
||||
// Build up our command for the error message
|
||||
- const command = try std.mem.join(alloc, " ", switch (self.io.backend) {
|
||||
- .exec => |*exec| exec.subprocess.args,
|
||||
- });
|
||||
+ const command = switch (self.io.backend) {
|
||||
+ .exec => |*exec| try std.mem.join(alloc, " ", exec.subprocess.args),
|
||||
+ .host_managed => try alloc.dupe(u8, "host-managed session"),
|
||||
+ };
|
||||
+ defer alloc.free(command);
|
||||
const runtime_str = try std.fmt.allocPrint(alloc, "{d} ms", .{info.runtime_ms});
|
||||
|
||||
self.renderer_state.mutex.lock();
|
||||
diff --git a/src/apprt/embedded.zig b/src/apprt/embedded.zig
|
||||
index 0d5a4f8..b44d4d3 100644
|
||||
--- a/src/apprt/embedded.zig
|
||||
+++ b/src/apprt/embedded.zig
|
||||
@@ -15,6 +15,7 @@ const input = @import("../input.zig");
|
||||
const internal_os = @import("../os/main.zig");
|
||||
const renderer = @import("../renderer.zig");
|
||||
const terminal = @import("../terminal/main.zig");
|
||||
+const termio = @import("../termio.zig");
|
||||
const CoreApp = @import("../App.zig");
|
||||
const CoreInspector = @import("../inspector/main.zig").Inspector;
|
||||
const CoreSurface = @import("../Surface.zig");
|
||||
@@ -409,6 +410,8 @@ pub const Surface = struct {
|
||||
app: *App,
|
||||
platform: Platform,
|
||||
userdata: ?*anyopaque = null,
|
||||
+ termio_backend: TermioBackend = .exec,
|
||||
+ host_io: HostIO = .{},
|
||||
core_surface: CoreSurface,
|
||||
content_scale: apprt.ContentScale,
|
||||
size: apprt.SurfaceSize,
|
||||
@@ -419,6 +422,17 @@ pub const Surface = struct {
|
||||
/// that getTitle works without the implementer needing to save it.
|
||||
title: ?[:0]const u8 = null,
|
||||
|
||||
+ pub const TermioBackend = enum(c_int) {
|
||||
+ exec = 0,
|
||||
+ host_managed = 1,
|
||||
+ };
|
||||
+
|
||||
+ pub const HostIO = struct {
|
||||
+ userdata: ?*anyopaque = null,
|
||||
+ receive_buffer: ?termio.HostManaged.Config.Write = null,
|
||||
+ receive_resize: ?termio.HostManaged.Config.Resize = null,
|
||||
+ };
|
||||
+
|
||||
/// Surface initialization options.
|
||||
pub const Options = extern struct {
|
||||
/// The platform that this surface is being initialized for and
|
||||
@@ -429,6 +443,20 @@ pub const Surface = struct {
|
||||
/// Userdata passed to some of the callbacks.
|
||||
userdata: ?*anyopaque = null,
|
||||
|
||||
+ /// The backend used to satisfy terminal IO for this surface.
|
||||
+ backend: TermioBackend = .exec,
|
||||
+
|
||||
+ /// Userdata passed to host-managed IO callbacks.
|
||||
+ receive_userdata: ?*anyopaque = null,
|
||||
+
|
||||
+ /// Called when Ghostty emits bytes that should be sent to the host
|
||||
+ /// transport.
|
||||
+ receive_buffer: ?termio.HostManaged.Config.Write = null,
|
||||
+
|
||||
+ /// Called when the terminal size changes and the host transport
|
||||
+ /// should be updated.
|
||||
+ receive_resize: ?termio.HostManaged.Config.Resize = null,
|
||||
+
|
||||
/// The scale factor of the screen.
|
||||
scale_factor: f64 = 1,
|
||||
|
||||
@@ -467,6 +495,12 @@ pub const Surface = struct {
|
||||
.app = app,
|
||||
.platform = try .init(opts.platform_tag, opts.platform),
|
||||
.userdata = opts.userdata,
|
||||
+ .termio_backend = opts.backend,
|
||||
+ .host_io = .{
|
||||
+ .userdata = opts.receive_userdata orelse opts.userdata,
|
||||
+ .receive_buffer = opts.receive_buffer,
|
||||
+ .receive_resize = opts.receive_resize,
|
||||
+ },
|
||||
.core_surface = undefined,
|
||||
.content_scale = .{
|
||||
.x = @floatCast(opts.scale_factor),
|
||||
@@ -632,6 +666,19 @@ pub const Surface = struct {
|
||||
return &self.core_surface;
|
||||
}
|
||||
|
||||
+ pub fn termioBackend(self: *const Surface) TermioBackend {
|
||||
+ return self.termio_backend;
|
||||
+ }
|
||||
+
|
||||
+ pub fn hostManagedConfig(self: *const Surface) !termio.HostManaged.Config {
|
||||
+ const write = self.host_io.receive_buffer orelse return error.HostManagedWriteRequired;
|
||||
+ return .{
|
||||
+ .userdata = self.host_io.userdata,
|
||||
+ .write = write,
|
||||
+ .resize = self.host_io.receive_resize,
|
||||
+ };
|
||||
+ }
|
||||
+
|
||||
pub fn rtApp(self: *const Surface) *App {
|
||||
return self.app;
|
||||
}
|
||||
@@ -1598,6 +1645,28 @@ pub const CAPI = struct {
|
||||
return surface.core_surface.child_exited;
|
||||
}
|
||||
|
||||
+ export fn ghostty_surface_write_buffer(
|
||||
+ surface: *Surface,
|
||||
+ ptr: [*]const u8,
|
||||
+ len: usize,
|
||||
+ ) void {
|
||||
+ if (len == 0) return;
|
||||
+ surface.core_surface.io.processOutput(ptr[0..len]);
|
||||
+ }
|
||||
+
|
||||
+ export fn ghostty_surface_process_exit(
|
||||
+ surface: *Surface,
|
||||
+ exit_code: u32,
|
||||
+ runtime_ms: u64,
|
||||
+ ) void {
|
||||
+ _ = surface.core_surface.io.surface_mailbox.push(.{
|
||||
+ .child_exited = .{
|
||||
+ .exit_code = exit_code,
|
||||
+ .runtime_ms = runtime_ms,
|
||||
+ },
|
||||
+ }, .{ .forever = {} });
|
||||
+ }
|
||||
+
|
||||
/// Returns true if the surface has a selection.
|
||||
export fn ghostty_surface_has_selection(surface: *Surface) bool {
|
||||
return surface.core_surface.hasSelection();
|
||||
diff --git a/src/termio.zig b/src/termio.zig
|
||||
index b168851..6931fcb 100644
|
||||
--- a/src/termio.zig
|
||||
+++ b/src/termio.zig
|
||||
@@ -23,6 +23,7 @@ const message = @import("termio/message.zig");
|
||||
pub const backend = @import("termio/backend.zig");
|
||||
pub const mailbox = @import("termio/mailbox.zig");
|
||||
pub const Exec = @import("termio/Exec.zig");
|
||||
+pub const HostManaged = @import("termio/HostManaged.zig");
|
||||
pub const Options = @import("termio/Options.zig");
|
||||
pub const Termio = @import("termio/Termio.zig");
|
||||
pub const Thread = @import("termio/Thread.zig");
|
||||
diff --git a/src/termio/HostManaged.zig b/src/termio/HostManaged.zig
|
||||
new file mode 100644
|
||||
index 0000000..0d18213
|
||||
--- /dev/null
|
||||
+++ b/src/termio/HostManaged.zig
|
||||
@@ -0,0 +1,136 @@
|
||||
+const HostManaged = @This();
|
||||
+
|
||||
+const std = @import("std");
|
||||
+const Allocator = std.mem.Allocator;
|
||||
+const renderer = @import("../renderer.zig");
|
||||
+const terminal = @import("../terminal/main.zig");
|
||||
+const termio = @import("../termio.zig");
|
||||
+
|
||||
+userdata: ?*anyopaque = null,
|
||||
+write: Config.Write,
|
||||
+resize_cb: ?Config.Resize = null,
|
||||
+grid_size: renderer.GridSize = .{
|
||||
+ .columns = 0,
|
||||
+ .rows = 0,
|
||||
+},
|
||||
+screen_size: renderer.ScreenSize = .{
|
||||
+ .width = 0,
|
||||
+ .height = 0,
|
||||
+},
|
||||
+
|
||||
+pub const Config = struct {
|
||||
+ pub const Write = *const fn (?*anyopaque, [*]const u8, usize) callconv(.c) void;
|
||||
+ pub const Resize = *const fn (?*anyopaque, u16, u16, u32, u32) callconv(.c) void;
|
||||
+
|
||||
+ userdata: ?*anyopaque = null,
|
||||
+ write: Write,
|
||||
+ resize: ?Resize = null,
|
||||
+};
|
||||
+
|
||||
+pub fn init(cfg: Config) HostManaged {
|
||||
+ return .{
|
||||
+ .userdata = cfg.userdata,
|
||||
+ .write = cfg.write,
|
||||
+ .resize_cb = cfg.resize,
|
||||
+ };
|
||||
+}
|
||||
+
|
||||
+pub fn deinit(self: *HostManaged) void {
|
||||
+ _ = self;
|
||||
+}
|
||||
+
|
||||
+pub fn initTerminal(self: *HostManaged, term: *terminal.Terminal) void {
|
||||
+ self.grid_size = .{
|
||||
+ .columns = term.cols,
|
||||
+ .rows = term.rows,
|
||||
+ };
|
||||
+ self.screen_size = .{
|
||||
+ .width = term.width_px,
|
||||
+ .height = term.height_px,
|
||||
+ };
|
||||
+}
|
||||
+
|
||||
+pub fn threadEnter(
|
||||
+ self: *HostManaged,
|
||||
+ alloc: Allocator,
|
||||
+ io: *termio.Termio,
|
||||
+ td: *termio.Termio.ThreadData,
|
||||
+) !void {
|
||||
+ _ = alloc;
|
||||
+ _ = io;
|
||||
+
|
||||
+ td.backend = .{ .host_managed = .{} };
|
||||
+ self.notifyResize();
|
||||
+}
|
||||
+
|
||||
+pub fn threadExit(self: *HostManaged, td: *termio.Termio.ThreadData) void {
|
||||
+ _ = self;
|
||||
+ _ = td;
|
||||
+}
|
||||
+
|
||||
+pub fn focusGained(
|
||||
+ self: *HostManaged,
|
||||
+ td: *termio.Termio.ThreadData,
|
||||
+ focused: bool,
|
||||
+) !void {
|
||||
+ _ = self;
|
||||
+ _ = td;
|
||||
+ _ = focused;
|
||||
+}
|
||||
+
|
||||
+pub fn resize(
|
||||
+ self: *HostManaged,
|
||||
+ grid_size: renderer.GridSize,
|
||||
+ screen_size: renderer.ScreenSize,
|
||||
+) !void {
|
||||
+ self.grid_size = grid_size;
|
||||
+ self.screen_size = screen_size;
|
||||
+ self.notifyResize();
|
||||
+}
|
||||
+
|
||||
+pub fn queueWrite(
|
||||
+ self: *HostManaged,
|
||||
+ alloc: Allocator,
|
||||
+ td: *termio.Termio.ThreadData,
|
||||
+ data: []const u8,
|
||||
+ linefeed: bool,
|
||||
+) !void {
|
||||
+ _ = alloc;
|
||||
+ _ = td;
|
||||
+ _ = linefeed;
|
||||
+
|
||||
+ if (data.len == 0) return;
|
||||
+ self.write(self.userdata, data.ptr, data.len);
|
||||
+}
|
||||
+
|
||||
+pub fn childExitedAbnormally(
|
||||
+ self: *HostManaged,
|
||||
+ gpa: Allocator,
|
||||
+ t: *terminal.Terminal,
|
||||
+ exit_code: u32,
|
||||
+ runtime_ms: u64,
|
||||
+) !void {
|
||||
+ _ = self;
|
||||
+ _ = gpa;
|
||||
+ _ = t;
|
||||
+ _ = exit_code;
|
||||
+ _ = runtime_ms;
|
||||
+}
|
||||
+
|
||||
+fn notifyResize(self: *HostManaged) void {
|
||||
+ const resize_cb = self.resize_cb orelse return;
|
||||
+ resize_cb(
|
||||
+ self.userdata,
|
||||
+ self.grid_size.columns,
|
||||
+ self.grid_size.rows,
|
||||
+ self.screen_size.width,
|
||||
+ self.screen_size.height,
|
||||
+ );
|
||||
+}
|
||||
+
|
||||
+pub const ThreadData = struct {
|
||||
+ pub fn deinit(self: *ThreadData, alloc: Allocator) void {
|
||||
+ _ = self;
|
||||
+ _ = alloc;
|
||||
+ }
|
||||
+};
|
||||
diff --git a/src/termio/backend.zig b/src/termio/backend.zig
|
||||
index ae0e200..f1d35d2 100644
|
||||
--- a/src/termio/backend.zig
|
||||
+++ b/src/termio/backend.zig
|
||||
@@ -10,28 +10,34 @@ const termio = @import("../termio.zig");
|
||||
const WRITE_REQ_PREALLOC = std.math.pow(usize, 2, 5);
|
||||
|
||||
/// The kinds of backends.
|
||||
-pub const Kind = enum { exec };
|
||||
+pub const Kind = enum { exec, host_managed };
|
||||
|
||||
/// Configuration for the various backend types.
|
||||
pub const Config = union(Kind) {
|
||||
/// Exec uses posix exec to run a command with a pty.
|
||||
exec: termio.Exec.Config,
|
||||
+
|
||||
+ /// Host managed forwards terminal IO through embedder callbacks.
|
||||
+ host_managed: termio.HostManaged.Config,
|
||||
};
|
||||
|
||||
/// Backend implementations. A backend is responsible for owning the pty
|
||||
/// behavior and providing read/write capabilities.
|
||||
pub const Backend = union(Kind) {
|
||||
exec: termio.Exec,
|
||||
+ host_managed: termio.HostManaged,
|
||||
|
||||
pub fn deinit(self: *Backend) void {
|
||||
switch (self.*) {
|
||||
.exec => |*exec| exec.deinit(),
|
||||
+ .host_managed => |*host_managed| host_managed.deinit(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn initTerminal(self: *Backend, t: *terminal.Terminal) void {
|
||||
switch (self.*) {
|
||||
.exec => |*exec| exec.initTerminal(t),
|
||||
+ .host_managed => |*host_managed| host_managed.initTerminal(t),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -43,12 +49,14 @@ pub const Backend = union(Kind) {
|
||||
) !void {
|
||||
switch (self.*) {
|
||||
.exec => |*exec| try exec.threadEnter(alloc, io, td),
|
||||
+ .host_managed => |*host_managed| try host_managed.threadEnter(alloc, io, td),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn threadExit(self: *Backend, td: *termio.Termio.ThreadData) void {
|
||||
switch (self.*) {
|
||||
.exec => |*exec| exec.threadExit(td),
|
||||
+ .host_managed => |*host_managed| host_managed.threadExit(td),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -59,6 +67,7 @@ pub const Backend = union(Kind) {
|
||||
) !void {
|
||||
switch (self.*) {
|
||||
.exec => |*exec| try exec.focusGained(td, focused),
|
||||
+ .host_managed => |*host_managed| try host_managed.focusGained(td, focused),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -69,6 +78,7 @@ pub const Backend = union(Kind) {
|
||||
) !void {
|
||||
switch (self.*) {
|
||||
.exec => |*exec| try exec.resize(grid_size, screen_size),
|
||||
+ .host_managed => |*host_managed| try host_managed.resize(grid_size, screen_size),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -81,6 +91,7 @@ pub const Backend = union(Kind) {
|
||||
) !void {
|
||||
switch (self.*) {
|
||||
.exec => |*exec| try exec.queueWrite(alloc, td, data, linefeed),
|
||||
+ .host_managed => |*host_managed| try host_managed.queueWrite(alloc, td, data, linefeed),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -98,6 +109,12 @@ pub const Backend = union(Kind) {
|
||||
exit_code,
|
||||
runtime_ms,
|
||||
),
|
||||
+ .host_managed => |*host_managed| try host_managed.childExitedAbnormally(
|
||||
+ gpa,
|
||||
+ t,
|
||||
+ exit_code,
|
||||
+ runtime_ms,
|
||||
+ ),
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -105,10 +122,12 @@ pub const Backend = union(Kind) {
|
||||
/// Termio thread data. See termio.ThreadData for docs.
|
||||
pub const ThreadData = union(Kind) {
|
||||
exec: termio.Exec.ThreadData,
|
||||
+ host_managed: termio.HostManaged.ThreadData,
|
||||
|
||||
pub fn deinit(self: *ThreadData, alloc: Allocator) void {
|
||||
switch (self.*) {
|
||||
.exec => |*exec| exec.deinit(alloc),
|
||||
+ .host_managed => |*host_managed| host_managed.deinit(alloc),
|
||||
}
|
||||
}
|
||||
|
||||
1720
vendor/libghostty-spm/Patches/ghostty/0003-prebuilt-framedata.patch
vendored
Normal file
1720
vendor/libghostty-spm/Patches/ghostty/0003-prebuilt-framedata.patch
vendored
Normal file
File diff suppressed because it is too large
Load Diff
97
vendor/libghostty-spm/Patches/ghostty/0004-ios-fixes.sh
vendored
Executable file
97
vendor/libghostty-spm/Patches/ghostty/0004-ios-fixes.sh
vendored
Executable file
@@ -0,0 +1,97 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
SOURCE_DIR="${1:?Usage: $0 <ghostty-source-dir>}"
|
||||
|
||||
# Patch 1: cf_release_thread — ignore loop.run errors on iOS
|
||||
# The kqueue-based event loop panics on iOS simulator due to mach port issues
|
||||
CF_RELEASE="${SOURCE_DIR}/src/os/cf_release_thread.zig"
|
||||
if [ -f "$CF_RELEASE" ]; then
|
||||
if grep -q 'try self.loop.run(.until_done);' "$CF_RELEASE"; then
|
||||
sed -i '' 's/try self\.loop\.run(\.until_done);/self.loop.run(.until_done) catch |err| { log.warn("cf release loop failed err={}", .{err}); return; };/' "$CF_RELEASE"
|
||||
echo "[+] patched cf_release_thread to ignore loop errors"
|
||||
else
|
||||
echo "[+] cf_release_thread already patched"
|
||||
fi
|
||||
fi
|
||||
|
||||
# Patch 2: Disable private window blur API (App Store compliance)
|
||||
EMBEDDED="${SOURCE_DIR}/src/apprt/embedded.zig"
|
||||
if [ -f "$EMBEDDED" ]; then
|
||||
if grep -q 'CGSSetWindowBackgroundBlurRadius' "$EMBEDDED"; then
|
||||
python3 - "$EMBEDDED" <<'PY'
|
||||
from pathlib import Path
|
||||
import sys
|
||||
|
||||
path = Path(sys.argv[1])
|
||||
text = path.read_text()
|
||||
|
||||
old_fn = """ export fn ghostty_set_window_background_blur(
|
||||
app: *App,
|
||||
window: *anyopaque,
|
||||
) void {
|
||||
// This is only supported on macOS
|
||||
if (comptime builtin.target.os.tag != .macos) return;
|
||||
|
||||
const config = &app.config;
|
||||
|
||||
// Do nothing if we don't have background transparency enabled
|
||||
if (config.@"background-opacity" >= 1.0) return;
|
||||
|
||||
const nswindow = objc.Object.fromId(window);
|
||||
_ = CGSSetWindowBackgroundBlurRadius(
|
||||
CGSDefaultConnectionForThread(),
|
||||
nswindow.msgSend(usize, objc.sel("windowNumber"), .{}),
|
||||
@intCast(config.@"background-blur".cval()),
|
||||
);
|
||||
}
|
||||
|
||||
/// See ghostty_set_window_background_blur
|
||||
extern "c" fn CGSSetWindowBackgroundBlurRadius(*anyopaque, usize, c_int) i32;
|
||||
extern "c" fn CGSDefaultConnectionForThread() *anyopaque;"""
|
||||
|
||||
new_fn = """ export fn ghostty_set_window_background_blur(
|
||||
app: *App,
|
||||
window: *anyopaque,
|
||||
) void {
|
||||
_ = app;
|
||||
_ = window;
|
||||
return;
|
||||
}"""
|
||||
|
||||
if old_fn not in text:
|
||||
print("[+] blur patch already applied or source changed")
|
||||
else:
|
||||
path.write_text(text.replace(old_fn, new_fn))
|
||||
print("[+] patched: disabled private blur API")
|
||||
PY
|
||||
else
|
||||
echo "[+] blur patch already applied"
|
||||
fi
|
||||
fi
|
||||
|
||||
# Patch 3: Link Metal frameworks
|
||||
BUILD_ZIG="${SOURCE_DIR}/pkg/macos/build.zig"
|
||||
if [ -f "$BUILD_ZIG" ]; then
|
||||
if ! grep -q 'lib.linkFramework("Metal")' "$BUILD_ZIG"; then
|
||||
perl -0pi -e 's/lib\.linkFramework\("IOSurface"\);/lib.linkFramework("IOSurface");\n lib.linkFramework("Metal");\n lib.linkFramework("MetalKit");/g' "$BUILD_ZIG"
|
||||
perl -0pi -e 's/module\.linkFramework\("IOSurface", \.\{\}\);/module.linkFramework("IOSurface", .{});\n module.linkFramework("Metal", .{});\n module.linkFramework("MetalKit", .{});/g' "$BUILD_ZIG"
|
||||
echo "[+] patched: linked Metal frameworks"
|
||||
else
|
||||
echo "[+] Metal frameworks already linked"
|
||||
fi
|
||||
fi
|
||||
|
||||
# Patch 4: Lower iOS deployment target to 15.0
|
||||
CONFIG_ZIG="${SOURCE_DIR}/src/build/Config.zig"
|
||||
if [ -f "$CONFIG_ZIG" ]; then
|
||||
if grep -q '\.ios => \.{ \.semver = \.{' "$CONFIG_ZIG"; then
|
||||
perl -0pi -e 's/\.ios => \.{ \.semver = \.{\n\s*\.major = \d+,\n\s*\.minor = \d+,\n\s*\.patch = \d+,/.ios => .{ .semver = .{\n .major = 15,\n .minor = 0,\n .patch = 0,/s' "$CONFIG_ZIG"
|
||||
echo "[+] patched: iOS deployment target -> 15.0"
|
||||
else
|
||||
echo "[+] iOS deployment target already patched"
|
||||
fi
|
||||
fi
|
||||
|
||||
echo "[+] all ios-fixes patches applied"
|
||||
473
vendor/libghostty-spm/Patches/ghostty/0005-ios-metal-rendering.sh
vendored
Executable file
473
vendor/libghostty-spm/Patches/ghostty/0005-ios-metal-rendering.sh
vendored
Executable file
@@ -0,0 +1,473 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
SOURCE_DIR="${1:?Usage: $0 <ghostty-source-dir>}"
|
||||
|
||||
# =============================================================================
|
||||
# Patch 1: IOSurfaceLayer — iOS rendering compatibility
|
||||
#
|
||||
# Problem: On iOS, IOSurface dimensions may differ by ±1 pixel from the
|
||||
# CALayer bounds due to rounding differences between UIKit point-to-pixel
|
||||
# conversion and Metal's integer pixel sizes. The upstream code does an
|
||||
# exact match and silently discards the surface, causing a blank screen.
|
||||
#
|
||||
# Additionally, iOS doesn't have a native CALayer subclass optimized for
|
||||
# IOSurface content display. Using the private CAIOSurfaceLayer class
|
||||
# (available since iOS 11) provides hardware-accelerated compositing.
|
||||
#
|
||||
# Fix:
|
||||
# - Allow ±1px tolerance on iOS when comparing surface vs layer dimensions
|
||||
# - Dynamically adjust contentsScale when dimensions don't match exactly
|
||||
# - Use CAIOSurfaceLayer as base class on iOS for native IOSurface compositing
|
||||
# - Mark layer as opaque since terminal content fills the entire bounds
|
||||
# =============================================================================
|
||||
IOSURFACE_LAYER="${SOURCE_DIR}/src/renderer/metal/IOSurfaceLayer.zig"
|
||||
if [ -f "$IOSURFACE_LAYER" ]; then
|
||||
if grep -q 'const log = std.log.scoped(.IOSurfaceLayer);' "$IOSURFACE_LAYER"; then
|
||||
python3 - "$IOSURFACE_LAYER" <<'PY'
|
||||
from pathlib import Path
|
||||
import sys
|
||||
|
||||
path = Path(sys.argv[1])
|
||||
src = path.read_text()
|
||||
|
||||
# Need builtin for comptime os.tag checks
|
||||
src = src.replace(
|
||||
'const std = @import("std");\nconst Allocator = std.mem.Allocator;',
|
||||
'const std = @import("std");\nconst builtin = @import("builtin");\nconst Allocator = std.mem.Allocator;'
|
||||
)
|
||||
|
||||
# The scoped log is only used in the size check we're replacing; drop it
|
||||
src = src.replace('\nconst log = std.log.scoped(.IOSurfaceLayer);\n', '\n')
|
||||
|
||||
# Terminal surface is always fully opaque — tell the compositor
|
||||
src = src.replace(
|
||||
'layer.setProperty("contentsGravity", macos.animation.kCAGravityTopLeft);\n\n layer.setInstanceVariable',
|
||||
'layer.setProperty("contentsGravity", macos.animation.kCAGravityTopLeft);\n layer.setProperty("opaque", true);\n\n layer.setInstanceVariable'
|
||||
)
|
||||
|
||||
# Replace the strict size equality check with a platform-aware version.
|
||||
# On iOS, UIKit's point→pixel rounding can produce a 1px discrepancy.
|
||||
# Rather than dropping the frame entirely (→ blank screen), we accept it
|
||||
# and recalculate contentsScale so CoreAnimation stretches correctly.
|
||||
old_block = """ if (width != surface.getWidth() or height != surface.getHeight()) {
|
||||
log.debug(
|
||||
"setSurfaceCallback(): surface is wrong size for layer, discarding. surface = {d}x{d}, layer = {d}x{d}",
|
||||
.{ surface.getWidth(), surface.getHeight(), width, height },
|
||||
);
|
||||
return;
|
||||
}"""
|
||||
|
||||
new_block = """ const sw = surface.getWidth();
|
||||
const sh = surface.getHeight();
|
||||
const dw: usize = if (width > sw) width - sw else sw - width;
|
||||
const dh: usize = if (height > sh) height - sh else sh - height;
|
||||
// iOS UIKit rounding can produce ±1px discrepancy; macOS must match exactly
|
||||
const max_drift: usize = if (comptime builtin.os.tag == .ios) 1 else 0;
|
||||
if (dw > max_drift or dh > max_drift) {
|
||||
if (comptime builtin.os.tag == .ios) {
|
||||
// Recalculate contentsScale so CA maps surface pixels to layer points
|
||||
const pw = bounds.size.width;
|
||||
const ph = bounds.size.height;
|
||||
if (pw > 0 and ph > 0) {
|
||||
const cs_x: f64 = @as(f64, @floatFromInt(sw)) / pw;
|
||||
const cs_y: f64 = @as(f64, @floatFromInt(sh)) / ph;
|
||||
const cs: f64 = @max(cs_x, cs_y);
|
||||
if (@abs(cs - scale) > 0.01) {
|
||||
layer.setProperty("contentsScale", cs);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
}"""
|
||||
|
||||
if old_block not in src:
|
||||
print("[!] IOSurfaceLayer size check block not found — source may have changed")
|
||||
sys.exit(1)
|
||||
src = src.replace(old_block, new_block)
|
||||
|
||||
# Use the system-provided CAIOSurfaceLayer on iOS; it handles
|
||||
# IOSurface display natively with zero-copy compositing.
|
||||
old_cls = """ const CALayer =
|
||||
objc.getClass("CALayer") orelse return error.ObjCFailed;
|
||||
|
||||
var subclass =
|
||||
objc.allocateClassPair(CALayer, "IOSurfaceLayer") orelse return error.ObjCFailed;"""
|
||||
|
||||
new_cls = """ const parent_cls = if (comptime builtin.os.tag == .ios)
|
||||
// CAIOSurfaceLayer provides native zero-copy IOSurface compositing
|
||||
objc.getClass("CAIOSurfaceLayer") orelse
|
||||
objc.getClass("CALayer") orelse return error.ObjCFailed
|
||||
else
|
||||
objc.getClass("CALayer") orelse return error.ObjCFailed;
|
||||
|
||||
var subclass =
|
||||
objc.allocateClassPair(parent_cls, "IOSurfaceLayer") orelse return error.ObjCFailed;"""
|
||||
|
||||
src = src.replace(old_cls, new_cls)
|
||||
|
||||
path.write_text(src)
|
||||
print("[+] patched IOSurfaceLayer: iOS size tolerance + CAIOSurfaceLayer")
|
||||
PY
|
||||
else
|
||||
echo "[+] IOSurfaceLayer already patched"
|
||||
fi
|
||||
fi
|
||||
|
||||
# =============================================================================
|
||||
# Patch 2: Metal.zig — iOS first-frame display + synchronous present
|
||||
#
|
||||
# Problem 1: On iOS the IOSurfaceLayer is added as a sublayer of the UIView's
|
||||
# backing layer. By the time the renderer registers its display callback the
|
||||
# sublayer already has its bounds set, so no "display" message is generated.
|
||||
# The first frame never renders.
|
||||
#
|
||||
# Problem 2: The async present path dispatches to the main thread via GCD.
|
||||
# On iOS the render loop already runs on the main thread, so the async
|
||||
# dispatch adds an unnecessary runloop turn of latency and can cause ordering
|
||||
# issues with UIKit layout.
|
||||
#
|
||||
# Fix:
|
||||
# - Call setNeedsDisplay after registering the display callback on iOS
|
||||
# - On iOS, always use the synchronous present path (setSurface checks
|
||||
# isMainThread internally and runs inline when true)
|
||||
# =============================================================================
|
||||
METAL_ZIG="${SOURCE_DIR}/src/renderer/Metal.zig"
|
||||
if [ -f "$METAL_ZIG" ]; then
|
||||
if ! grep -q 'setNeedsDisplay' "$METAL_ZIG"; then
|
||||
python3 - "$METAL_ZIG" <<'PY'
|
||||
from pathlib import Path
|
||||
import sys
|
||||
|
||||
path = Path(sys.argv[1])
|
||||
src = path.read_text()
|
||||
|
||||
# Kick the first display cycle after callback registration on iOS
|
||||
old_cb = """ @ptrCast(&displayCallback),
|
||||
@ptrCast(renderer),
|
||||
);
|
||||
}"""
|
||||
|
||||
new_cb = """ @ptrCast(&displayCallback),
|
||||
@ptrCast(renderer),
|
||||
);
|
||||
|
||||
// iOS: sublayer bounds are already set before the callback is wired up,
|
||||
// so no display message fires automatically. Kick the first frame.
|
||||
if (comptime builtin.os.tag == .ios) {
|
||||
self.layer.layer.msgSend(void, objc.sel("setNeedsDisplay"), .{});
|
||||
}
|
||||
}"""
|
||||
|
||||
if old_cb not in src:
|
||||
print("[!] Metal loopEnter callback not found")
|
||||
sys.exit(1)
|
||||
src = src.replace(old_cb, new_cb)
|
||||
|
||||
# iOS render loop is main-thread; skip the async dispatch path entirely.
|
||||
old_present = """pub inline fn present(self: *Metal, target: Target, sync: bool) !void {
|
||||
if (sync) {
|
||||
self.layer.setSurfaceSync(target.surface);
|
||||
} else {
|
||||
try self.layer.setSurface(target.surface);
|
||||
}
|
||||
}"""
|
||||
|
||||
new_present = """pub inline fn present(self: *Metal, target: Target, sync: bool) !void {
|
||||
// iOS: always present synchronously — the render loop already runs on
|
||||
// the main thread, so the async GCD hop is unnecessary overhead.
|
||||
if (comptime builtin.os.tag == .ios) {
|
||||
try self.layer.setSurface(target.surface);
|
||||
return;
|
||||
}
|
||||
if (sync) {
|
||||
self.layer.setSurfaceSync(target.surface);
|
||||
} else {
|
||||
try self.layer.setSurface(target.surface);
|
||||
}
|
||||
}"""
|
||||
|
||||
if old_present not in src:
|
||||
print("[!] Metal present function not found")
|
||||
sys.exit(1)
|
||||
src = src.replace(old_present, new_present)
|
||||
|
||||
path.write_text(src)
|
||||
print("[+] patched Metal.zig: iOS first-frame trigger + sync present")
|
||||
PY
|
||||
else
|
||||
echo "[+] Metal.zig already patched"
|
||||
fi
|
||||
fi
|
||||
|
||||
# =============================================================================
|
||||
# Patch 3: coretext.zig — Skip CF release thread on iOS
|
||||
#
|
||||
# Problem: The CoreText font shaper spawns a background thread that uses
|
||||
# libxev's kqueue event loop to asynchronously release CoreFoundation
|
||||
# objects. On iOS, kqueue's Mach port allocation fails (sandbox restrictions
|
||||
# + simulator incompatibility), crashing the thread and potentially stalling
|
||||
# font shaping operations.
|
||||
#
|
||||
# Fix: Make the CF release thread optional. On iOS, skip thread creation
|
||||
# entirely and release CF objects synchronously in endFrame(). This is
|
||||
# acceptable because iOS devices have fast enough CF release performance
|
||||
# and the terminal doesn't produce the same volume of shaped text as a
|
||||
# desktop compositor.
|
||||
# =============================================================================
|
||||
CORETEXT="${SOURCE_DIR}/src/font/shaper/coretext.zig"
|
||||
if [ -f "$CORETEXT" ]; then
|
||||
if grep -q 'cf_release_thread: \*CFReleaseThread,' "$CORETEXT"; then
|
||||
python3 - "$CORETEXT" <<'PY'
|
||||
from pathlib import Path
|
||||
import sys
|
||||
|
||||
path = Path(sys.argv[1])
|
||||
src = path.read_text()
|
||||
|
||||
# Make the struct fields optional so nil can represent "no thread"
|
||||
src = src.replace(
|
||||
'cf_release_thread: *CFReleaseThread,\n cf_release_thr: std.Thread,',
|
||||
'cf_release_thread: ?*CFReleaseThread,\n cf_release_thr: ?std.Thread,'
|
||||
)
|
||||
|
||||
# Guard thread creation behind a comptime platform check
|
||||
old_create = """ // Create the CF release thread.
|
||||
var cf_release_thread = try alloc.create(CFReleaseThread);
|
||||
errdefer alloc.destroy(cf_release_thread);
|
||||
cf_release_thread.* = try .init(alloc);
|
||||
errdefer cf_release_thread.deinit();
|
||||
|
||||
// Start the CF release thread.
|
||||
var cf_release_thr = try std.Thread.spawn(
|
||||
.{},
|
||||
CFReleaseThread.threadMain,
|
||||
.{cf_release_thread},
|
||||
);
|
||||
cf_release_thr.setName("cf_release") catch {};
|
||||
|
||||
return .{"""
|
||||
|
||||
new_create = """ // On iOS the kqueue-based event loop used by the release thread
|
||||
// crashes due to Mach port sandbox restrictions. Skip it entirely
|
||||
// and fall through to synchronous release in endFrame.
|
||||
var cf_release_thread: ?*CFReleaseThread = null;
|
||||
var cf_release_thr: ?std.Thread = null;
|
||||
if (comptime builtin.os.tag != .ios) {
|
||||
const thr_obj = try alloc.create(CFReleaseThread);
|
||||
errdefer alloc.destroy(thr_obj);
|
||||
thr_obj.* = try .init(alloc);
|
||||
errdefer thr_obj.deinit();
|
||||
const thr = try std.Thread.spawn(.{}, CFReleaseThread.threadMain, .{thr_obj});
|
||||
thr.setName("cf_release") catch {};
|
||||
cf_release_thread = thr_obj;
|
||||
cf_release_thr = thr;
|
||||
}
|
||||
|
||||
return .{"""
|
||||
|
||||
if old_create not in src:
|
||||
print("[!] coretext CF release thread creation block not found")
|
||||
sys.exit(1)
|
||||
src = src.replace(old_create, new_create)
|
||||
|
||||
# Deinit: only join/stop the thread if it was created
|
||||
old_deinit = """ // Stop the CF release thread
|
||||
{
|
||||
self.cf_release_thread.stop.notify() catch |err|
|
||||
log.err("error notifying cf release thread to stop, may stall err={}", .{err});
|
||||
self.cf_release_thr.join();
|
||||
}
|
||||
self.cf_release_thread.deinit();
|
||||
self.alloc.destroy(self.cf_release_thread);"""
|
||||
|
||||
new_deinit = """ // Stop the CF release thread (nil on iOS)
|
||||
if (self.cf_release_thread) |thr_obj| {
|
||||
thr_obj.stop.notify() catch |err|
|
||||
log.err("error notifying cf release thread to stop, may stall err={}", .{err});
|
||||
self.cf_release_thr.?.join();
|
||||
thr_obj.deinit();
|
||||
self.alloc.destroy(thr_obj);
|
||||
}"""
|
||||
|
||||
if old_deinit not in src:
|
||||
print("[!] coretext CF release thread deinit block not found")
|
||||
sys.exit(1)
|
||||
src = src.replace(old_deinit, new_deinit)
|
||||
|
||||
# endFrame: guard the mailbox push behind an optional check.
|
||||
# When nil (iOS), fall through to the synchronous release below.
|
||||
old_end = """ // Send the items. If the send succeeds then we wake up the
|
||||
// thread to process the items. If the send fails then do a manual
|
||||
// cleanup.
|
||||
if (self.cf_release_thread.mailbox.push(.{ .release = .{
|
||||
.refs = items,
|
||||
.alloc = self.alloc,
|
||||
} }, .{ .forever = {} }) != 0) {
|
||||
self.cf_release_thread.wakeup.notify() catch |err| {
|
||||
log.warn(
|
||||
"error notifying cf release thread to wake up, may stall err={}",
|
||||
.{err},
|
||||
);
|
||||
};
|
||||
return;
|
||||
}
|
||||
|
||||
for (items) |ref| macos.foundation.CFRelease(ref);"""
|
||||
|
||||
new_end = """ // Offload to the background release thread when available.
|
||||
// On iOS cf_release_thread is nil, so we fall through to sync release.
|
||||
if (self.cf_release_thread) |thr_obj| {
|
||||
if (thr_obj.mailbox.push(.{ .release = .{
|
||||
.refs = items,
|
||||
.alloc = self.alloc,
|
||||
} }, .{ .forever = {} }) != 0) {
|
||||
thr_obj.wakeup.notify() catch |err| {
|
||||
log.warn(
|
||||
"error notifying cf release thread to wake up, may stall err={}",
|
||||
.{err},
|
||||
);
|
||||
};
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
for (items) |ref| macos.foundation.CFRelease(ref);"""
|
||||
|
||||
if old_end not in src:
|
||||
print("[!] coretext endFrame mailbox block not found")
|
||||
sys.exit(1)
|
||||
src = src.replace(old_end, new_end)
|
||||
|
||||
path.write_text(src)
|
||||
print("[+] patched coretext.zig: CF release thread disabled on iOS")
|
||||
PY
|
||||
else
|
||||
echo "[+] coretext.zig already patched"
|
||||
fi
|
||||
fi
|
||||
|
||||
# =============================================================================
|
||||
# Patch 4: iosurface.zig — Explicit row byte alignment
|
||||
#
|
||||
# Problem: When creating an IOSurface without specifying bytesPerRow, the
|
||||
# system picks whatever alignment it wants. Metal textures created from
|
||||
# these surfaces may have mismatched row strides, causing corrupted or
|
||||
# shifted glyph rendering (especially visible on font atlas textures).
|
||||
#
|
||||
# Fix: Calculate 64-byte-aligned row bytes and pass kIOSurfaceBytesPerRow
|
||||
# when creating the IOSurface. Also suppress unused return value warnings
|
||||
# from IOSurfaceLock/Unlock.
|
||||
# =============================================================================
|
||||
IOSURFACE="${SOURCE_DIR}/pkg/macos/iosurface/iosurface.zig"
|
||||
if [ -f "$IOSURFACE" ]; then
|
||||
if ! grep -q 'kIOSurfaceBytesPerRow' "$IOSURFACE"; then
|
||||
python3 - "$IOSURFACE" <<'PY'
|
||||
from pathlib import Path
|
||||
import sys
|
||||
|
||||
path = Path(sys.argv[1])
|
||||
src = path.read_text()
|
||||
|
||||
# Compute aligned row stride before creating the Number objects
|
||||
old_start = """ pub fn init(properties: Properties) Allocator.Error!*IOSurface {
|
||||
var w = try foundation.Number.create(.int, &properties.width);"""
|
||||
|
||||
new_start = """ pub fn init(properties: Properties) Allocator.Error!*IOSurface {
|
||||
// Ensure row stride is 64-byte aligned for Metal texture compatibility.
|
||||
const aligned_stride: c_int = @intCast(
|
||||
(properties.width * properties.bytes_per_element + 63) & ~@as(c_int, 63),
|
||||
);
|
||||
var w = try foundation.Number.create(.int, &properties.width);"""
|
||||
|
||||
if old_start not in src:
|
||||
print("[!] iosurface init start not found")
|
||||
sys.exit(1)
|
||||
src = src.replace(old_start, new_start)
|
||||
|
||||
# Create a Number for the stride and include it in the dictionary
|
||||
old_dict_setup = """ var bpe = try foundation.Number.create(.int, &properties.bytes_per_element);
|
||||
defer bpe.release();
|
||||
|
||||
var properties_dict = try foundation.Dictionary.create("""
|
||||
|
||||
new_dict_setup = """ var bpe = try foundation.Number.create(.int, &properties.bytes_per_element);
|
||||
defer bpe.release();
|
||||
|
||||
var stride_num = try foundation.Number.create(.int, &aligned_stride);
|
||||
defer stride_num.release();
|
||||
|
||||
var properties_dict = try foundation.Dictionary.create("""
|
||||
|
||||
if old_dict_setup not in src:
|
||||
print("[!] iosurface bpe block not found")
|
||||
sys.exit(1)
|
||||
src = src.replace(old_dict_setup, new_dict_setup)
|
||||
|
||||
# Extend the dictionary keys/values arrays
|
||||
old_dict = """ &[_]?*const anyopaque{
|
||||
c.kIOSurfaceWidth,
|
||||
c.kIOSurfaceHeight,
|
||||
c.kIOSurfacePixelFormat,
|
||||
c.kIOSurfaceBytesPerElement,
|
||||
},
|
||||
&[_]?*const anyopaque{ w, h, pf, bpe },"""
|
||||
|
||||
new_dict = """ &[_]?*const anyopaque{
|
||||
c.kIOSurfaceWidth,
|
||||
c.kIOSurfaceHeight,
|
||||
c.kIOSurfacePixelFormat,
|
||||
c.kIOSurfaceBytesPerElement,
|
||||
c.kIOSurfaceBytesPerRow,
|
||||
},
|
||||
&[_]?*const anyopaque{ w, h, pf, bpe, stride_num },"""
|
||||
|
||||
if old_dict not in src:
|
||||
print("[!] iosurface dictionary keys not found")
|
||||
sys.exit(1)
|
||||
src = src.replace(old_dict, new_dict)
|
||||
|
||||
# Silence unused return value from IOSurfaceLock/Unlock
|
||||
src = src.replace(
|
||||
' c.IOSurfaceLock(\n @ptrCast(self),\n 0,\n null,\n );',
|
||||
' _ = c.IOSurfaceLock(\n @ptrCast(self),\n 0,\n null,\n );'
|
||||
)
|
||||
src = src.replace(
|
||||
' c.IOSurfaceUnlock(\n @ptrCast(self),\n 0,\n null,\n );',
|
||||
' _ = c.IOSurfaceUnlock(\n @ptrCast(self),\n 0,\n null,\n );'
|
||||
)
|
||||
|
||||
path.write_text(src)
|
||||
print("[+] patched iosurface.zig: 64-byte stride alignment for Metal")
|
||||
PY
|
||||
else
|
||||
echo "[+] iosurface.zig already patched"
|
||||
fi
|
||||
fi
|
||||
|
||||
# =============================================================================
|
||||
# Patch 5: build.zig.zon — Update libxev to fix iOS kqueue mach port panic
|
||||
#
|
||||
# Problem: The bundled libxev uses mach ports for async wakeup on Darwin.
|
||||
# Its kqueue backend checks `os.tag != .macos` and returns null for mach port
|
||||
# kevents on non-macOS Darwin (iOS). The caller then unwraps null with `.?`
|
||||
# causing a panic. A newer libxev version fixes this by properly supporting
|
||||
# iOS as a Darwin target.
|
||||
#
|
||||
# Fix: Update the libxev dependency URL and hash to a version that handles
|
||||
# iOS mach ports correctly.
|
||||
# =============================================================================
|
||||
BUILD_ZON="${SOURCE_DIR}/build.zig.zon"
|
||||
if [ -f "$BUILD_ZON" ]; then
|
||||
if ! grep -q '7e7d2f2ab4700544657f8ec268715c8ef320d839' "$BUILD_ZON"; then
|
||||
sed -i '' 's|"https://deps.files.ghostty.org/libxev-34fa50878aec6e5fa8f532867001ab3c36fae23e.tar.gz"|"https://github.com/mitchellh/libxev/archive/7e7d2f2ab4700544657f8ec268715c8ef320d839.tar.gz"|' "$BUILD_ZON"
|
||||
sed -i '' 's|"libxev-0.0.0-86vtc4IcEwCqEYxEYoN_3KXmc6A9VLcm22aVImfvecYs"|"libxev-0.0.0-86vtcwE9EwB942iWRnaNMXHv3n0BeLAs_tVhrs5cT8cQ"|' "$BUILD_ZON"
|
||||
echo "[+] patched build.zig.zon: updated libxev for iOS mach port fix"
|
||||
else
|
||||
echo "[+] libxev already updated"
|
||||
fi
|
||||
fi
|
||||
|
||||
echo "[+] all ios metal rendering patches applied"
|
||||
197
vendor/libghostty-spm/Patches/ghostty/0006-disable-custom-shaders.sh
vendored
Executable file
197
vendor/libghostty-spm/Patches/ghostty/0006-disable-custom-shaders.sh
vendored
Executable file
@@ -0,0 +1,197 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
SOURCE_DIR="${1:?Usage: $0 <ghostty-source-dir>}"
|
||||
|
||||
MARKER="LIBGHOSTTY_SPM_TRIM_PATCH"
|
||||
|
||||
# Skip if already applied
|
||||
if grep -q "$MARKER" "$SOURCE_DIR/src/build/Config.zig" 2>/dev/null; then
|
||||
echo "[+] trim patch already applied"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
python3 - "$SOURCE_DIR" "$MARKER" <<'PYEOF'
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
source_dir = Path(sys.argv[1])
|
||||
marker = sys.argv[2]
|
||||
|
||||
# ──────────────────────────────────────────────────────────────────────
|
||||
# 1. Config.zig — add custom_shaders feature flag
|
||||
# ──────────────────────────────────────────────────────────────────────
|
||||
config_path = source_dir / "src/build/Config.zig"
|
||||
text = config_path.read_text()
|
||||
|
||||
# Add field
|
||||
text = text.replace(
|
||||
"sentry: bool = true,",
|
||||
f"sentry: bool = true,\ncustom_shaders: bool = true, // {marker}",
|
||||
)
|
||||
|
||||
# Add option parsing after sentry block
|
||||
sentry_end = """ ) orelse sentry: {
|
||||
switch (target.result.os.tag) {
|
||||
.macos, .ios => break :sentry true,
|
||||
|
||||
// Note its false for linux because the crash reports on Linux
|
||||
// don't have much useful information.
|
||||
else => break :sentry false,
|
||||
}
|
||||
};"""
|
||||
|
||||
new_options = sentry_end + """
|
||||
|
||||
config.custom_shaders = b.option(
|
||||
bool,
|
||||
"custom-shaders",
|
||||
"Build with custom shader (glslang/spirv-cross) support.",
|
||||
) orelse true;"""
|
||||
|
||||
text = text.replace(sentry_end, new_options)
|
||||
|
||||
# Add to addOptions
|
||||
text = text.replace(
|
||||
'step.addOption(bool, "sentry", self.sentry);',
|
||||
'step.addOption(bool, "sentry", self.sentry);\n'
|
||||
' step.addOption(bool, "custom_shaders", self.custom_shaders);',
|
||||
)
|
||||
|
||||
config_path.write_text(text)
|
||||
print("[+] patched Config.zig")
|
||||
|
||||
# ──────────────────────────────────────────────────────────────────────
|
||||
# 2. SharedDeps.zig — gate glslang + spirv-cross on custom_shaders
|
||||
# ──────────────────────────────────────────────────────────────────────
|
||||
shared_path = source_dir / "src/build/SharedDeps.zig"
|
||||
text = shared_path.read_text()
|
||||
|
||||
# Gate glslang — wrap with custom_shaders check
|
||||
text = text.replace(
|
||||
' // Glslang\n if (b.lazyDependency("glslang", .{',
|
||||
' // Glslang — only needed for custom shaders\n if (self.config.custom_shaders) if (b.lazyDependency("glslang", .{',
|
||||
)
|
||||
# Close the extra if at end of glslang block
|
||||
text = text.replace(
|
||||
""" step.linkLibrary(glslang_dep.artifact("glslang"));
|
||||
try static_libs.append(
|
||||
b.allocator,
|
||||
glslang_dep.artifact("glslang").getEmittedBin(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Spirv-cross""",
|
||||
""" step.linkLibrary(glslang_dep.artifact("glslang"));
|
||||
try static_libs.append(
|
||||
b.allocator,
|
||||
glslang_dep.artifact("glslang").getEmittedBin(),
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
// Spirv-cross""",
|
||||
)
|
||||
|
||||
# Gate spirv-cross — wrap with custom_shaders check
|
||||
text = text.replace(
|
||||
' // Spirv-cross\n if (b.lazyDependency("spirv_cross", .{',
|
||||
' // Spirv-cross — only needed for custom shaders\n if (self.config.custom_shaders) if (b.lazyDependency("spirv_cross", .{',
|
||||
)
|
||||
text = text.replace(
|
||||
""" step.linkLibrary(spirv_cross_dep.artifact("spirv_cross"));
|
||||
try static_libs.append(
|
||||
b.allocator,
|
||||
spirv_cross_dep.artifact("spirv_cross").getEmittedBin(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Sentry""",
|
||||
""" step.linkLibrary(spirv_cross_dep.artifact("spirv_cross"));
|
||||
try static_libs.append(
|
||||
b.allocator,
|
||||
spirv_cross_dep.artifact("spirv_cross").getEmittedBin(),
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
// Sentry""",
|
||||
)
|
||||
|
||||
shared_path.write_text(text)
|
||||
print("[+] patched SharedDeps.zig")
|
||||
|
||||
# ──────────────────────────────────────────────────────────────────────
|
||||
# 3. build_config.zig — re-export custom_shaders flag
|
||||
# ──────────────────────────────────────────────────────────────────────
|
||||
bc_path = source_dir / "src/build_config.zig"
|
||||
text = bc_path.read_text()
|
||||
|
||||
if "custom_shaders" not in text:
|
||||
text = text.replace(
|
||||
'const options = @import("build_options");',
|
||||
'const options = @import("build_options");\npub const custom_shaders = options.custom_shaders;',
|
||||
)
|
||||
bc_path.write_text(text)
|
||||
print("[+] patched build_config.zig")
|
||||
|
||||
# ──────────────────────────────────────────────────────────────────────
|
||||
# 4. global.zig — conditional glslang init
|
||||
# (global.zig already imports build_config)
|
||||
# ──────────────────────────────────────────────────────────────────────
|
||||
global_path = source_dir / "src/global.zig"
|
||||
text = global_path.read_text()
|
||||
|
||||
text = text.replace(
|
||||
'const glslang = @import("glslang");',
|
||||
'const glslang = if (build_config.custom_shaders) @import("glslang") else struct {\n'
|
||||
' pub fn init() !void {}\n'
|
||||
'};',
|
||||
)
|
||||
|
||||
global_path.write_text(text)
|
||||
print("[+] patched global.zig")
|
||||
|
||||
# ──────────────────────────────────────────────────────────────────────
|
||||
# 5. renderer/shadertoy.zig — gate shader imports and loadFromFile
|
||||
# When custom_shaders is disabled, loadFromFile is unreachable
|
||||
# so glslang/spirv_cross are never semantically analyzed
|
||||
# ──────────────────────────────────────────────────────────────────────
|
||||
shader_path = source_dir / "src/renderer/shadertoy.zig"
|
||||
text = shader_path.read_text()
|
||||
|
||||
# Make imports conditional — these won't be analyzed if never reached
|
||||
text = text.replace(
|
||||
'const glslang = @import("glslang");',
|
||||
'const build_config = @import("../build_config.zig");\n'
|
||||
'const glslang = @import("glslang");',
|
||||
)
|
||||
|
||||
# Add early return in loadFromFiles when custom_shaders is disabled
|
||||
# This prevents loadFromFile (and thus spirvFromGlsl etc) from being analyzed
|
||||
text = text.replace(
|
||||
"""pub fn loadFromFiles(
|
||||
alloc_gpa: Allocator,
|
||||
paths: configpkg.RepeatablePath,
|
||||
target: Target,
|
||||
) ![]const [:0]const u8 {
|
||||
var list: std.ArrayList([:0]const u8) = .empty;""",
|
||||
"""pub fn loadFromFiles(
|
||||
alloc_gpa: Allocator,
|
||||
paths: configpkg.RepeatablePath,
|
||||
target: Target,
|
||||
) ![]const [:0]const u8 {
|
||||
if (comptime !build_config.custom_shaders) return &.{};
|
||||
var list: std.ArrayList([:0]const u8) = .empty;""",
|
||||
)
|
||||
|
||||
shader_path.write_text(text)
|
||||
print("[+] patched renderer/shadertoy.zig")
|
||||
|
||||
print(f"[+] all trim patches complete ({marker})")
|
||||
PYEOF
|
||||
|
||||
echo "[+] trim patch applied"
|
||||
408
vendor/libghostty-spm/Patches/ghostty/0007-disable-inspector.sh
vendored
Executable file
408
vendor/libghostty-spm/Patches/ghostty/0007-disable-inspector.sh
vendored
Executable file
@@ -0,0 +1,408 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
SOURCE_DIR="${1:?Usage: $0 <ghostty-source-dir>}"
|
||||
|
||||
MARKER="LIBGHOSTTY_SPM_INSPECTOR_DISABLE"
|
||||
|
||||
if grep -q "$MARKER" "$SOURCE_DIR/src/build/Config.zig" 2>/dev/null; then
|
||||
echo "[+] inspector disable patch already applied"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
python3 - "$SOURCE_DIR" "$MARKER" <<'PYEOF'
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
source_dir = Path(sys.argv[1])
|
||||
marker = sys.argv[2]
|
||||
|
||||
def patch_file(rel_path, replacements):
|
||||
path = source_dir / rel_path
|
||||
text = path.read_text()
|
||||
for old, new in replacements:
|
||||
if old not in text:
|
||||
print(f"[-] pattern not found in {rel_path}:")
|
||||
print(f" {old[:80]}...")
|
||||
sys.exit(1)
|
||||
count = text.count(old)
|
||||
if count > 1:
|
||||
print(f"[-] pattern matched {count} times in {rel_path} (expected 1):")
|
||||
print(f" {old[:80]}...")
|
||||
sys.exit(1)
|
||||
text = text.replace(old, new, 1)
|
||||
path.write_text(text)
|
||||
print(f"[+] patched {rel_path}")
|
||||
|
||||
# ──────────────────────────────────────────────────────────────────────
|
||||
# 1. Config.zig — add inspector feature flag
|
||||
# (runs AFTER 0006, so custom_shaders line already exists)
|
||||
# ──────────────────────────────────────────────────────────────────────
|
||||
patch_file("src/build/Config.zig", [
|
||||
# Add field after custom_shaders (added by 0006)
|
||||
(
|
||||
f"custom_shaders: bool = true, // LIBGHOSTTY_SPM_TRIM_PATCH",
|
||||
f"custom_shaders: bool = true, // LIBGHOSTTY_SPM_TRIM_PATCH\n"
|
||||
f"inspector: bool = true, // {marker}",
|
||||
),
|
||||
# Add option parsing after the custom_shaders option block (added by 0006)
|
||||
(
|
||||
' "custom-shaders",\n'
|
||||
' "Build with custom shader (glslang/spirv-cross) support.",\n'
|
||||
' ) orelse true;',
|
||||
' "custom-shaders",\n'
|
||||
' "Build with custom shader (glslang/spirv-cross) support.",\n'
|
||||
' ) orelse true;\n'
|
||||
'\n'
|
||||
' config.inspector = b.option(\n'
|
||||
' bool,\n'
|
||||
' "inspector",\n'
|
||||
' "Build with terminal inspector (dcimgui) support.",\n'
|
||||
' ) orelse true;',
|
||||
),
|
||||
# Add to addOptions after custom_shaders (added by 0006)
|
||||
(
|
||||
' step.addOption(bool, "custom_shaders", self.custom_shaders);',
|
||||
' step.addOption(bool, "custom_shaders", self.custom_shaders);\n'
|
||||
' step.addOption(bool, "inspector", self.inspector);',
|
||||
),
|
||||
])
|
||||
|
||||
# ──────────────────────────────────────────────────────────────────────
|
||||
# 2. SharedDeps.zig — gate dcimgui linking on inspector flag
|
||||
# ──────────────────────────────────────────────────────────────────────
|
||||
patch_file("src/build/SharedDeps.zig", [
|
||||
(
|
||||
' // cimgui\n'
|
||||
' if (b.lazyDependency("dcimgui", .{',
|
||||
' // cimgui — only needed for inspector\n'
|
||||
' if (self.config.inspector) if (b.lazyDependency("dcimgui", .{',
|
||||
),
|
||||
# Close the extra if — find the end of the dcimgui block
|
||||
(
|
||||
' );\n'
|
||||
' }\n'
|
||||
'\n'
|
||||
' // Fonts',
|
||||
' );\n'
|
||||
' };\n'
|
||||
'\n'
|
||||
' // Fonts',
|
||||
),
|
||||
])
|
||||
|
||||
# ──────────────────────────────────────────────────────────────────────
|
||||
# 3. build_config.zig — re-export inspector flag
|
||||
# (runs AFTER 0006, so custom_shaders line already exists)
|
||||
# ──────────────────────────────────────────────────────────────────────
|
||||
patch_file("src/build_config.zig", [
|
||||
(
|
||||
'pub const custom_shaders = options.custom_shaders;',
|
||||
'pub const custom_shaders = options.custom_shaders;\n'
|
||||
'pub const inspector = options.inspector;',
|
||||
),
|
||||
])
|
||||
|
||||
# ──────────────────────────────────────────────────────────────────────
|
||||
# 4. inspector/main.zig — COMPLETE FILE REPLACEMENT with stub module
|
||||
#
|
||||
# This is the key stability improvement. Instead of pattern-matching
|
||||
# downstream files, we replace the single chokepoint module with
|
||||
# stubs that satisfy all downstream type/method requirements.
|
||||
# When inspector=true, everything re-exports as before.
|
||||
# When inspector=false, stub types with no-op methods are provided
|
||||
# so Surface.zig, renderer, termio all compile without modification.
|
||||
# ──────────────────────────────────────────────────────────────────────
|
||||
inspector_main = source_dir / "src/inspector/main.zig"
|
||||
inspector_main.write_text('''\
|
||||
const build_config = @import("../build_config.zig");
|
||||
const std = @import("std");
|
||||
const terminal = @import("../terminal/main.zig");
|
||||
const input = @import("../input.zig");
|
||||
const renderer = @import("../renderer.zig");
|
||||
|
||||
pub const widgets = if (build_config.inspector) @import("widgets.zig") else struct {
|
||||
pub const key = struct {
|
||||
pub const Event = StubKeyEvent;
|
||||
};
|
||||
pub const renderer = struct {
|
||||
pub const Info = StubRendererInfo;
|
||||
};
|
||||
pub const surface = struct {
|
||||
pub const Mouse = StubMouse;
|
||||
};
|
||||
};
|
||||
|
||||
pub const Inspector = if (build_config.inspector) @import("Inspector.zig") else StubInspector;
|
||||
pub const KeyEvent = widgets.key.Event;
|
||||
|
||||
/// Stub types — these satisfy all downstream method/field accesses
|
||||
/// when inspector is disabled, so no other files need patching.
|
||||
|
||||
const StubMouse = struct {
|
||||
last_xpos: f64 = 0,
|
||||
last_ypos: f64 = 0,
|
||||
last_point: ?terminal.Pin = null,
|
||||
};
|
||||
|
||||
const StubRendererInfo = struct {
|
||||
pub const empty: @This() = .{};
|
||||
|
||||
pub fn deinit(_: *@This(), _: std.mem.Allocator) void {}
|
||||
|
||||
pub fn overlayFeatures(
|
||||
_: *const @This(),
|
||||
_: std.mem.Allocator,
|
||||
) std.mem.Allocator.Error![]renderer.Overlay.Feature {
|
||||
return &.{};
|
||||
}
|
||||
};
|
||||
|
||||
const StubKeyEvent = struct {
|
||||
event: input.KeyEvent = undefined,
|
||||
binding: []const input.Binding.Action = &.{},
|
||||
pty: []const u8 = "",
|
||||
|
||||
pub fn deinit(_: *const @This(), _: std.mem.Allocator) void {}
|
||||
};
|
||||
|
||||
const StubInspector = struct {
|
||||
mouse: StubMouse = .{},
|
||||
|
||||
pub fn setup() void {}
|
||||
pub fn init(_: std.mem.Allocator) !@This() { return .{}; }
|
||||
pub fn deinit(_: *@This(), _: std.mem.Allocator) void {}
|
||||
pub fn render(_: *@This(), _: anytype) void {}
|
||||
|
||||
pub fn rendererInfo(_: *@This()) *StubRendererInfo {
|
||||
return &stub_renderer_info;
|
||||
}
|
||||
|
||||
pub fn recordKeyEvent(
|
||||
_: *@This(),
|
||||
_: std.mem.Allocator,
|
||||
_: StubKeyEvent,
|
||||
) std.mem.Allocator.Error!void {}
|
||||
|
||||
pub fn recordPtyRead(
|
||||
_: *@This(),
|
||||
_: std.mem.Allocator,
|
||||
_: *terminal.Terminal,
|
||||
_: []const u8,
|
||||
) !void {}
|
||||
|
||||
var stub_renderer_info: StubRendererInfo = .{};
|
||||
};
|
||||
|
||||
test {
|
||||
if (build_config.inspector) {
|
||||
@import("std").testing.refAllDecls(@This());
|
||||
}
|
||||
}
|
||||
''')
|
||||
print("[+] replaced src/inspector/main.zig with stub module")
|
||||
|
||||
# ──────────────────────────────────────────────────────────────────────
|
||||
# 5. input/key.zig — gate dcimgui import + imguiKey method
|
||||
# ──────────────────────────────────────────────────────────────────────
|
||||
patch_file("src/input/key.zig", [
|
||||
(
|
||||
'const cimgui = @import("dcimgui");',
|
||||
'const _build_config = @import("../build_config.zig");\n'
|
||||
'const cimgui = if (_build_config.inspector) @import("dcimgui") else struct {};',
|
||||
),
|
||||
(
|
||||
' pub fn imguiKey(self: Key) ?c_int {',
|
||||
' pub fn imguiKey(self: Key) ?c_int {\n'
|
||||
' if (comptime !_build_config.inspector) return null;',
|
||||
),
|
||||
])
|
||||
|
||||
# ──────────────────────────────────────────────────────────────────────
|
||||
# 6. apprt/embedded.zig — gate Inspector struct + CoreInspector import
|
||||
# ──────────────────────────────────────────────────────────────────────
|
||||
embedded_path = source_dir / "src/apprt/embedded.zig"
|
||||
text = embedded_path.read_text()
|
||||
|
||||
# 6a. Gate CoreInspector import
|
||||
old = 'const CoreInspector = @import("../inspector/main.zig").Inspector;'
|
||||
new = 'const CoreInspector = if (@import("../build_config.zig").inspector) @import("../inspector/main.zig").Inspector else struct {};'
|
||||
assert old in text, f"pattern not found: {old}"
|
||||
text = text.replace(old, new, 1)
|
||||
|
||||
# 6b. Gate the Inspector struct definition using brace-depth counting
|
||||
# Find the struct opening
|
||||
struct_marker = 'pub const Inspector = struct {\n const cimgui = @import("dcimgui");'
|
||||
assert struct_marker in text, f"Inspector struct marker not found"
|
||||
|
||||
struct_start_idx = text.index('pub const Inspector = struct {\n const cimgui = @import("dcimgui");')
|
||||
# Find the opening brace
|
||||
brace_start = text.index('{', struct_start_idx)
|
||||
# Count braces to find the matching close
|
||||
depth = 0
|
||||
i = brace_start
|
||||
while i < len(text):
|
||||
if text[i] == '{':
|
||||
depth += 1
|
||||
elif text[i] == '}':
|
||||
depth -= 1
|
||||
if depth == 0:
|
||||
break
|
||||
i += 1
|
||||
# i now points to the closing }
|
||||
# The struct ends with `};` — we want to capture up to and including `}`
|
||||
# but NOT the `;` since the semicolon will come after the else branch
|
||||
struct_end_brace = i + 1 # position after closing }
|
||||
# Check for trailing semicolon to know where to resume after replacement
|
||||
struct_end = struct_end_brace
|
||||
if struct_end < len(text) and text[struct_end] == ';':
|
||||
struct_end += 1 # skip the original ; in the replacement range
|
||||
|
||||
# Extract the original struct content (up to } but not ;)
|
||||
original_struct = text[struct_start_idx:struct_end_brace]
|
||||
|
||||
# Wrap: replace opening with comptime conditional
|
||||
new_struct = original_struct.replace(
|
||||
'pub const Inspector = struct {',
|
||||
'pub const Inspector = if (@import("../build_config.zig").inspector) struct {',
|
||||
1,
|
||||
)
|
||||
|
||||
# Add else branch after closing };
|
||||
stub = """ else struct {
|
||||
surface: *Surface = undefined,
|
||||
pub fn init(_: *Surface) !@This() { return .{}; }
|
||||
pub fn deinit(_: *@This()) void {}
|
||||
};"""
|
||||
new_struct = new_struct + stub
|
||||
|
||||
text = text[:struct_start_idx] + new_struct + text[struct_end:]
|
||||
|
||||
# 6c. Gate initInspector body
|
||||
old_init = """ pub fn initInspector(self: *Surface) !*Inspector {
|
||||
if (self.inspector) |v| return v;
|
||||
|
||||
const alloc = self.app.core_app.alloc;
|
||||
const inspector = try alloc.create(Inspector);
|
||||
errdefer alloc.destroy(inspector);
|
||||
inspector.* = try .init(self);
|
||||
self.inspector = inspector;
|
||||
return inspector;
|
||||
}"""
|
||||
new_init = """ pub fn initInspector(self: *Surface) !*Inspector {
|
||||
if (comptime !@import("../build_config.zig").inspector) return error.InspectorUnavailable;
|
||||
if (self.inspector) |v| return v;
|
||||
|
||||
const alloc = self.app.core_app.alloc;
|
||||
const inspector = try alloc.create(Inspector);
|
||||
errdefer alloc.destroy(inspector);
|
||||
inspector.* = try .init(self);
|
||||
self.inspector = inspector;
|
||||
return inspector;
|
||||
}"""
|
||||
assert old_init in text, "initInspector pattern not found"
|
||||
text = text.replace(old_init, new_init, 1)
|
||||
|
||||
# 6d. Gate freeInspector body
|
||||
old_free = """ pub fn freeInspector(self: *Surface) void {
|
||||
if (self.inspector) |v| {
|
||||
v.deinit();
|
||||
self.app.core_app.alloc.destroy(v);
|
||||
self.inspector = null;
|
||||
}
|
||||
}"""
|
||||
new_free = """ pub fn freeInspector(self: *Surface) void {
|
||||
if (comptime !@import("../build_config.zig").inspector) return;
|
||||
if (self.inspector) |v| {
|
||||
v.deinit();
|
||||
self.app.core_app.alloc.destroy(v);
|
||||
self.inspector = null;
|
||||
}
|
||||
}"""
|
||||
assert old_free in text, "freeInspector pattern not found"
|
||||
text = text.replace(old_free, new_free, 1)
|
||||
|
||||
# 6e. Gate CAPI inspector functions that call methods on Inspector
|
||||
# These functions take *Inspector and call methods that don't exist on the stub struct.
|
||||
# We add comptime early-returns so the method calls are never analyzed.
|
||||
capi_guards = [
|
||||
(' export fn ghostty_inspector_set_size(ptr: *Inspector, w: u32, h: u32) void {\n'
|
||||
' ptr.updateSize(w, h);',
|
||||
' export fn ghostty_inspector_set_size(ptr: *Inspector, w: u32, h: u32) void {\n'
|
||||
' if (comptime !@import("../build_config.zig").inspector) return;\n'
|
||||
' ptr.updateSize(w, h);'),
|
||||
(' export fn ghostty_inspector_set_content_scale(ptr: *Inspector, x: f64, y: f64) void {\n'
|
||||
' ptr.updateContentScale(x, y);',
|
||||
' export fn ghostty_inspector_set_content_scale(ptr: *Inspector, x: f64, y: f64) void {\n'
|
||||
' if (comptime !@import("../build_config.zig").inspector) return;\n'
|
||||
' ptr.updateContentScale(x, y);'),
|
||||
(' ptr.mouseButtonCallback(\n'
|
||||
' action,\n'
|
||||
' button,',
|
||||
' if (comptime !@import("../build_config.zig").inspector) return;\n'
|
||||
' ptr.mouseButtonCallback(\n'
|
||||
' action,\n'
|
||||
' button,'),
|
||||
(' export fn ghostty_inspector_mouse_pos(ptr: *Inspector, x: f64, y: f64) void {\n'
|
||||
' ptr.cursorPosCallback(x, y);',
|
||||
' export fn ghostty_inspector_mouse_pos(ptr: *Inspector, x: f64, y: f64) void {\n'
|
||||
' if (comptime !@import("../build_config.zig").inspector) return;\n'
|
||||
' ptr.cursorPosCallback(x, y);'),
|
||||
(' ptr.scrollCallback(\n'
|
||||
' x,\n'
|
||||
' y,',
|
||||
' if (comptime !@import("../build_config.zig").inspector) return;\n'
|
||||
' ptr.scrollCallback(\n'
|
||||
' x,\n'
|
||||
' y,'),
|
||||
(' ptr.keyCallback(\n'
|
||||
' action,\n'
|
||||
' key,',
|
||||
' if (comptime !@import("../build_config.zig").inspector) return;\n'
|
||||
' ptr.keyCallback(\n'
|
||||
' action,\n'
|
||||
' key,'),
|
||||
(' export fn ghostty_inspector_text(\n'
|
||||
' ptr: *Inspector,\n'
|
||||
' str: [*:0]const u8,\n'
|
||||
' ) void {\n'
|
||||
' ptr.textCallback(std.mem.sliceTo(str, 0));',
|
||||
' export fn ghostty_inspector_text(\n'
|
||||
' ptr: *Inspector,\n'
|
||||
' str: [*:0]const u8,\n'
|
||||
' ) void {\n'
|
||||
' if (comptime !@import("../build_config.zig").inspector) return;\n'
|
||||
' ptr.textCallback(std.mem.sliceTo(str, 0));'),
|
||||
(' export fn ghostty_inspector_set_focus(ptr: *Inspector, focused: bool) void {\n'
|
||||
' ptr.focusCallback(focused);',
|
||||
' export fn ghostty_inspector_set_focus(ptr: *Inspector, focused: bool) void {\n'
|
||||
' if (comptime !@import("../build_config.zig").inspector) return;\n'
|
||||
' ptr.focusCallback(focused);'),
|
||||
(' export fn ghostty_inspector_metal_init(ptr: *Inspector, device: objc.c.id) bool {\n'
|
||||
' return ptr.initMetal(.fromId(device));',
|
||||
' export fn ghostty_inspector_metal_init(ptr: *Inspector, device: objc.c.id) bool {\n'
|
||||
' if (comptime !@import("../build_config.zig").inspector) return false;\n'
|
||||
' return ptr.initMetal(.fromId(device));'),
|
||||
(' return ptr.renderMetal(\n'
|
||||
' .fromId(command_buffer),',
|
||||
' if (comptime !@import("../build_config.zig").inspector) return;\n'
|
||||
' return ptr.renderMetal(\n'
|
||||
' .fromId(command_buffer),'),
|
||||
(' export fn ghostty_inspector_metal_shutdown(ptr: *Inspector) void {\n'
|
||||
' if (ptr.backend) |v| {',
|
||||
' export fn ghostty_inspector_metal_shutdown(ptr: *Inspector) void {\n'
|
||||
' if (comptime !@import("../build_config.zig").inspector) return;\n'
|
||||
' if (ptr.backend) |v| {'),
|
||||
]
|
||||
for old_capi, new_capi in capi_guards:
|
||||
assert old_capi in text, f"CAPI pattern not found: {old_capi[:60]}..."
|
||||
text = text.replace(old_capi, new_capi, 1)
|
||||
|
||||
embedded_path.write_text(text)
|
||||
print("[+] patched apprt/embedded.zig")
|
||||
|
||||
print(f"[+] all inspector patches complete ({marker})")
|
||||
PYEOF
|
||||
|
||||
echo "[+] inspector disable patch applied"
|
||||
111
vendor/libghostty-spm/Patches/ghostty/0008-macos-metal-texture-storage.sh
vendored
Executable file
111
vendor/libghostty-spm/Patches/ghostty/0008-macos-metal-texture-storage.sh
vendored
Executable file
@@ -0,0 +1,111 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
SOURCE_DIR="${1:?Usage: $0 <ghostty-source-dir>}"
|
||||
METAL_ZIG="$SOURCE_DIR/src/renderer/Metal.zig"
|
||||
|
||||
if [ ! -f "$METAL_ZIG" ]; then
|
||||
echo "[-] Metal.zig not found"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if grep -q 'LIBGHOSTTY_SPM_TEXTURE_STORAGE_PATCH' "$METAL_ZIG"; then
|
||||
echo "[+] Metal texture storage already patched"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
python3 - "$METAL_ZIG" <<'PY'
|
||||
from pathlib import Path
|
||||
import sys
|
||||
|
||||
path = Path(sys.argv[1])
|
||||
src = path.read_text()
|
||||
|
||||
replacements = [
|
||||
(
|
||||
"""default_storage_mode: mtl.MTLResourceOptions.StorageMode,
|
||||
|
||||
/// The maximum 2D texture width and height supported by the device.
|
||||
""",
|
||||
"""default_storage_mode: mtl.MTLResourceOptions.StorageMode,
|
||||
|
||||
/// The default storage mode to use for MTLTexture resources.
|
||||
default_texture_storage_mode: mtl.MTLResourceOptions.StorageMode,
|
||||
|
||||
/// The maximum 2D texture width and height supported by the device.
|
||||
""",
|
||||
),
|
||||
(
|
||||
""" const max_texture_size = queryMaxTextureSize(device);
|
||||
log.debug(
|
||||
"device properties default_storage_mode={} max_texture_size={}",
|
||||
.{ default_storage_mode, max_texture_size },
|
||||
);
|
||||
""",
|
||||
""" // LIBGHOSTTY_SPM_TEXTURE_STORAGE_PATCH
|
||||
// MTLStorageModeShared is valid for textures on Apple GPUs, while Intel
|
||||
// and AMD macOS GPUs require managed textures even when hasUnifiedMemory is
|
||||
// true. Keep buffer storage unchanged, but choose texture storage from the
|
||||
// Metal GPU family as Apple recommends for CPU-updated textures.
|
||||
const default_texture_storage_mode: mtl.MTLResourceOptions.StorageMode = switch (comptime builtin.os.tag) {
|
||||
.ios => .shared,
|
||||
.macos => if (device.msgSend(
|
||||
bool,
|
||||
objc.sel("supportsFamily:"),
|
||||
.{mtl.MTLGPUFamily.apple1},
|
||||
)) .shared else .managed,
|
||||
else => default_storage_mode,
|
||||
};
|
||||
const max_texture_size = queryMaxTextureSize(device);
|
||||
log.debug(
|
||||
"device properties default_storage_mode={} default_texture_storage_mode={} max_texture_size={}",
|
||||
.{ default_storage_mode, default_texture_storage_mode, max_texture_size },
|
||||
);
|
||||
""",
|
||||
),
|
||||
(
|
||||
""" .default_storage_mode = default_storage_mode,
|
||||
.max_texture_size = max_texture_size,
|
||||
""",
|
||||
""" .default_storage_mode = default_storage_mode,
|
||||
.default_texture_storage_mode = default_texture_storage_mode,
|
||||
.max_texture_size = max_texture_size,
|
||||
""",
|
||||
),
|
||||
]
|
||||
|
||||
for old, new in replacements:
|
||||
if old not in src:
|
||||
print("[-] Metal.zig structure block not found")
|
||||
sys.exit(1)
|
||||
src = src.replace(old, new, 1)
|
||||
|
||||
old_count = src.count(".storage_mode = self.default_storage_mode")
|
||||
if old_count < 4:
|
||||
print("[-] expected texture storage call sites not found")
|
||||
sys.exit(1)
|
||||
|
||||
src = src.replace(
|
||||
".storage_mode = self.default_storage_mode",
|
||||
".storage_mode = self.default_texture_storage_mode",
|
||||
)
|
||||
|
||||
buffer_marker = """pub inline fn bufferOptions(self: Metal) bufferpkg.Options {"""
|
||||
buffer_start = src.find(buffer_marker)
|
||||
if buffer_start == -1:
|
||||
print("[-] bufferOptions not found")
|
||||
sys.exit(1)
|
||||
|
||||
src = (
|
||||
src[:buffer_start]
|
||||
+ src[buffer_start:].replace(
|
||||
".storage_mode = self.default_texture_storage_mode",
|
||||
".storage_mode = self.default_storage_mode",
|
||||
1,
|
||||
)
|
||||
)
|
||||
|
||||
path.write_text(src)
|
||||
print("[+] patched Metal.zig: split buffer and texture storage modes")
|
||||
PY
|
||||
95
vendor/libghostty-spm/Patches/ghostty/0009-libcxx-apple-availability.sh
vendored
Executable file
95
vendor/libghostty-spm/Patches/ghostty/0009-libcxx-apple-availability.sh
vendored
Executable file
@@ -0,0 +1,95 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
SOURCE_DIR="${1:?Usage: $0 <ghostty-source-dir>}"
|
||||
|
||||
# Zig's bundled libc++ headers ship with Apple vendor availability annotations
|
||||
# disabled, so C++ code references runtime symbols that the Apple system
|
||||
# libc++.1.dylib may not export at our deployment floors (e.g.
|
||||
# std::__1::__libcpp_verbose_abort, exported only since iOS 16.3 /
|
||||
# macOS 13.3 / tvOS 16.3). Forcing the annotations on makes libc++ headers
|
||||
# degrade gracefully below those floors (exactly like Apple SDK clang) and
|
||||
# turns any hard dependency on a too-new symbol into a compile error instead
|
||||
# of a dyld crash at app launch.
|
||||
#
|
||||
# -Wno-macro-redefined: zig predefines the macro to 0 on its own command
|
||||
# line; our -D override redefines it.
|
||||
|
||||
# Patch 1: highway flags (hwy/abort.cc et al reference __libcpp_verbose_abort
|
||||
# through the -fno-exceptions throw helpers)
|
||||
HIGHWAY_BUILD="${SOURCE_DIR}/pkg/highway/build.zig"
|
||||
if [ ! -f "$HIGHWAY_BUILD" ]; then
|
||||
echo "[-] missing: $HIGHWAY_BUILD; upstream changed, update this patch"
|
||||
exit 1
|
||||
fi
|
||||
if ! grep -q '_LIBCPP_HAS_VENDOR_AVAILABILITY_ANNOTATIONS' "$HIGHWAY_BUILD"; then
|
||||
perl -0pi -e 's/try flags\.appendSlice\(b\.allocator, &\.\{\n/try flags.appendSlice(b.allocator, &.{\n "-D_LIBCPP_HAS_VENDOR_AVAILABILITY_ANNOTATIONS=1",\n "-Wno-macro-redefined",\n/' "$HIGHWAY_BUILD"
|
||||
grep -q '_LIBCPP_HAS_VENDOR_AVAILABILITY_ANNOTATIONS' "$HIGHWAY_BUILD" || {
|
||||
echo "[-] highway flags block not found; upstream changed, update this patch"
|
||||
exit 1
|
||||
}
|
||||
echo "[+] patched: highway libc++ availability annotations"
|
||||
else
|
||||
echo "[+] highway libc++ availability already patched"
|
||||
fi
|
||||
|
||||
# Patch 2: simdutf flags
|
||||
SIMDUTF_BUILD="${SOURCE_DIR}/pkg/simdutf/build.zig"
|
||||
if [ ! -f "$SIMDUTF_BUILD" ]; then
|
||||
echo "[-] missing: $SIMDUTF_BUILD; upstream changed, update this patch"
|
||||
exit 1
|
||||
fi
|
||||
if ! grep -q '_LIBCPP_HAS_VENDOR_AVAILABILITY_ANNOTATIONS' "$SIMDUTF_BUILD"; then
|
||||
perl -0pi -e 's/try flags\.appendSlice\(b\.allocator, &\.\{\n/try flags.appendSlice(b.allocator, &.{\n "-D_LIBCPP_HAS_VENDOR_AVAILABILITY_ANNOTATIONS=1",\n "-Wno-macro-redefined",\n/' "$SIMDUTF_BUILD"
|
||||
grep -q '_LIBCPP_HAS_VENDOR_AVAILABILITY_ANNOTATIONS' "$SIMDUTF_BUILD" || {
|
||||
echo "[-] simdutf flags block not found; upstream changed, update this patch"
|
||||
exit 1
|
||||
}
|
||||
echo "[+] patched: simdutf libc++ availability annotations"
|
||||
else
|
||||
echo "[+] simdutf libc++ availability already patched"
|
||||
fi
|
||||
|
||||
# Patch 3: ghostty's own C++ SIMD sources (src/simd/*.cpp)
|
||||
SHARED_DEPS="${SOURCE_DIR}/src/build/SharedDeps.zig"
|
||||
if [ ! -f "$SHARED_DEPS" ]; then
|
||||
echo "[-] missing: $SHARED_DEPS; upstream changed, update this patch"
|
||||
exit 1
|
||||
fi
|
||||
if grep -q 'HWY_NO_LIBCXX' "$SHARED_DEPS" &&
|
||||
grep -q 'SIMDUTF_NO_LIBCXX' "$SHARED_DEPS"; then
|
||||
echo "[+] src/simd already builds without libc++"
|
||||
elif ! grep -q '_LIBCPP_HAS_VENDOR_AVAILABILITY_ANNOTATIONS' "$SHARED_DEPS"; then
|
||||
python3 - "$SHARED_DEPS" <<'PY'
|
||||
from pathlib import Path
|
||||
import sys
|
||||
|
||||
path = Path(sys.argv[1])
|
||||
text = path.read_text()
|
||||
|
||||
old_flags = """ .flags = if (target.result.cpu.arch == .x86_64) &.{
|
||||
b.fmt("-DHWY_DISABLED_TARGETS={}", .{HWY_DISABLED_TARGETS}),
|
||||
} else &.{},"""
|
||||
|
||||
new_flags = """ .flags = if (target.result.cpu.arch == .x86_64) &.{
|
||||
"-D_LIBCPP_HAS_VENDOR_AVAILABILITY_ANNOTATIONS=1",
|
||||
"-Wno-macro-redefined",
|
||||
b.fmt("-DHWY_DISABLED_TARGETS={}", .{HWY_DISABLED_TARGETS}),
|
||||
} else &.{
|
||||
"-D_LIBCPP_HAS_VENDOR_AVAILABILITY_ANNOTATIONS=1",
|
||||
"-Wno-macro-redefined",
|
||||
},"""
|
||||
|
||||
if old_flags not in text:
|
||||
print("[-] src/simd flags block not found; upstream changed, update this patch")
|
||||
sys.exit(1)
|
||||
|
||||
path.write_text(text.replace(old_flags, new_flags))
|
||||
print("[+] patched: src/simd libc++ availability annotations")
|
||||
PY
|
||||
else
|
||||
echo "[+] src/simd libc++ availability already patched"
|
||||
fi
|
||||
|
||||
echo "[+] all libcxx-apple-availability patches applied"
|
||||
22
vendor/libghostty-spm/Patches/ghostty/0010-fix-scroll-remainder-zeroing.patch
vendored
Normal file
22
vendor/libghostty-spm/Patches/ghostty/0010-fix-scroll-remainder-zeroing.patch
vendored
Normal file
@@ -0,0 +1,22 @@
|
||||
diff --git a/src/Surface.zig b/src/Surface.zig
|
||||
index 4d66622..c0fe19e 100644
|
||||
--- a/src/Surface.zig
|
||||
+++ b/src/Surface.zig
|
||||
@@ -3430,7 +3430,7 @@ pub fn scrollCallback(
|
||||
}
|
||||
|
||||
// We scroll by the number of rows in the offset and save the remainder
|
||||
- const amount = poff / cell_size;
|
||||
+ const amount = @trunc(poff / cell_size);
|
||||
assert(@abs(amount) >= 1);
|
||||
self.mouse.pending_scroll_y = poff - (amount * cell_size);
|
||||
|
||||
@@ -3455,7 +3455,7 @@ pub fn scrollCallback(
|
||||
break :x .{};
|
||||
}
|
||||
|
||||
- const amount = poff / cell_size;
|
||||
+ const amount = @trunc(poff / cell_size);
|
||||
assert(@abs(amount) >= 1);
|
||||
self.mouse.pending_scroll_x = poff - (amount * cell_size);
|
||||
const delta: isize = @intFromFloat(@trunc(amount));
|
||||
25
vendor/libghostty-spm/Patches/ghostty/README.md
vendored
Normal file
25
vendor/libghostty-spm/Patches/ghostty/README.md
vendored
Normal file
@@ -0,0 +1,25 @@
|
||||
# Ghostty Patches
|
||||
|
||||
This directory is the single place for local upstream Ghostty patches used by
|
||||
the `libghostty-spm` build pipeline.
|
||||
|
||||
## Rules
|
||||
|
||||
- Keep patches numbered so they apply in a stable order.
|
||||
- Prefer standard unified diff files (`.patch`) when the upstream context is
|
||||
stable.
|
||||
- Use executable patch scripts (`.sh`) only when upstream context is too
|
||||
unstable for a reliable diff.
|
||||
- Keep version-specific variants beside the original patch and select them in
|
||||
`Script/apply-patches.sh` using an upstream API marker.
|
||||
- Preserve newer Ghostty's renamed internal-library outputs when extending its
|
||||
Darwin static-library build path.
|
||||
- Every patch in this directory must be safe to re-run.
|
||||
- Patches here are applied automatically by `Script/build-ghostty.sh`, so they
|
||||
affect macOS, iOS, and Mac Catalyst builds equally.
|
||||
|
||||
## Current goal
|
||||
|
||||
This patch workflow exists so we can carry host-managed IO work required for
|
||||
sandboxed iOS, macOS, and Mac Catalyst integration without hiding upstream
|
||||
modifications inside ad-hoc build script edits.
|
||||
174
vendor/libghostty-spm/README.md
vendored
Normal file
174
vendor/libghostty-spm/README.md
vendored
Normal file
@@ -0,0 +1,174 @@
|
||||
# GhosttyKit
|
||||
|
||||
Swift Package wrapping [Ghostty](https://ghostty.org)'s terminal emulator library for Apple platforms.
|
||||
|
||||
> Pre-built `libghostty` static library distributed as an XCFramework binary target.
|
||||
|
||||
## Platforms
|
||||
|
||||
- macOS 13+
|
||||
- iOS 15+
|
||||
- Mac Catalyst 15+
|
||||
|
||||
## Products
|
||||
|
||||
| Library | Description |
|
||||
| ----------------- | ------------------------------------------------------------------------------- |
|
||||
| `GhosttyKit` | Re-exports the libghostty C API (`ghostty.h`) |
|
||||
| `GhosttyTerminal` | Swift wrapper — native views, SwiftUI integration, input handling, display link |
|
||||
| `GhosttyTheme` | 485 terminal color themes from [iTerm2-Color-Schemes](https://github.com/mbadolato/iTerm2-Color-Schemes) (MIT License) |
|
||||
| `ShellCraftKit` | Sandboxed shell emulation framework (depends on GhosttyTerminal) |
|
||||
|
||||
## Installation
|
||||
|
||||
Add to your `Package.swift`:
|
||||
|
||||
```swift
|
||||
dependencies: [
|
||||
.package(url: "https://github.com/Lakr233/libghostty-spm.git", from: "1.2.0"),
|
||||
]
|
||||
```
|
||||
|
||||
Then add the product you need:
|
||||
|
||||
```swift
|
||||
.target(
|
||||
name: "YourTarget",
|
||||
dependencies: [
|
||||
.product(name: "GhosttyTerminal", package: "libghostty-spm"),
|
||||
]
|
||||
)
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
The example apps are the best starting point for real integration:
|
||||
|
||||
- `Example/GhosttyTerminalApp/` — macOS AppKit demo with delegate callbacks
|
||||
- `Example/MobileGhosttyApp/` — iOS UIKit demo with keyboard, safe area, themes, and text selection
|
||||
|
||||
### SwiftUI (iOS 15+ / macOS 13+ / Mac Catalyst 15+)
|
||||
|
||||
```swift
|
||||
import SwiftUI
|
||||
import GhosttyTerminal
|
||||
|
||||
struct ContentView: View {
|
||||
@StateObject private var terminal = TerminalViewState()
|
||||
private let session = InMemoryTerminalSession(
|
||||
write: { data in
|
||||
// Handle bytes produced by the terminal.
|
||||
},
|
||||
resize: { viewport in
|
||||
// Keep your host backend in sync with the terminal grid.
|
||||
}
|
||||
)
|
||||
|
||||
var body: some View {
|
||||
TerminalSurfaceView(context: terminal)
|
||||
.navigationTitle(terminal.title)
|
||||
.onAppear {
|
||||
terminal.configuration = TerminalSurfaceOptions(
|
||||
backend: .inMemory(session)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### UIKit / AppKit
|
||||
|
||||
```swift
|
||||
import GhosttyTerminal
|
||||
|
||||
let terminalView = TerminalView(frame: .zero)
|
||||
terminalView.delegate = self
|
||||
terminalView.controller = TerminalController(configFilePath: path)
|
||||
terminalView.configuration = TerminalSurfaceOptions(
|
||||
backend: .inMemory(session)
|
||||
)
|
||||
```
|
||||
|
||||
`TerminalView` is a type alias that resolves to `UITerminalView` (iOS/Catalyst) or `AppTerminalView` (macOS).
|
||||
|
||||
### Prompt and scrollback navigation
|
||||
|
||||
`TerminalViewState`, `TerminalView`, and `TerminalSurface` expose the same
|
||||
programmatic navigation APIs:
|
||||
|
||||
```swift
|
||||
terminal.jumpToPrompt(by: -1) // Previous prompt.
|
||||
terminal.jumpToPrompt(by: 1) // Next prompt.
|
||||
terminal.scrollToRow(0) // First absolute scrollback row.
|
||||
```
|
||||
|
||||
Prompt navigation requires [Ghostty shell integration](https://ghostty.org/docs/features/shell-integration),
|
||||
which records prompt boundaries. A host-managed backend must preserve or emit
|
||||
equivalent OSC 133 prompt markers. Arbitrary Ghostty actions remain available
|
||||
through `performBindingAction(_:)`.
|
||||
|
||||
## Notes
|
||||
|
||||
- `TerminalViewState` is the SwiftUI state container.
|
||||
- `TerminalView` is the UIKit/AppKit view typealias.
|
||||
- `TerminalController` owns app lifecycle, config resolution, themes, and surface creation.
|
||||
- `InMemoryTerminalSession` provides the host-managed backend used by the sandboxed example apps.
|
||||
- `GhosttyThemeCatalog` exposes bundled iTerm2 color schemes.
|
||||
|
||||
## Building from Source
|
||||
|
||||
The package includes a pre-built XCFramework. To rebuild libghostty from the Ghostty source:
|
||||
|
||||
```bash
|
||||
# Requires: zig compiler
|
||||
./Script/build.sh
|
||||
```
|
||||
|
||||
This applies patches from `Patches/ghostty/`, builds for all target architectures, and assembles the XCFramework.
|
||||
|
||||
## Release Versioning
|
||||
|
||||
Bare semantic-version tags such as `1.3.1` are GhosttyKit Swift package
|
||||
versions. They are independent from Ghostty's upstream tags. The matching
|
||||
`storage.1.3.1` release stores the XCFramework consumed by that package tag.
|
||||
|
||||
Release builds use the immutable upstream Ghostty commit recorded in
|
||||
`Ghostty.ref`. Updating Ghostty requires a reviewed change to that file, so a
|
||||
package release cannot silently switch to a different upstream tag or commit.
|
||||
Manual releases require an explicit package version; scheduled releases only
|
||||
increment the package patch version when `main` is newer than the latest
|
||||
package tag.
|
||||
|
||||
## Trimmed Build
|
||||
|
||||
The bundled `libghostty` is a trimmed build optimized for sandboxed, embedded use on Apple platforms.
|
||||
|
||||
| Component | Upstream Ghostty | libghostty-spm | Reason |
|
||||
| -------------------------------- | ---------------- | ---------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| Terminal emulation core | Yes | Yes | Full VT parser, state machine, grid — retained |
|
||||
| Metal renderer | Yes | Yes | GPU rendering via CAMetalLayer / IOSurface — retained |
|
||||
| Font rasterization & shaping | Yes | Yes | CoreText font backend — retained |
|
||||
| Configuration system | Yes | Yes | All terminal config options — retained |
|
||||
| Input handling (key, mouse, IME) | Yes | Yes | Full keyboard/mouse/touch/IME pipeline — retained |
|
||||
| Text selection & clipboard | Yes | Yes | Selection, copy/paste APIs — retained |
|
||||
| Custom shaders (GLSL) | Yes | **No** | `glslang` and `spirv-cross` removed (`-Dcustom-shaders=false`). Shadertoy/post-processing shaders are a desktop feature unnecessary for embedded use. |
|
||||
| Terminal inspector (ImGui) | Yes | **No** | `dcimgui` removed (`-Dinspector=false`). Debug inspector UI replaced with no-op stubs. |
|
||||
| Sentry crash reporting | Yes | **No** | Disabled (`-Dsentry=false`). |
|
||||
| Native app runtime | Yes | **No** | Cocoa/GTK/Wayland app shell disabled (`-Dapp-runtime=none`). The host app provides its own runtime. |
|
||||
| Standalone executable | Yes | **No** | No terminal `.app` or CLI binary emitted (`-Demit-exe=false`). |
|
||||
| Documentation generation | Yes | **No** | Skipped (`-Demit-docs=false`). |
|
||||
| Frame data generator | Build-time tool | **Pre-compiled** | `framedata.compressed` shipped pre-built; framegen C tool dependency removed. |
|
||||
| Host-managed I/O backend | No | **Added** | New `GHOSTTY_SURFACE_IO_BACKEND_HOST_MANAGED` for non-PTY, sandbox-safe terminal I/O. |
|
||||
| iOS Metal rendering fixes | No | **Added** | IOSurface +1px tolerance, synchronous present, 64-byte row alignment for iOS. |
|
||||
| iOS platform fixes | No | **Added** | Deployment target lowered, private API removed, kqueue fix for simulator. |
|
||||
|
||||
## License
|
||||
|
||||
MIT License. See [LICENSE](LICENSE) for details.
|
||||
|
||||
The bundled `libghostty` binary is built from [Ghostty](https://ghostty.org), which has its own license terms.
|
||||
|
||||
## Sponsor
|
||||
|
||||
- [LookInside](https://lookinside-app.com/) helps you inspect a running iOS or macOS app UI from your Mac.
|
||||
- This project/repository is sponsored by AFK AI, INC.
|
||||
106
vendor/libghostty-spm/Script/apply-patches.sh
vendored
Executable file
106
vendor/libghostty-spm/Script/apply-patches.sh
vendored
Executable file
@@ -0,0 +1,106 @@
|
||||
#!/bin/zsh
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
cd "$(dirname "$0")/.."
|
||||
|
||||
if [ ! -f .root ]; then
|
||||
echo "[-] malformed project structure"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
SOURCE_DIR=${1:-}
|
||||
PATCH_DIR=${2:-"$(pwd)/Patches/ghostty"}
|
||||
|
||||
if [ -z "$SOURCE_DIR" ]; then
|
||||
echo "Usage: $0 <source_dir> [patch_dir]"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ ! -d "$SOURCE_DIR" ]; then
|
||||
echo "[-] ghostty source directory not found: $SOURCE_DIR"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ ! -d "$PATCH_DIR" ]; then
|
||||
echo "[+] no patches directory found: $PATCH_DIR"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
apply_unified_patch() {
|
||||
local patch_file="$1"
|
||||
|
||||
if [ -d "$SOURCE_DIR/.git" ] && command -v git >/dev/null 2>&1; then
|
||||
if git -C "$SOURCE_DIR" apply --check --reverse "$patch_file" >/dev/null 2>&1; then
|
||||
echo "[+] patch already applied: $(basename "$patch_file")"
|
||||
return
|
||||
fi
|
||||
|
||||
if ! git -C "$SOURCE_DIR" apply --check "$patch_file" >/dev/null 2>&1; then
|
||||
echo "[-] failed to validate patch: $patch_file"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
git -C "$SOURCE_DIR" apply "$patch_file"
|
||||
echo "[+] applied patch: $(basename "$patch_file")"
|
||||
return
|
||||
fi
|
||||
|
||||
if patch -p1 -R --dry-run -d "$SOURCE_DIR" <"$patch_file" >/dev/null 2>&1; then
|
||||
echo "[+] patch already applied: $(basename "$patch_file")"
|
||||
return
|
||||
fi
|
||||
|
||||
if ! patch -p1 --dry-run -d "$SOURCE_DIR" <"$patch_file" >/dev/null 2>&1; then
|
||||
echo "[-] failed to validate patch: $patch_file"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
patch -p1 -d "$SOURCE_DIR" <"$patch_file" >/dev/null
|
||||
echo "[+] applied patch: $(basename "$patch_file")"
|
||||
}
|
||||
|
||||
modern_host_io=false
|
||||
if grep -q "ghostty_surface_foreground_pid" "$SOURCE_DIR/include/ghostty.h"; then
|
||||
modern_host_io=true
|
||||
fi
|
||||
|
||||
host_io_applied=false
|
||||
if grep -q "GHOSTTY_SURFACE_IO_BACKEND_HOST_MANAGED" "$SOURCE_DIR/include/ghostty.h"; then
|
||||
host_io_applied=true
|
||||
fi
|
||||
|
||||
for patch_file in "$PATCH_DIR"/*; do
|
||||
[ -e "$patch_file" ] || continue
|
||||
|
||||
patch_name=$(basename "$patch_file")
|
||||
case "$patch_name" in
|
||||
0002-host-managed-io.patch)
|
||||
[ "$modern_host_io" = false ] || continue
|
||||
if [ "$host_io_applied" = true ]; then
|
||||
echo "[+] patch already applied: $patch_name"
|
||||
continue
|
||||
fi
|
||||
apply_unified_patch "$patch_file"
|
||||
;;
|
||||
0002-host-managed-io-modern.patch)
|
||||
[ "$modern_host_io" = true ] || continue
|
||||
if [ "$host_io_applied" = true ]; then
|
||||
echo "[+] patch already applied: $patch_name"
|
||||
continue
|
||||
fi
|
||||
apply_unified_patch "$patch_file"
|
||||
;;
|
||||
*.md) ;;
|
||||
*.patch)
|
||||
apply_unified_patch "$patch_file"
|
||||
;;
|
||||
*.sh)
|
||||
"$patch_file" "$SOURCE_DIR"
|
||||
;;
|
||||
*)
|
||||
echo "[-] unsupported patch file: $patch_file"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
done
|
||||
273
vendor/libghostty-spm/Script/build-ghostty.sh
vendored
Executable file
273
vendor/libghostty-spm/Script/build-ghostty.sh
vendored
Executable file
@@ -0,0 +1,273 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
cd "$(dirname "$0")/.."
|
||||
|
||||
if [ ! -f .root ]; then
|
||||
echo "[*] malformed project structure"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
ROOT_DIR=$(pwd)
|
||||
SOURCE_DIR=${1:-}
|
||||
ZIG_TARGET=${2:-}
|
||||
OUTPUT_DIR=${3:-}
|
||||
ZIG_CPU=${ZIG_CPU:-}
|
||||
ZIG_BUILD_EXTRA_ARGS=${ZIG_BUILD_EXTRA_ARGS:-}
|
||||
|
||||
if [ -z "$SOURCE_DIR" ] || [ -z "$ZIG_TARGET" ] || [ -z "$OUTPUT_DIR" ]; then
|
||||
echo "Usage: $0 <source_dir> <zig_target> <output_dir>"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ ! -d "$SOURCE_DIR" ]; then
|
||||
echo "[!] ghostty source directory not found: $SOURCE_DIR"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ ! -f "$SOURCE_DIR/include/ghostty.h" ]; then
|
||||
echo "[!] ghostty header not found: $SOURCE_DIR/include/ghostty.h"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if ! command -v zig >/dev/null 2>&1; then
|
||||
echo "[!] zig not found"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
REQUESTED_TARGET="$ZIG_TARGET"
|
||||
IS_ARM64E=0
|
||||
APPLE_SDK=
|
||||
APPLE_TARGET=
|
||||
|
||||
case "$REQUESTED_TARGET" in
|
||||
arm64e-macos)
|
||||
IS_ARM64E=1
|
||||
ZIG_TARGET="aarch64-macos"
|
||||
APPLE_SDK="macosx"
|
||||
APPLE_TARGET="arm64e-apple-macosx13.0"
|
||||
;;
|
||||
arm64e-ios)
|
||||
IS_ARM64E=1
|
||||
ZIG_TARGET="aarch64-ios"
|
||||
APPLE_SDK="iphoneos"
|
||||
APPLE_TARGET="arm64e-apple-ios15.0"
|
||||
;;
|
||||
arm64e-ios-macabi)
|
||||
IS_ARM64E=1
|
||||
ZIG_TARGET="aarch64-ios-macabi"
|
||||
APPLE_SDK="macosx"
|
||||
APPLE_TARGET="arm64e-apple-ios15.0-macabi"
|
||||
;;
|
||||
esac
|
||||
|
||||
./Script/apply-patches.sh "$SOURCE_DIR"
|
||||
|
||||
CACHE_ROOT="${BUILD_CACHE_ROOT:-$ROOT_DIR/build/cache}"
|
||||
GLOBAL_CACHE_DIR="${ZIG_GLOBAL_CACHE_DIR:-$CACHE_ROOT/zig-global}"
|
||||
LOCAL_CACHE_DIR="$CACHE_ROOT/$REQUESTED_TARGET/zig-local"
|
||||
MODULE_CACHE_DIR="${CLANG_MODULE_CACHE_ROOT:-$CACHE_ROOT/clang-module-cache}/$REQUESTED_TARGET"
|
||||
|
||||
if [ "$IS_ARM64E" -eq 1 ]; then
|
||||
if ! command -v xcrun >/dev/null 2>&1; then
|
||||
echo "[!] xcrun not found for arm64e build"
|
||||
exit 1
|
||||
fi
|
||||
./Script/prepare-arm64e-source.sh "$SOURCE_DIR" "$GLOBAL_CACHE_DIR"
|
||||
fi
|
||||
|
||||
echo "[*] building ghostty static library"
|
||||
echo " target: $REQUESTED_TARGET"
|
||||
echo " source: $SOURCE_DIR"
|
||||
echo " output: $OUTPUT_DIR"
|
||||
|
||||
rm -rf "$OUTPUT_DIR" "$LOCAL_CACHE_DIR" "$MODULE_CACHE_DIR"
|
||||
mkdir -p \
|
||||
"$OUTPUT_DIR/lib" \
|
||||
"$OUTPUT_DIR/include" \
|
||||
"$GLOBAL_CACHE_DIR" \
|
||||
"$LOCAL_CACHE_DIR" \
|
||||
"$MODULE_CACHE_DIR"
|
||||
|
||||
rm -rf "$SOURCE_DIR/zig-out"
|
||||
|
||||
ZIG_BUILD_COMMAND=(
|
||||
zig build
|
||||
-Doptimize=${ZIG_OPTIMIZE:-ReleaseFast}
|
||||
-Dapp-runtime=none
|
||||
-Demit-exe=false
|
||||
-Demit-xcframework=false
|
||||
-Demit-macos-app=false
|
||||
-Demit-docs=false
|
||||
-Dsentry=false
|
||||
-Dcustom-shaders=false
|
||||
-Dinspector=false
|
||||
-Dtarget="$ZIG_TARGET"
|
||||
)
|
||||
|
||||
if [ -n "$ZIG_CPU" ]; then
|
||||
ZIG_BUILD_COMMAND+=("-Dcpu=$ZIG_CPU")
|
||||
fi
|
||||
|
||||
if [ -n "$ZIG_BUILD_EXTRA_ARGS" ]; then
|
||||
# shellcheck disable=SC2206
|
||||
EXTRA_ARGS=($ZIG_BUILD_EXTRA_ARGS)
|
||||
ZIG_BUILD_COMMAND+=("${EXTRA_ARGS[@]}")
|
||||
fi
|
||||
|
||||
(
|
||||
cd "$SOURCE_DIR"
|
||||
env \
|
||||
CLANG_MODULE_CACHE_PATH="$MODULE_CACHE_DIR" \
|
||||
ZIG_GLOBAL_CACHE_DIR="$GLOBAL_CACHE_DIR" \
|
||||
ZIG_LOCAL_CACHE_DIR="$LOCAL_CACHE_DIR" \
|
||||
"${ZIG_BUILD_COMMAND[@]}"
|
||||
)
|
||||
|
||||
find_built_library() {
|
||||
local preferred_name="$1"
|
||||
find "$LOCAL_CACHE_DIR/o" -type f -name "$preferred_name" -print 2>/dev/null | sort | tail -n 1
|
||||
}
|
||||
|
||||
LIBRARY_PATH=
|
||||
|
||||
if [ -f "$SOURCE_DIR/zig-out/lib/libghostty.a" ]; then
|
||||
LIBRARY_PATH="$SOURCE_DIR/zig-out/lib/libghostty.a"
|
||||
fi
|
||||
|
||||
if [ -z "$LIBRARY_PATH" ]; then
|
||||
LIBRARY_PATH=$(find_built_library "libghostty-fat.a")
|
||||
fi
|
||||
|
||||
if [ -z "$LIBRARY_PATH" ]; then
|
||||
LIBRARY_PATH=$(find_built_library "libghostty.a")
|
||||
fi
|
||||
|
||||
if [ -z "$LIBRARY_PATH" ]; then
|
||||
echo "[!] failed to locate built libghostty archive in $LOCAL_CACHE_DIR"
|
||||
if [[ "$ZIG_TARGET" == *macos* || "$ZIG_TARGET" == *ios* || "$ZIG_TARGET" == *tvos* || "$ZIG_TARGET" == *visionos* || "$ZIG_TARGET" == *watchos* ]]; then
|
||||
echo "[!] note: upstream Ghostty does not install Darwin libghostty for app-runtime=none unless extra build wiring is triggered"
|
||||
echo "[!] try again with ZIG_BUILD_EXTRA_ARGS='-Demit-xcframework=true' if you want to force Darwin libghostty build graph execution"
|
||||
fi
|
||||
find "$LOCAL_CACHE_DIR" -maxdepth 3 -type f | sort | tail -n 50
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Resolve std::__1::__libcpp_verbose_abort inside the archive: the Apple
|
||||
# system libc++ only exports it since iOS 16.3 / macOS 13.3 / tvOS 16.3, and
|
||||
# Zig's bundled libc++ headers reference it strongly (no availability markup),
|
||||
# which crashes consumers at launch on older OS versions.
|
||||
COMPAT_SOURCE="$ROOT_DIR/Script/support/libcxx-verbose-abort-compat.c"
|
||||
COMPAT_OBJECT="$LOCAL_CACHE_DIR/libcxx-verbose-abort-compat.o"
|
||||
zig cc -target "$ZIG_TARGET" -Os -fno-sanitize=undefined -c "$COMPAT_SOURCE" -o "$COMPAT_OBJECT"
|
||||
|
||||
if [ "$IS_ARM64E" -eq 1 ]; then
|
||||
RAW_ARCHIVE="$LOCAL_CACHE_DIR/libghostty-arm64.a"
|
||||
CONVERTED_ARCHIVE="$LOCAL_CACHE_DIR/libghostty-arm64e.a"
|
||||
CORE_OBJECT="$LOCAL_CACHE_DIR/libghostty-arm64e-core.o"
|
||||
ARM64E_COMPAT_OBJECT="$LOCAL_CACHE_DIR/arm64e-compat.o"
|
||||
|
||||
xcrun libtool -static -no_warning_for_no_symbols -o "$RAW_ARCHIVE" "$LIBRARY_PATH" "$COMPAT_OBJECT"
|
||||
python3 "$ROOT_DIR/Script/convert-archive-arm64e.py" "$RAW_ARCHIVE" "$CONVERTED_ARCHIVE"
|
||||
|
||||
xcrun --sdk "$APPLE_SDK" clang \
|
||||
-target "$APPLE_TARGET" \
|
||||
-r \
|
||||
-nostdlib \
|
||||
"-Wl,-force_load,$CONVERTED_ARCHIVE" \
|
||||
-Wl,-alias,_ghostty_app_new,_ghostty_arm64e_core_app_new \
|
||||
-Wl,-unexported_symbol,_ghostty_app_new \
|
||||
-Wl,-alias,_ghostty_surface_new,_ghostty_arm64e_core_surface_new \
|
||||
-Wl,-unexported_symbol,_ghostty_surface_new \
|
||||
-o "$CORE_OBJECT"
|
||||
|
||||
xcrun --sdk "$APPLE_SDK" clang \
|
||||
-target "$APPLE_TARGET" \
|
||||
-Os \
|
||||
-fno-sanitize=undefined \
|
||||
-I "$SOURCE_DIR/include" \
|
||||
-c "$ROOT_DIR/Script/support/arm64e-compat.c" \
|
||||
-o "$ARM64E_COMPAT_OBJECT"
|
||||
|
||||
xcrun libtool -static -no_warning_for_no_symbols \
|
||||
-o "$OUTPUT_DIR/lib/libghostty.a" \
|
||||
"$CORE_OBJECT" \
|
||||
"$ARM64E_COMPAT_OBJECT"
|
||||
|
||||
if [ "$(lipo -archs "$OUTPUT_DIR/lib/libghostty.a")" != "arm64e" ]; then
|
||||
echo "[!] built archive is not a single arm64e slice"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
SYMBOLS=$(nm -gjU "$OUTPUT_DIR/lib/libghostty.a" | sort -u)
|
||||
for symbol in \
|
||||
_ghostty_app_new \
|
||||
_ghostty_surface_new \
|
||||
_ghostty_arm64e_core_app_new \
|
||||
_ghostty_arm64e_core_surface_new \
|
||||
_pa_pthread_cr \
|
||||
_pa_qs; do
|
||||
if ! grep -Fxq "$symbol" <<<"$SYMBOLS"; then
|
||||
echo "[!] arm64e archive missing symbol: $symbol"
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
|
||||
CORE_IMPORTS=$(nm -u "$CORE_OBJECT" | sort -u)
|
||||
for symbol in _pa_pthread_cr _pa_qs; do
|
||||
if ! grep -Fxq "$symbol" <<<"$CORE_IMPORTS"; then
|
||||
echo "[!] arm64e core missing redirected import: $symbol"
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
for symbol in _pthread_create _qsort; do
|
||||
if grep -Fxq "$symbol" <<<"$CORE_IMPORTS"; then
|
||||
echo "[!] arm64e core contains unsigned callback import: $symbol"
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
echo "[*] built pointer-authenticated arm64e archive"
|
||||
else
|
||||
ARM64E_COMPAT_OBJECT="$LOCAL_CACHE_DIR/arm64e-compat.o"
|
||||
case "$ZIG_TARGET" in
|
||||
*-ios-simulator) COMPAT_SDK="iphonesimulator" ;;
|
||||
*-ios-macabi | *-macos) COMPAT_SDK="macosx" ;;
|
||||
*-ios) COMPAT_SDK="iphoneos" ;;
|
||||
*-tvos-simulator) COMPAT_SDK="appletvsimulator" ;;
|
||||
*-tvos) COMPAT_SDK="appletvos" ;;
|
||||
*-visionos-simulator) COMPAT_SDK="xrsimulator" ;;
|
||||
*-visionos) COMPAT_SDK="xros" ;;
|
||||
*-watchos-simulator) COMPAT_SDK="watchsimulator" ;;
|
||||
*-watchos) COMPAT_SDK="watchos" ;;
|
||||
*)
|
||||
echo "[!] unsupported compatibility target: $ZIG_TARGET"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
COMPAT_SDK_PATH=$(xcrun --sdk "$COMPAT_SDK" --show-sdk-path)
|
||||
zig cc \
|
||||
-target "$ZIG_TARGET" \
|
||||
-Os \
|
||||
-fno-sanitize=undefined \
|
||||
-isystem "$COMPAT_SDK_PATH/usr/include" \
|
||||
-I "$SOURCE_DIR/include" \
|
||||
-c "$ROOT_DIR/Script/support/arm64e-compat.c" \
|
||||
-o "$ARM64E_COMPAT_OBJECT"
|
||||
xcrun libtool -static -no_warning_for_no_symbols \
|
||||
-o "$OUTPUT_DIR/lib/libghostty.a" \
|
||||
"$LIBRARY_PATH" \
|
||||
"$COMPAT_OBJECT" \
|
||||
"$ARM64E_COMPAT_OBJECT"
|
||||
echo "[*] appended compatibility objects"
|
||||
fi
|
||||
|
||||
cp "$SOURCE_DIR/include/ghostty.h" "$OUTPUT_DIR/include/ghostty.h"
|
||||
cat >"$OUTPUT_DIR/include/module.modulemap" <<'EOF'
|
||||
module libghostty {
|
||||
umbrella header "ghostty.h"
|
||||
export *
|
||||
}
|
||||
EOF
|
||||
|
||||
echo "[*] built archive: $OUTPUT_DIR/lib/libghostty.a"
|
||||
32
vendor/libghostty-spm/Script/build-manifest.sh
vendored
Executable file
32
vendor/libghostty-spm/Script/build-manifest.sh
vendored
Executable file
@@ -0,0 +1,32 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
cd "$(dirname "$0")/.."
|
||||
|
||||
if [ ! -f .root ]; then
|
||||
echo "[*] malformed project structure"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
XCFRAMEWORK_PATH_ZIP=${1:-}
|
||||
DOWNLOAD_URL=${2:-}
|
||||
|
||||
if [ -z "$XCFRAMEWORK_PATH_ZIP" ] || [ -z "$DOWNLOAD_URL" ]; then
|
||||
echo "Usage: $0 <xcframework_zip> <download_url>"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ ! -f "$XCFRAMEWORK_PATH_ZIP" ]; then
|
||||
echo "[!] $XCFRAMEWORK_PATH_ZIP not found"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
SHA256SUM=$(shasum -a 256 "$XCFRAMEWORK_PATH_ZIP" | awk '{print $1}')
|
||||
echo "[*] SHA256: $SHA256SUM"
|
||||
|
||||
PACKAGE_MANIFEST=$(cat Package.swift.template)
|
||||
PACKAGE_MANIFEST=${PACKAGE_MANIFEST/__DOWNLOAD_URL__/$DOWNLOAD_URL}
|
||||
PACKAGE_MANIFEST=${PACKAGE_MANIFEST/__CHECKSUM__/$SHA256SUM}
|
||||
|
||||
echo "$PACKAGE_MANIFEST" >Package.swift
|
||||
126
vendor/libghostty-spm/Script/build-platform.sh
vendored
Executable file
126
vendor/libghostty-spm/Script/build-platform.sh
vendored
Executable file
@@ -0,0 +1,126 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
cd "$(dirname "$0")/.."
|
||||
|
||||
if [ ! -f .root ]; then
|
||||
echo "[*] malformed project structure"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
SOURCE_DIR=${1:-}
|
||||
PLATFORM_GROUP=${2:-}
|
||||
OUTPUT_DIR=${3:-}
|
||||
|
||||
if [ -z "$SOURCE_DIR" ] || [ -z "$PLATFORM_GROUP" ] || [ -z "$OUTPUT_DIR" ]; then
|
||||
echo "Usage: $0 <source_dir> <platform_group> <output_dir>"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ ! -d "$SOURCE_DIR" ]; then
|
||||
echo "[!] ghostty source directory not found: $SOURCE_DIR"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
build_variant() {
|
||||
local variant_name="$1"
|
||||
shift
|
||||
|
||||
local variant_dir="$OUTPUT_DIR/$variant_name"
|
||||
local intermediate_dir="$OUTPUT_DIR/.intermediates/$variant_name"
|
||||
local first_headers=
|
||||
local libraries=()
|
||||
local target_spec=
|
||||
local target=
|
||||
local cpu=
|
||||
|
||||
rm -rf "$variant_dir" "$intermediate_dir"
|
||||
mkdir -p "$variant_dir/lib" "$variant_dir/include" "$intermediate_dir"
|
||||
|
||||
for target_spec in "$@"; do
|
||||
target=${target_spec%%@*}
|
||||
cpu=
|
||||
if [ "$target_spec" != "$target" ]; then
|
||||
cpu=${target_spec#*@}
|
||||
fi
|
||||
|
||||
local target_dir="$intermediate_dir/$target"
|
||||
if [ -n "$cpu" ]; then
|
||||
env ZIG_CPU="$cpu" ./Script/build-ghostty.sh "$SOURCE_DIR" "$target" "$target_dir"
|
||||
else
|
||||
./Script/build-ghostty.sh "$SOURCE_DIR" "$target" "$target_dir"
|
||||
fi
|
||||
libraries+=("$target_dir/lib/libghostty.a")
|
||||
if [ -z "$first_headers" ]; then
|
||||
first_headers="$target_dir/include"
|
||||
fi
|
||||
done
|
||||
|
||||
if [ -z "$first_headers" ]; then
|
||||
echo "[!] no libraries were built for variant: $variant_name"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
cp -R "$first_headers/." "$variant_dir/include/"
|
||||
|
||||
if [ "${#libraries[@]}" -eq 1 ]; then
|
||||
cp "${libraries[0]}" "$variant_dir/lib/libghostty.a"
|
||||
else
|
||||
lipo -create "${libraries[@]}" -output "$variant_dir/lib/libghostty.a"
|
||||
fi
|
||||
|
||||
echo "[*] assembled variant: $variant_name"
|
||||
}
|
||||
|
||||
mkdir -p "$OUTPUT_DIR"
|
||||
|
||||
case "$PLATFORM_GROUP" in
|
||||
macos)
|
||||
build_variant "macosx" \
|
||||
"aarch64-macos" \
|
||||
"arm64e-macos@apple_a12" \
|
||||
"x86_64-macos"
|
||||
;;
|
||||
ios)
|
||||
build_variant "iphoneos" \
|
||||
"aarch64-ios" \
|
||||
"arm64e-ios@apple_a12"
|
||||
build_variant "iphonesimulator" \
|
||||
"aarch64-ios-simulator@apple_a17" \
|
||||
"x86_64-ios-simulator"
|
||||
;;
|
||||
maccatalyst)
|
||||
build_variant "maccatalyst" \
|
||||
"aarch64-ios-macabi@apple_a17" \
|
||||
"arm64e-ios-macabi@apple_a17" \
|
||||
"x86_64-ios-macabi"
|
||||
;;
|
||||
tvos)
|
||||
build_variant "appletvos" \
|
||||
"aarch64-tvos"
|
||||
build_variant "appletvsimulator" \
|
||||
"aarch64-tvos-simulator@apple_a17" \
|
||||
"x86_64-tvos-simulator"
|
||||
;;
|
||||
visionos)
|
||||
build_variant "xros" \
|
||||
"aarch64-visionos"
|
||||
build_variant "xrsimulator" \
|
||||
"aarch64-visionos-simulator@apple_a17" \
|
||||
"x86_64-visionos-simulator"
|
||||
;;
|
||||
watchos)
|
||||
build_variant "watchos" \
|
||||
"aarch64-watchos"
|
||||
build_variant "watchsimulator" \
|
||||
"aarch64-watchos-simulator@apple_a17" \
|
||||
"x86_64-watchos-simulator"
|
||||
;;
|
||||
*)
|
||||
echo "[!] unknown platform group: $PLATFORM_GROUP"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
echo "[*] built platform group: $PLATFORM_GROUP"
|
||||
130
vendor/libghostty-spm/Script/build.sh
vendored
Executable file
130
vendor/libghostty-spm/Script/build.sh
vendored
Executable file
@@ -0,0 +1,130 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
cd "$(dirname "$0")/.."
|
||||
|
||||
if [ ! -f .root ]; then
|
||||
echo "[*] malformed project structure"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
usage() {
|
||||
cat <<'EOF'
|
||||
Usage: ./build.sh [options]
|
||||
|
||||
Options:
|
||||
--source <path> Use an existing Ghostty checkout.
|
||||
--ref <tag-or-commit> Checkout the given ref in the source checkout.
|
||||
--platforms <csv> Build platform groups. Default:
|
||||
macos,ios,maccatalyst
|
||||
--download-url <url> Generate Package.swift from Package.swift.template.
|
||||
--skip-tests Skip local xcodebuild and swift test verification.
|
||||
-h, --help Show this help.
|
||||
|
||||
Notes:
|
||||
- Default source path is ./References/ghostty-upstream
|
||||
- This builds real per-target static archives, then assembles
|
||||
BinaryTarget/GhosttyKit.xcframework and build/GhosttyKit.xcframework.zip
|
||||
- Upstream Ghostty patches from ./Patches/ghostty are applied automatically
|
||||
- Current verified groups: macos, ios, maccatalyst
|
||||
- Current upstream Ghostty crashes for: tvos, visionos, watchos
|
||||
EOF
|
||||
}
|
||||
|
||||
ROOT_DIR=$(pwd)
|
||||
SOURCE_DIR="$ROOT_DIR/References/ghostty-upstream"
|
||||
PLATFORMS="macos,ios,maccatalyst"
|
||||
DOWNLOAD_URL=${DOWNLOAD_URL:-}
|
||||
GHOSTTY_REF=
|
||||
SKIP_TESTS=0
|
||||
|
||||
while [ $# -gt 0 ]; do
|
||||
case "$1" in
|
||||
--source)
|
||||
SOURCE_DIR="$2"
|
||||
shift 2
|
||||
;;
|
||||
--ref)
|
||||
GHOSTTY_REF="$2"
|
||||
shift 2
|
||||
;;
|
||||
--platforms)
|
||||
PLATFORMS="$2"
|
||||
shift 2
|
||||
;;
|
||||
--download-url)
|
||||
DOWNLOAD_URL="$2"
|
||||
shift 2
|
||||
;;
|
||||
--skip-tests)
|
||||
SKIP_TESTS=1
|
||||
shift
|
||||
;;
|
||||
-h | --help)
|
||||
usage
|
||||
exit 0
|
||||
;;
|
||||
*)
|
||||
echo "[!] unknown argument: $1"
|
||||
usage
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
if ! command -v zig >/dev/null 2>&1; then
|
||||
echo "[!] zig not found"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ ! -d "$SOURCE_DIR" ]; then
|
||||
echo "[*] ghostty source not found, cloning into $SOURCE_DIR"
|
||||
mkdir -p "$(dirname "$SOURCE_DIR")"
|
||||
git clone https://github.com/ghostty-org/ghostty "$SOURCE_DIR"
|
||||
fi
|
||||
|
||||
if [ -n "$GHOSTTY_REF" ]; then
|
||||
echo "[*] checking out ghostty ref: $GHOSTTY_REF"
|
||||
git -C "$SOURCE_DIR" fetch --tags origin
|
||||
git -C "$SOURCE_DIR" checkout "$GHOSTTY_REF"
|
||||
fi
|
||||
|
||||
ARTIFACTS_DIR="$ROOT_DIR/build/artifacts"
|
||||
XCFRAMEWORK_PATH="$ROOT_DIR/BinaryTarget/GhosttyKit.xcframework"
|
||||
XCFRAMEWORK_ZIP="$ROOT_DIR/build/GhosttyKit.xcframework.zip"
|
||||
|
||||
rm -rf "$ARTIFACTS_DIR" "$XCFRAMEWORK_PATH" "$XCFRAMEWORK_ZIP"
|
||||
mkdir -p "$ARTIFACTS_DIR" "$(dirname "$XCFRAMEWORK_PATH")"
|
||||
|
||||
echo "[*] zig version: $(zig version)"
|
||||
echo "[*] ghostty source: $SOURCE_DIR"
|
||||
echo "[*] platform groups: $PLATFORMS"
|
||||
|
||||
OLD_IFS=$IFS
|
||||
IFS=','
|
||||
set -- $PLATFORMS
|
||||
IFS=$OLD_IFS
|
||||
|
||||
for platform_group in "$@"; do
|
||||
platform_group=$(echo "$platform_group" | xargs)
|
||||
[ -n "$platform_group" ] || continue
|
||||
./Script/build-platform.sh "$SOURCE_DIR" "$platform_group" "$ARTIFACTS_DIR"
|
||||
done
|
||||
|
||||
./Script/merge-xcframework.sh \
|
||||
"$ARTIFACTS_DIR" \
|
||||
"$XCFRAMEWORK_PATH" \
|
||||
"$XCFRAMEWORK_ZIP"
|
||||
|
||||
if [ -n "$DOWNLOAD_URL" ]; then
|
||||
./Script/build-manifest.sh "$XCFRAMEWORK_ZIP" "$DOWNLOAD_URL"
|
||||
fi
|
||||
|
||||
if [ "$SKIP_TESTS" -eq 0 ]; then
|
||||
./Script/test.sh
|
||||
swift test
|
||||
fi
|
||||
|
||||
echo "[*] xcframework: $XCFRAMEWORK_PATH"
|
||||
echo "[*] zip: $XCFRAMEWORK_ZIP"
|
||||
194
vendor/libghostty-spm/Script/convert-archive-arm64e.py
vendored
Executable file
194
vendor/libghostty-spm/Script/convert-archive-arm64e.py
vendored
Executable file
@@ -0,0 +1,194 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
import os
|
||||
import pathlib
|
||||
import struct
|
||||
import sys
|
||||
|
||||
|
||||
AR_MAGIC = b"!<arch>\n"
|
||||
AR_HEADER_SIZE = 60
|
||||
MACHO64_LITTLE_ENDIAN_MAGIC = b"\xcf\xfa\xed\xfe"
|
||||
CPU_TYPE_ARM64 = 0x0100000C
|
||||
CPU_SUBTYPE_ARM64_ALL = 0
|
||||
CPU_SUBTYPE_ARM64E = 0x80000002
|
||||
LC_SYMTAB = 0x2
|
||||
N_TYPE_MASK = 0x0E
|
||||
N_UNDF = 0x0
|
||||
NLIST64_SIZE = 16
|
||||
UNDEFINED_SYMBOL_RENAMES = {
|
||||
b"_pthread_create": b"_pa_pthread_cr",
|
||||
b"_qsort": b"_pa_qs",
|
||||
}
|
||||
|
||||
|
||||
def fail(message: str) -> None:
|
||||
raise SystemExit(f"[-] {message}")
|
||||
|
||||
|
||||
def parse_decimal(raw: bytes, field: str) -> int:
|
||||
try:
|
||||
return int(raw.decode("ascii").strip())
|
||||
except ValueError:
|
||||
fail(f"invalid {field}: {raw!r}")
|
||||
|
||||
|
||||
def rewrite_undefined_symbols(
|
||||
data: bytearray,
|
||||
object_start: int,
|
||||
member_end: int,
|
||||
) -> dict[bytes, int]:
|
||||
object_size = member_end - object_start
|
||||
if object_size < 32:
|
||||
fail(f"truncated Mach-O header at offset {object_start}")
|
||||
|
||||
command_count, command_bytes = struct.unpack_from("<II", data, object_start + 16)
|
||||
command_start = object_start + 32
|
||||
command_end = command_start + command_bytes
|
||||
if command_end > member_end:
|
||||
fail(f"Mach-O load commands cross member boundary at offset {object_start}")
|
||||
|
||||
symtab = None
|
||||
cursor = command_start
|
||||
for _ in range(command_count):
|
||||
if cursor + 8 > command_end:
|
||||
fail(f"truncated Mach-O load command at offset {cursor}")
|
||||
command, command_size = struct.unpack_from("<II", data, cursor)
|
||||
if command_size < 8 or cursor + command_size > command_end:
|
||||
fail(f"invalid Mach-O load command size at offset {cursor}")
|
||||
if command == LC_SYMTAB:
|
||||
if command_size < 24 or symtab is not None:
|
||||
fail(f"invalid Mach-O symbol table command at offset {cursor}")
|
||||
symtab = struct.unpack_from("<IIII", data, cursor + 8)
|
||||
cursor += command_size
|
||||
|
||||
if cursor != command_end:
|
||||
fail(f"Mach-O load command sizes do not match header at offset {object_start}")
|
||||
if symtab is None:
|
||||
return {name: 0 for name in UNDEFINED_SYMBOL_RENAMES}
|
||||
|
||||
symbol_offset, symbol_count, string_offset, string_size = symtab
|
||||
symbol_bytes = symbol_count * NLIST64_SIZE
|
||||
if symbol_offset + symbol_bytes > object_size:
|
||||
fail(f"Mach-O symbol table crosses member boundary at offset {object_start}")
|
||||
if string_offset + string_size > object_size:
|
||||
fail(f"Mach-O string table crosses member boundary at offset {object_start}")
|
||||
|
||||
string_start = object_start + string_offset
|
||||
string_end = string_start + string_size
|
||||
renamed = {name: 0 for name in UNDEFINED_SYMBOL_RENAMES}
|
||||
|
||||
for index in range(symbol_count):
|
||||
entry = object_start + symbol_offset + index * NLIST64_SIZE
|
||||
string_index = struct.unpack_from("<I", data, entry)[0]
|
||||
symbol_type = data[entry + 4]
|
||||
if string_index == 0 or symbol_type & N_TYPE_MASK != N_UNDF:
|
||||
continue
|
||||
|
||||
name_start = string_start + string_index
|
||||
if name_start >= string_end:
|
||||
fail(f"Mach-O symbol name crosses string table at offset {entry}")
|
||||
name_end = data.find(0, name_start, string_end)
|
||||
if name_end < 0:
|
||||
fail(f"unterminated Mach-O symbol name at offset {name_start}")
|
||||
|
||||
name = bytes(data[name_start:name_end])
|
||||
replacement = UNDEFINED_SYMBOL_RENAMES.get(name)
|
||||
if replacement is None:
|
||||
continue
|
||||
if len(replacement) > len(name):
|
||||
fail(f"replacement symbol is longer than source: {name!r}")
|
||||
|
||||
replacement_size = len(name) + 1
|
||||
data[name_start : name_start + replacement_size] = replacement.ljust(
|
||||
replacement_size,
|
||||
b"\0",
|
||||
)
|
||||
renamed[name] += 1
|
||||
|
||||
return renamed
|
||||
|
||||
|
||||
def convert_archive(source: pathlib.Path, destination: pathlib.Path) -> int:
|
||||
data = bytearray(source.read_bytes())
|
||||
if not data.startswith(AR_MAGIC):
|
||||
fail(f"not a static archive: {source}")
|
||||
|
||||
cursor = len(AR_MAGIC)
|
||||
patched_objects = 0
|
||||
renamed_symbols = {name: 0 for name in UNDEFINED_SYMBOL_RENAMES}
|
||||
|
||||
while cursor < len(data):
|
||||
if cursor + AR_HEADER_SIZE > len(data):
|
||||
fail(f"truncated archive member header at offset {cursor}")
|
||||
|
||||
header = data[cursor : cursor + AR_HEADER_SIZE]
|
||||
if header[58:60] != b"`\n":
|
||||
fail(f"invalid archive member header at offset {cursor}")
|
||||
|
||||
member_size = parse_decimal(header[48:58], "archive member size")
|
||||
member_start = cursor + AR_HEADER_SIZE
|
||||
member_end = member_start + member_size
|
||||
if member_end > len(data):
|
||||
fail(f"archive member crosses file boundary at offset {cursor}")
|
||||
|
||||
object_start = member_start
|
||||
member_name = header[:16].decode("ascii", errors="replace").strip()
|
||||
if member_name.startswith("#1/"):
|
||||
name_size = parse_decimal(member_name[3:].encode("ascii"), "extended member name size")
|
||||
object_start += name_size
|
||||
if object_start > member_end:
|
||||
fail(f"extended member name crosses member boundary at offset {cursor}")
|
||||
|
||||
if data[object_start : object_start + 4] == MACHO64_LITTLE_ENDIAN_MAGIC:
|
||||
if object_start + 12 > member_end:
|
||||
fail(f"truncated Mach-O header at offset {object_start}")
|
||||
|
||||
cpu_type, cpu_subtype = struct.unpack_from("<II", data, object_start + 4)
|
||||
if cpu_type != CPU_TYPE_ARM64:
|
||||
fail(f"unexpected Mach-O CPU type 0x{cpu_type:08x} at offset {object_start}")
|
||||
if cpu_subtype not in {CPU_SUBTYPE_ARM64_ALL, CPU_SUBTYPE_ARM64E}:
|
||||
fail(f"unexpected ARM64 CPU subtype 0x{cpu_subtype:08x} at offset {object_start}")
|
||||
|
||||
struct.pack_into("<I", data, object_start + 8, CPU_SUBTYPE_ARM64E)
|
||||
member_renames = rewrite_undefined_symbols(data, object_start, member_end)
|
||||
for name, count in member_renames.items():
|
||||
renamed_symbols[name] += count
|
||||
patched_objects += 1
|
||||
|
||||
cursor = member_end + (member_size & 1)
|
||||
|
||||
if cursor != len(data):
|
||||
fail(f"archive alignment crosses file boundary: {source}")
|
||||
if patched_objects == 0:
|
||||
fail(f"archive contains no ARM64 Mach-O objects: {source}")
|
||||
|
||||
destination.parent.mkdir(parents=True, exist_ok=True)
|
||||
temporary = destination.with_name(f".{destination.name}.{os.getpid()}.tmp")
|
||||
temporary.write_bytes(data)
|
||||
os.replace(temporary, destination)
|
||||
for name, replacement in UNDEFINED_SYMBOL_RENAMES.items():
|
||||
print(
|
||||
f"[+] redirected {renamed_symbols[name]} {name.decode()} imports "
|
||||
f"to {replacement.decode()}"
|
||||
)
|
||||
return patched_objects
|
||||
|
||||
|
||||
def main() -> None:
|
||||
if len(sys.argv) != 3:
|
||||
fail(f"usage: {sys.argv[0]} <arm64_archive> <arm64e_archive>")
|
||||
|
||||
source = pathlib.Path(sys.argv[1])
|
||||
destination = pathlib.Path(sys.argv[2])
|
||||
if not source.is_file():
|
||||
fail(f"archive not found: {source}")
|
||||
if source.resolve() == destination.resolve():
|
||||
fail("source and destination must differ")
|
||||
|
||||
count = convert_archive(source, destination)
|
||||
print(f"[+] converted {count} Mach-O objects to arm64e: {destination}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
195
vendor/libghostty-spm/Script/generate-themes.sh
vendored
Executable file
195
vendor/libghostty-spm/Script/generate-themes.sh
vendored
Executable file
@@ -0,0 +1,195 @@
|
||||
#!/bin/zsh
|
||||
set -euo pipefail
|
||||
|
||||
# Downloads all Ghostty theme files from iTerm2-Color-Schemes and generates
|
||||
# Swift source files for the GhosttyTheme library.
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
ROOT_DIR="$(dirname "$SCRIPT_DIR")"
|
||||
OUTPUT_DIR="$ROOT_DIR/Sources/GhosttyTheme/Themes"
|
||||
TEMP_DIR="$(mktemp -d)"
|
||||
|
||||
trap 'rm -rf "$TEMP_DIR"' EXIT
|
||||
|
||||
mkdir -p "$OUTPUT_DIR"
|
||||
|
||||
# Fetch the list of theme files from GitHub API
|
||||
echo "[+] fetching theme list from github api"
|
||||
curl -sL "https://api.github.com/repos/mbadolato/iTerm2-Color-Schemes/contents/ghostty" \
|
||||
| python3 -c "
|
||||
import json, sys
|
||||
data = json.load(sys.stdin)
|
||||
for item in data:
|
||||
if item['type'] == 'file':
|
||||
print(item['name'])
|
||||
" > "$TEMP_DIR/theme_list.txt"
|
||||
|
||||
THEME_COUNT=$(wc -l < "$TEMP_DIR/theme_list.txt" | tr -d ' ')
|
||||
echo "[+] found $THEME_COUNT themes"
|
||||
|
||||
# Download all themes
|
||||
echo "[+] downloading themes"
|
||||
mkdir -p "$TEMP_DIR/themes"
|
||||
while IFS= read -r name; do
|
||||
encoded_name=$(python3 -c "import urllib.parse; print(urllib.parse.quote('$name'))")
|
||||
curl -sL "https://raw.githubusercontent.com/mbadolato/iTerm2-Color-Schemes/master/ghostty/$encoded_name" \
|
||||
-o "$TEMP_DIR/themes/$name" &
|
||||
# Limit concurrent downloads
|
||||
if (( $(jobs -r | wc -l) >= 20 )); then
|
||||
wait -n
|
||||
fi
|
||||
done < "$TEMP_DIR/theme_list.txt"
|
||||
wait
|
||||
echo "[+] downloaded all themes"
|
||||
|
||||
# Generate Swift files using Python
|
||||
echo "[+] generating swift source files"
|
||||
python3 - "$TEMP_DIR/themes" "$OUTPUT_DIR" "$TEMP_DIR/theme_list.txt" << 'PYEOF'
|
||||
import os, sys, re, string
|
||||
|
||||
themes_dir = sys.argv[1]
|
||||
output_dir = sys.argv[2]
|
||||
list_file = sys.argv[3]
|
||||
|
||||
with open(list_file) as f:
|
||||
theme_names = [line.strip() for line in f if line.strip()]
|
||||
|
||||
def parse_theme(path):
|
||||
result = {}
|
||||
palette = {}
|
||||
with open(path) as f:
|
||||
for line in f:
|
||||
line = line.strip()
|
||||
if not line or line.startswith('#'):
|
||||
continue
|
||||
if '=' not in line:
|
||||
continue
|
||||
key, _, value = line.partition('=')
|
||||
key = key.strip()
|
||||
value = value.strip()
|
||||
if key == 'palette':
|
||||
# format: N=#RRGGBB or N=RRGGBB
|
||||
idx_str, _, color = value.partition('=')
|
||||
idx = int(idx_str.strip())
|
||||
color = color.strip().lstrip('#')
|
||||
palette[idx] = color
|
||||
else:
|
||||
# strip # from color values
|
||||
clean_val = value.lstrip('#')
|
||||
result[key] = clean_val
|
||||
result['_palette'] = palette
|
||||
return result
|
||||
|
||||
def swift_identifier(name):
|
||||
"""Convert theme name to a valid Swift identifier."""
|
||||
# Replace + with Plus before stripping
|
||||
ident = name.replace('+', 'Plus')
|
||||
# Replace non-alphanumeric with underscore
|
||||
ident = re.sub(r'[^a-zA-Z0-9]', '_', ident)
|
||||
# Collapse multiple underscores
|
||||
ident = re.sub(r'_+', '_', ident)
|
||||
# Strip leading/trailing underscores
|
||||
ident = ident.strip('_')
|
||||
# Prefix with underscore if starts with digit
|
||||
if ident and ident[0].isdigit():
|
||||
ident = '_' + ident
|
||||
# camelCase: lowercase first char
|
||||
if ident:
|
||||
ident = ident[0].lower() + ident[1:]
|
||||
return ident
|
||||
|
||||
def swift_string(s):
|
||||
return '"' + s.replace('\\', '\\\\').replace('"', '\\"') + '"'
|
||||
|
||||
def optional_field(data, ghostty_key, swift_type="String"):
|
||||
val = data.get(ghostty_key)
|
||||
if val:
|
||||
return swift_string(val)
|
||||
return "nil"
|
||||
|
||||
# Group themes by first letter
|
||||
groups = {}
|
||||
all_themes = []
|
||||
|
||||
for name in sorted(theme_names, key=str.lower):
|
||||
path = os.path.join(themes_dir, name)
|
||||
if not os.path.isfile(path):
|
||||
continue
|
||||
try:
|
||||
data = parse_theme(path)
|
||||
except Exception as e:
|
||||
print(f" [-] skipping {name}: {e}", file=sys.stderr)
|
||||
continue
|
||||
|
||||
bg = data.get('background')
|
||||
fg = data.get('foreground')
|
||||
if not bg or not fg:
|
||||
print(f" [-] skipping {name}: missing background/foreground", file=sys.stderr)
|
||||
continue
|
||||
|
||||
ident = swift_identifier(name)
|
||||
first_char = name[0].upper()
|
||||
if first_char not in string.ascii_uppercase:
|
||||
first_char = 'Symbols'
|
||||
|
||||
palette = data.get('_palette', {})
|
||||
palette_str = '['
|
||||
for i in sorted(palette.keys()):
|
||||
palette_str += f'{i}: {swift_string(palette[i])}, '
|
||||
if palette_str.endswith(', '):
|
||||
palette_str = palette_str[:-2]
|
||||
palette_str += ']'
|
||||
|
||||
entry = {
|
||||
'name': name,
|
||||
'ident': ident,
|
||||
'background': swift_string(bg),
|
||||
'foreground': swift_string(fg),
|
||||
'cursorColor': optional_field(data, 'cursor-color'),
|
||||
'cursorText': optional_field(data, 'cursor-text'),
|
||||
'selectionBackground': optional_field(data, 'selection-background'),
|
||||
'selectionForeground': optional_field(data, 'selection-foreground'),
|
||||
'palette': palette_str,
|
||||
'group': first_char,
|
||||
}
|
||||
|
||||
groups.setdefault(first_char, []).append(entry)
|
||||
all_themes.append(entry)
|
||||
|
||||
# Write per-group Swift files
|
||||
for group, entries in sorted(groups.items()):
|
||||
filename = f"Themes_{group}.swift"
|
||||
filepath = os.path.join(output_dir, filename)
|
||||
with open(filepath, 'w') as f:
|
||||
f.write("// Auto-generated by Script/generate-themes.sh — do not edit\n\n")
|
||||
f.write("public extension GhosttyThemeDefinition {\n")
|
||||
for e in entries:
|
||||
f.write(f" static let {e['ident']} = GhosttyThemeDefinition(\n")
|
||||
f.write(f" name: {swift_string(e['name'])},\n")
|
||||
f.write(f" background: {e['background']},\n")
|
||||
f.write(f" foreground: {e['foreground']},\n")
|
||||
f.write(f" cursorColor: {e['cursorColor']},\n")
|
||||
f.write(f" cursorText: {e['cursorText']},\n")
|
||||
f.write(f" selectionBackground: {e['selectionBackground']},\n")
|
||||
f.write(f" selectionForeground: {e['selectionForeground']},\n")
|
||||
f.write(f" palette: {e['palette']}\n")
|
||||
f.write(f" )\n\n")
|
||||
f.write("}\n")
|
||||
print(f" [+] wrote {filename} ({len(entries)} themes)")
|
||||
|
||||
# Write catalog file
|
||||
catalog_path = os.path.join(output_dir, "ThemeCatalog_Generated.swift")
|
||||
with open(catalog_path, 'w') as f:
|
||||
f.write("// Auto-generated by Script/generate-themes.sh — do not edit\n\n")
|
||||
f.write("public extension GhosttyThemeCatalog {\n")
|
||||
f.write(" static let allThemes: [GhosttyThemeDefinition] = [\n")
|
||||
for e in all_themes:
|
||||
f.write(f" .{e['ident']},\n")
|
||||
f.write(" ]\n")
|
||||
f.write("}\n")
|
||||
print(f" [+] wrote ThemeCatalog_Generated.swift ({len(all_themes)} themes)")
|
||||
|
||||
print(f"[+] generated {len(all_themes)} themes total")
|
||||
PYEOF
|
||||
|
||||
echo "[+] done"
|
||||
93
vendor/libghostty-spm/Script/merge-xcframework.sh
vendored
Executable file
93
vendor/libghostty-spm/Script/merge-xcframework.sh
vendored
Executable file
@@ -0,0 +1,93 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
cd "$(dirname "$0")/.."
|
||||
if [ ! -f .root ]; then
|
||||
echo "[*] malformed project structure"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
ARTIFACTS_DIR=${1:-}
|
||||
OUTPUT_XCFRAMEWORK=${2:-}
|
||||
OUTPUT_ZIP=${3:-}
|
||||
|
||||
if [ -z "$ARTIFACTS_DIR" ] || [ -z "$OUTPUT_XCFRAMEWORK" ]; then
|
||||
echo "Usage: $0 <artifacts_dir> <output_xcframework> [output_zip]"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ ! -d "$ARTIFACTS_DIR" ]; then
|
||||
echo "[!] artifacts directory not found: $ARTIFACTS_DIR"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
XCFRAMEWORK_COMMAND=()
|
||||
FOUND_VARIANTS=0
|
||||
|
||||
verify_headers() {
|
||||
local header_dir="$1"
|
||||
|
||||
if [ ! -f "$header_dir/ghostty.h" ]; then
|
||||
echo "[!] missing header: $header_dir/ghostty.h"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ ! -f "$header_dir/module.modulemap" ]; then
|
||||
echo "[!] missing module map: $header_dir/module.modulemap"
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
add_library() {
|
||||
local variant_dir="$1"
|
||||
local library_path="$variant_dir/lib/libghostty.a"
|
||||
local header_dir="$variant_dir/include"
|
||||
|
||||
if [ ! -f "$library_path" ]; then
|
||||
echo "[!] missing library: $library_path"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ ! -d "$header_dir" ]; then
|
||||
echo "[!] missing headers: $header_dir"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
verify_headers "$header_dir"
|
||||
XCFRAMEWORK_COMMAND+=("-library" "$library_path" "-headers" "$header_dir")
|
||||
FOUND_VARIANTS=$((FOUND_VARIANTS + 1))
|
||||
}
|
||||
|
||||
for variant_dir in "$ARTIFACTS_DIR"/*; do
|
||||
[ -d "$variant_dir" ] || continue
|
||||
[ "$(basename "$variant_dir")" = ".intermediates" ] && continue
|
||||
add_library "$variant_dir"
|
||||
done
|
||||
|
||||
if [ "$FOUND_VARIANTS" -eq 0 ]; then
|
||||
echo "[!] no staged variants found in $ARTIFACTS_DIR"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
mkdir -p "$(dirname "$OUTPUT_XCFRAMEWORK")"
|
||||
rm -rf "$OUTPUT_XCFRAMEWORK"
|
||||
|
||||
echo "[*] creating static-library xcframework: $OUTPUT_XCFRAMEWORK"
|
||||
xcodebuild -create-xcframework \
|
||||
-output "$OUTPUT_XCFRAMEWORK" \
|
||||
"${XCFRAMEWORK_COMMAND[@]}"
|
||||
|
||||
./Script/verify-xcframework.sh "$OUTPUT_XCFRAMEWORK"
|
||||
|
||||
if [ -n "$OUTPUT_ZIP" ]; then
|
||||
mkdir -p "$(dirname "$OUTPUT_ZIP")"
|
||||
rm -f "$OUTPUT_ZIP"
|
||||
(
|
||||
cd "$(dirname "$OUTPUT_XCFRAMEWORK")"
|
||||
ditto -c -k --sequesterRsrc --keepParent "$(basename "$OUTPUT_XCFRAMEWORK")" "$(basename "$OUTPUT_ZIP")"
|
||||
)
|
||||
mv "$(dirname "$OUTPUT_XCFRAMEWORK")/$(basename "$OUTPUT_ZIP")" "$OUTPUT_ZIP"
|
||||
./Script/verify-xcframework.sh "$OUTPUT_ZIP"
|
||||
echo "[*] packed xcframework zip: $OUTPUT_ZIP"
|
||||
fi
|
||||
53
vendor/libghostty-spm/Script/prepare-arm64e-source.sh
vendored
Executable file
53
vendor/libghostty-spm/Script/prepare-arm64e-source.sh
vendored
Executable file
@@ -0,0 +1,53 @@
|
||||
#!/bin/zsh
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
cd "$(dirname "$0")/.."
|
||||
|
||||
if [ ! -f .root ]; then
|
||||
echo "[-] malformed project structure"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
SOURCE_DIR=${1:-}
|
||||
GLOBAL_CACHE_DIR=${2:-}
|
||||
|
||||
if [ -z "$SOURCE_DIR" ] || [ -z "$GLOBAL_CACHE_DIR" ]; then
|
||||
echo "Usage: $0 <source_dir> <zig_global_cache_dir>"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ ! -d "$SOURCE_DIR" ]; then
|
||||
echo "[-] ghostty source directory not found: $SOURCE_DIR"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
ZIG_OBJC_URL="https://deps.files.ghostty.org/zig_objc-f356ed02833f0f1b8e84d50bed9e807bf7cdc0ae.tar.gz"
|
||||
ZIG_OBJC_HASH="zig_objc-0.0.0-Ir_Sp5gTAQCvxxR7oVIrPXxXwsfKgVP7_wqoOQrZjFeK"
|
||||
VENDOR_DIR="$SOURCE_DIR/.libghostty-spm/zig_objc"
|
||||
VENDOR_PATCH="$(pwd)/Patches/arm64e/zig-objc/0001-arm64e-boundaries.patch"
|
||||
VENDOR_PATCH_MARKER="$VENDOR_DIR/.libghostty-spm-arm64e-patched"
|
||||
|
||||
./Script/apply-patches.sh "$SOURCE_DIR" "$(pwd)/Patches/arm64e/ghostty"
|
||||
|
||||
if [ ! -f "$VENDOR_DIR/src/block.zig" ]; then
|
||||
fetched_hash=$(zig fetch --global-cache-dir "$GLOBAL_CACHE_DIR" "$ZIG_OBJC_URL")
|
||||
if [ "$fetched_hash" != "$ZIG_OBJC_HASH" ]; then
|
||||
echo "[-] zig-objc hash mismatch: expected $ZIG_OBJC_HASH, got $fetched_hash"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
mkdir -p "$(dirname "$VENDOR_DIR")"
|
||||
cp -R "$GLOBAL_CACHE_DIR/p/$ZIG_OBJC_HASH" "$VENDOR_DIR"
|
||||
echo "[+] vendored pinned zig-objc for arm64e"
|
||||
fi
|
||||
|
||||
if [ ! -f "$VENDOR_PATCH_MARKER" ]; then
|
||||
patch -p1 -f -d "$VENDOR_DIR" < "$VENDOR_PATCH" >/dev/null
|
||||
touch "$VENDOR_PATCH_MARKER"
|
||||
echo "[+] applied zig-objc arm64e patch"
|
||||
else
|
||||
echo "[+] zig-objc arm64e patch already applied"
|
||||
fi
|
||||
|
||||
echo "[+] prepared arm64e pointer-authentication boundaries"
|
||||
152
vendor/libghostty-spm/Script/support/arm64e-compat.c
vendored
Normal file
152
vendor/libghostty-spm/Script/support/arm64e-compat.c
vendored
Normal file
@@ -0,0 +1,152 @@
|
||||
#include "ghostty.h"
|
||||
|
||||
#include <stdbool.h>
|
||||
#include <stdint.h>
|
||||
|
||||
#if defined(__arm64e__)
|
||||
#include <pthread.h>
|
||||
#include <ptrauth.h>
|
||||
#include <stdatomic.h>
|
||||
#include <stdlib.h>
|
||||
|
||||
enum {
|
||||
ghostty_block_has_copy_dispose = 1 << 25,
|
||||
ghostty_objc_isa_discriminator = 0x6AE1,
|
||||
};
|
||||
|
||||
typedef struct {
|
||||
uintptr_t reserved;
|
||||
uintptr_t size;
|
||||
void *copy_helper;
|
||||
void *dispose_helper;
|
||||
} ghostty_block_descriptor_s;
|
||||
|
||||
typedef struct {
|
||||
void *isa;
|
||||
int flags;
|
||||
int reserved;
|
||||
void *invoke;
|
||||
ghostty_block_descriptor_s *descriptor;
|
||||
} ghostty_block_s;
|
||||
|
||||
static atomic_flag ghostty_descriptor_lock = ATOMIC_FLAG_INIT;
|
||||
|
||||
static void *ghostty_sign_function_pointer(void *pointer,
|
||||
uintptr_t discriminator) {
|
||||
void *raw = ptrauth_strip(pointer, ptrauth_key_function_pointer);
|
||||
return ptrauth_sign_unauthenticated(
|
||||
raw, ptrauth_key_function_pointer, discriminator);
|
||||
}
|
||||
|
||||
static void *ghostty_sign_objc_isa_pointer(void *pointer,
|
||||
uintptr_t discriminator) {
|
||||
void *raw = ptrauth_strip(pointer, ptrauth_key_objc_isa_pointer);
|
||||
return ptrauth_sign_unauthenticated(
|
||||
raw, ptrauth_key_objc_isa_pointer, discriminator);
|
||||
}
|
||||
|
||||
static void ghostty_prepare_block_descriptor(ghostty_block_descriptor_s *descriptor) {
|
||||
while (atomic_flag_test_and_set_explicit(&ghostty_descriptor_lock,
|
||||
memory_order_acquire)) {
|
||||
}
|
||||
|
||||
descriptor->copy_helper = ghostty_sign_function_pointer(
|
||||
descriptor->copy_helper,
|
||||
(uintptr_t)&descriptor->copy_helper);
|
||||
descriptor->dispose_helper = ghostty_sign_function_pointer(
|
||||
descriptor->dispose_helper,
|
||||
(uintptr_t)&descriptor->dispose_helper);
|
||||
|
||||
atomic_flag_clear_explicit(&ghostty_descriptor_lock, memory_order_release);
|
||||
}
|
||||
|
||||
const void *ghostty_arm64e_sign_function(const void *pointer) {
|
||||
return ghostty_sign_function_pointer((void *)pointer, 0);
|
||||
}
|
||||
|
||||
void ghostty_arm64e_prepare_block(void *raw_block) {
|
||||
ghostty_block_s *block = raw_block;
|
||||
|
||||
if ((block->flags & ghostty_block_has_copy_dispose) != 0) {
|
||||
ghostty_prepare_block_descriptor(block->descriptor);
|
||||
}
|
||||
|
||||
block->isa = ghostty_sign_objc_isa_pointer(
|
||||
block->isa,
|
||||
ptrauth_blend_discriminator(&block->isa, ghostty_objc_isa_discriminator));
|
||||
block->invoke = ghostty_sign_function_pointer(
|
||||
block->invoke,
|
||||
(uintptr_t)&block->invoke);
|
||||
|
||||
#if __has_feature(ptrauth_signed_block_descriptors)
|
||||
block->descriptor = ptrauth_sign_unauthenticated(
|
||||
ptrauth_strip(block->descriptor, ptrauth_key_block_descriptor_pointer),
|
||||
ptrauth_key_block_descriptor_pointer,
|
||||
ptrauth_blend_discriminator(&block->descriptor, 0xC0BB));
|
||||
#endif
|
||||
}
|
||||
|
||||
__attribute__((visibility("hidden"))) int pa_pthread_cr(
|
||||
pthread_t *thread,
|
||||
const pthread_attr_t *attributes,
|
||||
void *(*start_routine)(void *),
|
||||
void *argument) {
|
||||
start_routine = ghostty_sign_function_pointer((void *)start_routine, 0);
|
||||
return pthread_create(thread, attributes, start_routine, argument);
|
||||
}
|
||||
|
||||
__attribute__((visibility("hidden"))) void pa_qs(
|
||||
void *base,
|
||||
size_t count,
|
||||
size_t size,
|
||||
int (*comparator)(const void *, const void *)) {
|
||||
comparator = ghostty_sign_function_pointer((void *)comparator, 0);
|
||||
qsort(base, count, size, comparator);
|
||||
}
|
||||
|
||||
extern ghostty_app_t ghostty_arm64e_core_app_new(
|
||||
const ghostty_runtime_config_s *,
|
||||
ghostty_config_t);
|
||||
extern ghostty_surface_t ghostty_arm64e_core_surface_new(
|
||||
ghostty_app_t,
|
||||
const ghostty_surface_config_s *);
|
||||
|
||||
GHOSTTY_API ghostty_app_t ghostty_app_new(
|
||||
const ghostty_runtime_config_s *config,
|
||||
ghostty_config_t ghostty_config) {
|
||||
ghostty_runtime_config_s raw = *config;
|
||||
raw.wakeup_cb = ptrauth_strip(raw.wakeup_cb, ptrauth_key_function_pointer);
|
||||
raw.action_cb = ptrauth_strip(raw.action_cb, ptrauth_key_function_pointer);
|
||||
raw.read_clipboard_cb =
|
||||
ptrauth_strip(raw.read_clipboard_cb, ptrauth_key_function_pointer);
|
||||
raw.confirm_read_clipboard_cb =
|
||||
ptrauth_strip(raw.confirm_read_clipboard_cb, ptrauth_key_function_pointer);
|
||||
raw.write_clipboard_cb =
|
||||
ptrauth_strip(raw.write_clipboard_cb, ptrauth_key_function_pointer);
|
||||
raw.close_surface_cb =
|
||||
ptrauth_strip(raw.close_surface_cb, ptrauth_key_function_pointer);
|
||||
return ghostty_arm64e_core_app_new(&raw, ghostty_config);
|
||||
}
|
||||
|
||||
GHOSTTY_API ghostty_surface_t ghostty_surface_new(
|
||||
ghostty_app_t app,
|
||||
const ghostty_surface_config_s *config) {
|
||||
ghostty_surface_config_s raw = *config;
|
||||
raw.receive_buffer =
|
||||
ptrauth_strip(raw.receive_buffer, ptrauth_key_function_pointer);
|
||||
raw.receive_resize =
|
||||
ptrauth_strip(raw.receive_resize, ptrauth_key_function_pointer);
|
||||
return ghostty_arm64e_core_surface_new(app, &raw);
|
||||
}
|
||||
|
||||
#else
|
||||
|
||||
const void *ghostty_arm64e_sign_function(const void *pointer) {
|
||||
return pointer;
|
||||
}
|
||||
|
||||
void ghostty_arm64e_prepare_block(void *block) {
|
||||
(void)block;
|
||||
}
|
||||
|
||||
#endif
|
||||
18
vendor/libghostty-spm/Script/support/libcxx-verbose-abort-compat.c
vendored
Normal file
18
vendor/libghostty-spm/Script/support/libcxx-verbose-abort-compat.c
vendored
Normal file
@@ -0,0 +1,18 @@
|
||||
// libc++'s std::__1::__libcpp_verbose_abort is only exported by the Apple
|
||||
// system libc++.1.dylib since iOS 16.3 / macOS 13.3 / tvOS 16.3. Zig's
|
||||
// bundled libc++ headers reference it unconditionally (no Apple availability
|
||||
// markup), so apps linking libghostty.a crash at launch on older OS versions
|
||||
// with dyld "Symbol missing". Shipping the definition inside the archive
|
||||
// resolves the reference at static link time instead.
|
||||
//
|
||||
// Header-free on purpose: zig cc has no Apple sysroot in this build.
|
||||
|
||||
__attribute__((noreturn)) void abort(void);
|
||||
|
||||
__attribute__((visibility("hidden"), noreturn))
|
||||
void libghostty_libcpp_verbose_abort(const char *format, ...) __asm__("__ZNSt3__122__libcpp_verbose_abortEPKcz");
|
||||
|
||||
void libghostty_libcpp_verbose_abort(const char *format, ...) {
|
||||
(void)format;
|
||||
abort();
|
||||
}
|
||||
80
vendor/libghostty-spm/Script/tag-release.sh
vendored
Executable file
80
vendor/libghostty-spm/Script/tag-release.sh
vendored
Executable file
@@ -0,0 +1,80 @@
|
||||
#!/bin/zsh
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
cd "$(dirname "$0")/.."
|
||||
|
||||
if [ ! -f .root ]; then
|
||||
echo "[-] malformed project structure"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
usage() {
|
||||
cat <<'EOF'
|
||||
Usage: ./Script/tag-release.sh [options]
|
||||
|
||||
Options:
|
||||
--push push the new tag to origin after creating it
|
||||
--suffix <value> override the ci-style release suffix
|
||||
-h, --help show this help
|
||||
|
||||
Notes:
|
||||
- release tags follow the ci pattern: 1.0.<suffix>
|
||||
- this script tags swift source releases only
|
||||
- this script does not create storage.* tags or update binaries
|
||||
EOF
|
||||
}
|
||||
|
||||
PUSH_TAG=0
|
||||
RELEASE_SUFFIX=
|
||||
|
||||
while [ $# -gt 0 ]; do
|
||||
case "$1" in
|
||||
--push)
|
||||
PUSH_TAG=1
|
||||
shift
|
||||
;;
|
||||
--suffix)
|
||||
RELEASE_SUFFIX="$2"
|
||||
shift 2
|
||||
;;
|
||||
-h | --help)
|
||||
usage
|
||||
exit 0
|
||||
;;
|
||||
*)
|
||||
echo "[-] unknown argument: $1"
|
||||
usage
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
if [ -z "$RELEASE_SUFFIX" ]; then
|
||||
RELEASE_SUFFIX=$(date -u +%s)
|
||||
fi
|
||||
|
||||
RELEASE_TAG="1.0.$RELEASE_SUFFIX"
|
||||
RELEASE_DATE=$(date -u +%Y-%m-%d)
|
||||
HEAD_SHA=$(git rev-parse --short=12 HEAD)
|
||||
|
||||
if git rev-parse "$RELEASE_TAG" >/dev/null 2>&1; then
|
||||
echo "[-] tag already exists: $RELEASE_TAG"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
TAG_MESSAGE=$(cat <<EOF
|
||||
Swift source release $RELEASE_DATE
|
||||
|
||||
Tag: $RELEASE_TAG
|
||||
Commit: $HEAD_SHA
|
||||
EOF
|
||||
)
|
||||
|
||||
git tag -a "$RELEASE_TAG" -m "$TAG_MESSAGE"
|
||||
echo "[+] created tag $RELEASE_TAG for $HEAD_SHA on $RELEASE_DATE"
|
||||
|
||||
if [ "$PUSH_TAG" -eq 1 ]; then
|
||||
git push origin "$RELEASE_TAG"
|
||||
echo "[+] pushed tag $RELEASE_TAG"
|
||||
fi
|
||||
288
vendor/libghostty-spm/Script/test-xcframework-consumer.sh
vendored
Executable file
288
vendor/libghostty-spm/Script/test-xcframework-consumer.sh
vendored
Executable file
@@ -0,0 +1,288 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
cd "$(dirname "$0")/.."
|
||||
if [ ! -f .root ]; then
|
||||
echo "[*] malformed project structure"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
XCFRAMEWORK_PATH=${1:-BinaryTarget/GhosttyKit.xcframework}
|
||||
|
||||
if [ ! -d "$XCFRAMEWORK_PATH" ]; then
|
||||
echo "[!] xcframework not found: $XCFRAMEWORK_PATH"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
format_output() {
|
||||
if command -v xcbeautify >/dev/null 2>&1; then
|
||||
xcbeautify
|
||||
else
|
||||
cat
|
||||
fi
|
||||
}
|
||||
|
||||
WORK_DIR=$(mktemp -d)
|
||||
trap 'rm -rf "$WORK_DIR"' EXIT
|
||||
|
||||
mkdir -p "$WORK_DIR/Consumer/BinaryTarget" "$WORK_DIR/Consumer/Sources/Consumer"
|
||||
ditto "$XCFRAMEWORK_PATH" "$WORK_DIR/Consumer/BinaryTarget/GhosttyKit.xcframework"
|
||||
|
||||
cat >"$WORK_DIR/Consumer/Package.swift" <<'EOF'
|
||||
// swift-tools-version: 6.0
|
||||
import PackageDescription
|
||||
|
||||
let package = Package(
|
||||
name: "Consumer",
|
||||
platforms: [
|
||||
.iOS(.v15),
|
||||
.macOS(.v13),
|
||||
.macCatalyst(.v15),
|
||||
],
|
||||
products: [
|
||||
.library(name: "Consumer", targets: ["Consumer"]),
|
||||
],
|
||||
targets: [
|
||||
.binaryTarget(
|
||||
name: "libghostty",
|
||||
path: "BinaryTarget/GhosttyKit.xcframework"
|
||||
),
|
||||
.target(
|
||||
name: "Consumer",
|
||||
dependencies: ["libghostty"],
|
||||
linkerSettings: [
|
||||
.linkedLibrary("c++"),
|
||||
.linkedFramework("Carbon", .when(platforms: [.macOS])),
|
||||
]
|
||||
),
|
||||
]
|
||||
)
|
||||
EOF
|
||||
|
||||
cat >"$WORK_DIR/Consumer/Sources/Consumer/Consumer.swift" <<'EOF'
|
||||
import libghostty
|
||||
|
||||
public func passThroughPlatform(_ platform: ghostty_platform_e) -> ghostty_platform_e {
|
||||
platform
|
||||
}
|
||||
EOF
|
||||
|
||||
ARM64E_LINK_SOURCE="$WORK_DIR/arm64e-link.c"
|
||||
cat >"$ARM64E_LINK_SOURCE" <<'EOF'
|
||||
#include "ghostty.h"
|
||||
|
||||
#include <pthread.h>
|
||||
#include <ptrauth.h>
|
||||
#include <stdlib.h>
|
||||
|
||||
extern int pa_pthread_cr(
|
||||
pthread_t *,
|
||||
const pthread_attr_t *,
|
||||
void *(*)(void *),
|
||||
void *);
|
||||
extern void pa_qs(
|
||||
void *,
|
||||
size_t,
|
||||
size_t,
|
||||
int (*)(const void *, const void *));
|
||||
|
||||
static void wakeup(void *userdata) {
|
||||
(void)userdata;
|
||||
}
|
||||
|
||||
static void *thread_main(void *context) {
|
||||
int *value = context;
|
||||
*value = 42;
|
||||
return NULL;
|
||||
}
|
||||
|
||||
static int compare_ints(const void *lhs, const void *rhs) {
|
||||
const int left = *(const int *)lhs;
|
||||
const int right = *(const int *)rhs;
|
||||
return (left > right) - (left < right);
|
||||
}
|
||||
|
||||
int main(void) {
|
||||
if (ghostty_init(0, NULL) != GHOSTTY_SUCCESS) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
ghostty_config_t config = ghostty_config_new();
|
||||
if (config == NULL) {
|
||||
return 2;
|
||||
}
|
||||
ghostty_config_finalize(config);
|
||||
|
||||
ghostty_runtime_config_s runtime = {0};
|
||||
runtime.wakeup_cb = wakeup;
|
||||
ghostty_app_t app = ghostty_app_new(&runtime, config);
|
||||
if (app == NULL) {
|
||||
ghostty_config_free(config);
|
||||
return 3;
|
||||
}
|
||||
|
||||
int thread_value = 0;
|
||||
pthread_t thread;
|
||||
void *(*raw_thread_main)(void *) =
|
||||
ptrauth_strip(&thread_main, ptrauth_key_function_pointer);
|
||||
if (pa_pthread_cr(&thread, NULL, raw_thread_main, &thread_value) != 0) {
|
||||
return 4;
|
||||
}
|
||||
if (pthread_join(thread, NULL) != 0 || thread_value != 42) {
|
||||
return 5;
|
||||
}
|
||||
|
||||
int values[] = {3, 1, 2};
|
||||
int (*raw_compare)(const void *, const void *) =
|
||||
ptrauth_strip(&compare_ints, ptrauth_key_function_pointer);
|
||||
pa_qs(values, 3, sizeof(values[0]), raw_compare);
|
||||
if (values[0] != 1 || values[1] != 2 || values[2] != 3) {
|
||||
return 6;
|
||||
}
|
||||
|
||||
ghostty_app_free(app);
|
||||
ghostty_config_free(config);
|
||||
return 0;
|
||||
}
|
||||
EOF
|
||||
|
||||
find_slice_paths() {
|
||||
local platform="$1"
|
||||
local platform_variant="$2"
|
||||
|
||||
python3 - "$XCFRAMEWORK_PATH" "$platform" "$platform_variant" <<'PY'
|
||||
import os
|
||||
import plistlib
|
||||
import sys
|
||||
|
||||
xcframework, expected_platform, expected_variant = sys.argv[1:]
|
||||
expected_variant = expected_variant or None
|
||||
|
||||
with open(os.path.join(xcframework, "Info.plist"), "rb") as handle:
|
||||
libraries = plistlib.load(handle)["AvailableLibraries"]
|
||||
|
||||
for library in libraries:
|
||||
if library.get("SupportedPlatform") != expected_platform:
|
||||
continue
|
||||
if library.get("SupportedPlatformVariant") != expected_variant:
|
||||
continue
|
||||
|
||||
root = os.path.join(xcframework, library["LibraryIdentifier"])
|
||||
print(os.path.join(root, library["LibraryPath"]))
|
||||
print(os.path.join(root, library["HeadersPath"]))
|
||||
break
|
||||
else:
|
||||
raise SystemExit(
|
||||
f"[!] xcframework slice not found: {expected_platform}/{expected_variant}"
|
||||
)
|
||||
PY
|
||||
}
|
||||
|
||||
link_arm64e() {
|
||||
local name="$1"
|
||||
local sdk="$2"
|
||||
local target="$3"
|
||||
local platform="$4"
|
||||
local platform_variant="$5"
|
||||
local slice_paths
|
||||
local library_path
|
||||
local headers_path
|
||||
local output_path="$WORK_DIR/arm64e-link-$name"
|
||||
local command
|
||||
|
||||
slice_paths=$(find_slice_paths "$platform" "$platform_variant")
|
||||
library_path=${slice_paths%%$'\n'*}
|
||||
headers_path=${slice_paths#*$'\n'}
|
||||
command=(
|
||||
xcrun --sdk "$sdk" clang
|
||||
-target "$target"
|
||||
"$ARM64E_LINK_SOURCE"
|
||||
-I "$headers_path"
|
||||
"-Wl,-force_load,$library_path"
|
||||
-lc++
|
||||
-framework Foundation
|
||||
-framework CoreFoundation
|
||||
-framework CoreGraphics
|
||||
-framework CoreText
|
||||
-framework CoreVideo
|
||||
-framework QuartzCore
|
||||
-framework IOSurface
|
||||
-framework Metal
|
||||
-framework MetalKit
|
||||
)
|
||||
|
||||
case "$name" in
|
||||
macos)
|
||||
command+=(-framework Carbon)
|
||||
;;
|
||||
ios)
|
||||
command+=(-framework UIKit)
|
||||
;;
|
||||
maccatalyst)
|
||||
local sdk_path
|
||||
sdk_path=$(xcrun --sdk macosx --show-sdk-path)
|
||||
command+=(
|
||||
-F "$sdk_path/System/iOSSupport/System/Library/Frameworks"
|
||||
-L "$sdk_path/System/iOSSupport/usr/lib"
|
||||
-framework UIKit
|
||||
)
|
||||
;;
|
||||
esac
|
||||
|
||||
command+=(-o "$output_path")
|
||||
echo "[*] full link platform=$name architecture=arm64e"
|
||||
"${command[@]}"
|
||||
|
||||
if [ "$(lipo -archs "$output_path")" != "arm64e" ]; then
|
||||
echo "[!] linked executable is not a single arm64e slice: $output_path"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ "$name" = "macos" ] && [ "$(uname -m)" = "arm64" ]; then
|
||||
echo "[*] runtime smoke platform=macos architecture=arm64e"
|
||||
"$output_path"
|
||||
fi
|
||||
}
|
||||
|
||||
test_build() {
|
||||
local destination="$1"
|
||||
local architecture=${2:-}
|
||||
local command=(
|
||||
xcodebuild
|
||||
-scheme Consumer
|
||||
-destination "$destination"
|
||||
-derivedDataPath "$WORK_DIR/DerivedData"
|
||||
-packageCachePath "$WORK_DIR/PackageCache"
|
||||
)
|
||||
|
||||
if [ -n "$architecture" ]; then
|
||||
command+=("ARCHS=$architecture" "ONLY_ACTIVE_ARCH=YES")
|
||||
fi
|
||||
command+=(build)
|
||||
|
||||
echo "[*] consumer build destination=$destination architecture=${architecture:-default}"
|
||||
"${command[@]}" 2>&1 | format_output
|
||||
local exit_code=${PIPESTATUS[0]}
|
||||
if [ "$exit_code" -ne 0 ]; then
|
||||
echo "[!] consumer build failed destination=$destination"
|
||||
exit "$exit_code"
|
||||
fi
|
||||
}
|
||||
|
||||
(
|
||||
cd "$WORK_DIR/Consumer"
|
||||
test_build "generic/platform=macOS"
|
||||
test_build "generic/platform=macOS,variant=Mac Catalyst"
|
||||
test_build "generic/platform=iOS"
|
||||
test_build "generic/platform=iOS Simulator"
|
||||
test_build "generic/platform=macOS" "arm64e"
|
||||
test_build "generic/platform=macOS,variant=Mac Catalyst" "arm64e"
|
||||
test_build "generic/platform=iOS" "arm64e"
|
||||
)
|
||||
|
||||
link_arm64e "macos" "macosx" "arm64e-apple-macosx13.0" "macos" ""
|
||||
link_arm64e "ios" "iphoneos" "arm64e-apple-ios15.0" "ios" ""
|
||||
link_arm64e "maccatalyst" "macosx" "arm64e-apple-ios15.0-macabi" "ios" "maccatalyst"
|
||||
|
||||
echo "[*] xcframework consumer tests passed"
|
||||
56
vendor/libghostty-spm/Script/test.sh
vendored
Executable file
56
vendor/libghostty-spm/Script/test.sh
vendored
Executable file
@@ -0,0 +1,56 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
cd "$(dirname "$0")/.."
|
||||
|
||||
export CLANG_MODULE_CACHE_PATH="${CLANG_MODULE_CACHE_PATH:-/tmp/clang-module-cache}"
|
||||
export SWIFTPM_MODULECACHE_OVERRIDE="${SWIFTPM_MODULECACHE_OVERRIDE:-/tmp/swiftpm-module-cache}"
|
||||
|
||||
format_output() {
|
||||
if command -v xcbeautify >/dev/null 2>&1; then
|
||||
xcbeautify
|
||||
else
|
||||
cat
|
||||
fi
|
||||
}
|
||||
|
||||
test_build() {
|
||||
local scheme="$1"
|
||||
local destination="$2"
|
||||
local architecture=${3:-}
|
||||
local command=(
|
||||
xcodebuild
|
||||
-scheme "$scheme"
|
||||
-destination "$destination"
|
||||
)
|
||||
|
||||
if [ -n "$architecture" ]; then
|
||||
command+=("ARCHS=$architecture" "ONLY_ACTIVE_ARCH=YES")
|
||||
fi
|
||||
command+=(build)
|
||||
|
||||
echo "[*] build scheme=$scheme destination=$destination architecture=${architecture:-default}"
|
||||
"${command[@]}" 2>&1 | format_output
|
||||
local exit_code=${PIPESTATUS[0]}
|
||||
if [ "$exit_code" -ne 0 ]; then
|
||||
echo "[!] failed scheme=$scheme destination=$destination"
|
||||
exit "$exit_code"
|
||||
fi
|
||||
}
|
||||
|
||||
test_build "GhosttyKit" "generic/platform=macOS"
|
||||
test_build "GhosttyKit" "generic/platform=iOS"
|
||||
test_build "GhosttyKit" "generic/platform=iOS Simulator"
|
||||
test_build "GhosttyTerminal" "generic/platform=macOS"
|
||||
test_build "GhosttyTerminal" "generic/platform=macOS,variant=Mac Catalyst"
|
||||
test_build "GhosttyTerminal" "generic/platform=iOS"
|
||||
test_build "GhosttyTerminal" "generic/platform=iOS Simulator"
|
||||
test_build "GhosttyKit" "generic/platform=macOS" "arm64e"
|
||||
test_build "GhosttyKit" "generic/platform=macOS,variant=Mac Catalyst" "arm64e"
|
||||
test_build "GhosttyKit" "generic/platform=iOS" "arm64e"
|
||||
test_build "GhosttyTerminal" "generic/platform=macOS" "arm64e"
|
||||
test_build "GhosttyTerminal" "generic/platform=macOS,variant=Mac Catalyst" "arm64e"
|
||||
test_build "GhosttyTerminal" "generic/platform=iOS" "arm64e"
|
||||
|
||||
echo "[*] all tests passed"
|
||||
73
vendor/libghostty-spm/Script/verify-release.sh
vendored
Executable file
73
vendor/libghostty-spm/Script/verify-release.sh
vendored
Executable file
@@ -0,0 +1,73 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
cd "$(dirname "$0")/.."
|
||||
|
||||
if [ ! -f .root ]; then
|
||||
echo "[*] malformed project structure"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
PACKAGE_TAG=${1:-}
|
||||
STORAGE_TAG=${2:-}
|
||||
ASSET_NAME=${3:-GhosttyKit.xcframework.zip}
|
||||
|
||||
if [ -z "$PACKAGE_TAG" ] || [ -z "$STORAGE_TAG" ]; then
|
||||
echo "Usage: $0 <package_tag> <storage_tag> [asset_name]"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if ! command -v gh >/dev/null 2>&1; then
|
||||
echo "[!] gh not found"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
git fetch --tags origin
|
||||
|
||||
package_commit=$(git rev-parse "refs/tags/$PACKAGE_TAG")
|
||||
storage_commit=$(git rev-parse "refs/tags/$STORAGE_TAG")
|
||||
|
||||
if [ "$package_commit" != "$storage_commit" ]; then
|
||||
echo "[!] package tag and storage tag point at different commits"
|
||||
echo " $PACKAGE_TAG: $package_commit"
|
||||
echo " $STORAGE_TAG: $storage_commit"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
manifest=$(git show "$PACKAGE_TAG:Package.swift")
|
||||
download_url=$(printf '%s\n' "$manifest" | python3 -c 'import re, sys; text=sys.stdin.read(); urls=re.findall(r"url:\s*\"([^\"]+)\"", text); matches=[url for url in urls if "/releases/download/" in url and url.endswith("/GhosttyKit.xcframework.zip")]; print(matches[0] if matches else "", end="")')
|
||||
checksum=$(printf '%s\n' "$manifest" | python3 -c 'import re, sys; text=sys.stdin.read(); match=re.search(r"checksum:\s*\"([0-9a-f]{64})\"", text); print(match.group(1) if match else "", end="")')
|
||||
|
||||
if [ -z "$download_url" ] || [ -z "$checksum" ]; then
|
||||
echo "[!] failed to read binary target URL/checksum from Package.swift at $PACKAGE_TAG"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
expected_url="https://github.com/Lakr233/libghostty-spm/releases/download/$STORAGE_TAG/$ASSET_NAME"
|
||||
if [ "$download_url" != "$expected_url" ]; then
|
||||
echo "[!] Package.swift download URL does not match storage release"
|
||||
echo " expected: $expected_url"
|
||||
echo " actual: $download_url"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
asset_digest=$(
|
||||
gh release view "$STORAGE_TAG" \
|
||||
--json assets \
|
||||
--jq ".assets[] | select(.name == \"$ASSET_NAME\") | .digest"
|
||||
)
|
||||
|
||||
if [ -z "$asset_digest" ]; then
|
||||
echo "[!] release asset not found: $STORAGE_TAG/$ASSET_NAME"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ "$asset_digest" != "sha256:$checksum" ]; then
|
||||
echo "[!] release asset digest does not match Package.swift checksum"
|
||||
echo " asset: $asset_digest"
|
||||
echo " manifest: sha256:$checksum"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "[*] release verified: $PACKAGE_TAG -> $STORAGE_TAG/$ASSET_NAME"
|
||||
125
vendor/libghostty-spm/Script/verify-xcframework.sh
vendored
Executable file
125
vendor/libghostty-spm/Script/verify-xcframework.sh
vendored
Executable file
@@ -0,0 +1,125 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
cd "$(dirname "$0")/.."
|
||||
if [ ! -f .root ]; then
|
||||
echo "[*] malformed project structure"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
INPUT_PATH=${1:-}
|
||||
|
||||
if [ -z "$INPUT_PATH" ]; then
|
||||
echo "Usage: $0 <xcframework_or_zip>"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ ! -e "$INPUT_PATH" ]; then
|
||||
echo "[!] not found: $INPUT_PATH"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
TEMP_DIR=
|
||||
XCFRAMEWORK_PATH="$INPUT_PATH"
|
||||
|
||||
cleanup() {
|
||||
if [ -n "$TEMP_DIR" ]; then
|
||||
rm -rf "$TEMP_DIR"
|
||||
fi
|
||||
}
|
||||
trap cleanup EXIT
|
||||
|
||||
if [[ "$INPUT_PATH" == *.zip ]]; then
|
||||
TEMP_DIR=$(mktemp -d)
|
||||
ditto -x -k "$INPUT_PATH" "$TEMP_DIR"
|
||||
XCFRAMEWORK_PATH=$(find "$TEMP_DIR" -maxdepth 1 -name "*.xcframework" -type d | head -1)
|
||||
fi
|
||||
|
||||
if [ -z "$XCFRAMEWORK_PATH" ] || [ ! -d "$XCFRAMEWORK_PATH" ]; then
|
||||
echo "[!] xcframework not found in input: $INPUT_PATH"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
python3 - "$XCFRAMEWORK_PATH" <<'PY'
|
||||
import os
|
||||
import plistlib
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
xcframework = sys.argv[1]
|
||||
info_path = os.path.join(xcframework, "Info.plist")
|
||||
|
||||
if not os.path.isfile(info_path):
|
||||
raise SystemExit(f"[!] missing xcframework Info.plist: {info_path}")
|
||||
|
||||
for root, dirs, _ in os.walk(xcframework):
|
||||
for directory in dirs:
|
||||
if directory.endswith(".framework"):
|
||||
raise SystemExit(f"[!] framework wrapper is not allowed in static-library xcframework: {os.path.join(root, directory)}")
|
||||
|
||||
with open(info_path, "rb") as handle:
|
||||
info = plistlib.load(handle)
|
||||
|
||||
libraries = info.get("AvailableLibraries")
|
||||
if not isinstance(libraries, list) or not libraries:
|
||||
raise SystemExit("[!] AvailableLibraries is empty or missing")
|
||||
|
||||
for library in libraries:
|
||||
identifier = library.get("LibraryIdentifier")
|
||||
library_path = library.get("LibraryPath")
|
||||
binary_path = library.get("BinaryPath")
|
||||
headers_path = library.get("HeadersPath")
|
||||
platform = library.get("SupportedPlatform")
|
||||
platform_variant = library.get("SupportedPlatformVariant")
|
||||
declared_architectures = set(library.get("SupportedArchitectures", []))
|
||||
|
||||
if not identifier:
|
||||
raise SystemExit("[!] library entry missing LibraryIdentifier")
|
||||
if platform not in {"ios", "macos", "tvos", "watchos", "xros"}:
|
||||
raise SystemExit(f"[!] unsupported platform in {identifier}: {platform}")
|
||||
if library_path != "libghostty.a" or binary_path != "libghostty.a":
|
||||
raise SystemExit(f"[!] {identifier} must reference libghostty.a, got LibraryPath={library_path!r} BinaryPath={binary_path!r}")
|
||||
if headers_path != "Headers":
|
||||
raise SystemExit(f"[!] {identifier} must use HeadersPath=Headers, got {headers_path!r}")
|
||||
|
||||
variant_dir = os.path.join(xcframework, identifier)
|
||||
archive_path = os.path.join(variant_dir, library_path)
|
||||
headers_dir = os.path.join(variant_dir, headers_path)
|
||||
header_path = os.path.join(headers_dir, "ghostty.h")
|
||||
modulemap_path = os.path.join(headers_dir, "module.modulemap")
|
||||
|
||||
for required_path in (archive_path, header_path, modulemap_path):
|
||||
if not os.path.exists(required_path):
|
||||
raise SystemExit(f"[!] {identifier} missing required file: {required_path}")
|
||||
|
||||
actual_architectures = set(
|
||||
subprocess.check_output(["lipo", "-archs", archive_path], text=True).split()
|
||||
)
|
||||
if actual_architectures != declared_architectures:
|
||||
raise SystemExit(
|
||||
f"[!] {identifier} architecture metadata mismatch: "
|
||||
f"plist={sorted(declared_architectures)} archive={sorted(actual_architectures)}"
|
||||
)
|
||||
|
||||
expected_architectures = {
|
||||
("macos", None): {"arm64", "arm64e", "x86_64"},
|
||||
("ios", None): {"arm64", "arm64e"},
|
||||
("ios", "maccatalyst"): {"arm64", "arm64e", "x86_64"},
|
||||
("ios", "simulator"): {"arm64", "x86_64"},
|
||||
}.get((platform, platform_variant))
|
||||
if expected_architectures is not None and actual_architectures != expected_architectures:
|
||||
raise SystemExit(
|
||||
f"[!] {identifier} architectures must be {sorted(expected_architectures)}, "
|
||||
f"got {sorted(actual_architectures)}"
|
||||
)
|
||||
|
||||
with open(modulemap_path, "r", encoding="utf-8") as handle:
|
||||
modulemap = handle.read()
|
||||
if "framework module libghostty" in modulemap:
|
||||
raise SystemExit(f"[!] {identifier} module map must not declare a framework module")
|
||||
if "module libghostty" not in modulemap:
|
||||
raise SystemExit(f"[!] {identifier} module map does not declare module libghostty")
|
||||
|
||||
print(f"[*] verified static-library xcframework layout and architectures: {xcframework}")
|
||||
PY
|
||||
8
vendor/libghostty-spm/Sources/GhosttyKit/GhosttyKit.swift
vendored
Normal file
8
vendor/libghostty-spm/Sources/GhosttyKit/GhosttyKit.swift
vendored
Normal file
@@ -0,0 +1,8 @@
|
||||
//
|
||||
// GhosttyKit.swift
|
||||
// libghostty-spm
|
||||
//
|
||||
// Created by Lakr233 on 2026/3/16.
|
||||
//
|
||||
|
||||
@_exported import libghostty
|
||||
40
vendor/libghostty-spm/Sources/GhosttyTerminal/Configuration/GhosttyConfigRenderer.swift
vendored
Normal file
40
vendor/libghostty-spm/Sources/GhosttyTerminal/Configuration/GhosttyConfigRenderer.swift
vendored
Normal file
@@ -0,0 +1,40 @@
|
||||
//
|
||||
// GhosttyConfigRenderer.swift
|
||||
// libghostty-spm
|
||||
//
|
||||
// Created by Lakr233 on 2026/3/17.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
enum GhosttyConfigRenderer {
|
||||
static func render(
|
||||
baseContents: String,
|
||||
configuration: TerminalConfiguration,
|
||||
theme: TerminalConfiguration
|
||||
) -> String {
|
||||
var sections: [String] = []
|
||||
|
||||
let normalizedBase = normalize(baseContents)
|
||||
if !normalizedBase.isEmpty {
|
||||
sections.append(normalizedBase)
|
||||
}
|
||||
|
||||
let configurationLines = configuration.commands.map(\.renderedLine)
|
||||
if !configurationLines.isEmpty {
|
||||
sections.append(configurationLines.joined(separator: "\n"))
|
||||
}
|
||||
|
||||
let themeLines = theme.commands.map(\.renderedLine)
|
||||
if !themeLines.isEmpty {
|
||||
sections.append(themeLines.joined(separator: "\n"))
|
||||
}
|
||||
|
||||
guard !sections.isEmpty else { return "" }
|
||||
return sections.joined(separator: "\n") + "\n"
|
||||
}
|
||||
|
||||
private static func normalize(_ contents: String) -> String {
|
||||
contents.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
}
|
||||
}
|
||||
21
vendor/libghostty-spm/Sources/GhosttyTerminal/Configuration/TerminalColorScheme+SwiftUI.swift
vendored
Normal file
21
vendor/libghostty-spm/Sources/GhosttyTerminal/Configuration/TerminalColorScheme+SwiftUI.swift
vendored
Normal file
@@ -0,0 +1,21 @@
|
||||
//
|
||||
// TerminalColorScheme+SwiftUI.swift
|
||||
// libghostty-spm
|
||||
//
|
||||
// Created by Lakr233 on 2026/3/17.
|
||||
//
|
||||
|
||||
#if canImport(SwiftUI)
|
||||
import SwiftUI
|
||||
|
||||
extension TerminalColorScheme {
|
||||
init(_ colorScheme: ColorScheme) {
|
||||
switch colorScheme {
|
||||
case .dark:
|
||||
self = .dark
|
||||
default:
|
||||
self = .light
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
18
vendor/libghostty-spm/Sources/GhosttyTerminal/Configuration/TerminalColorScheme.swift
vendored
Normal file
18
vendor/libghostty-spm/Sources/GhosttyTerminal/Configuration/TerminalColorScheme.swift
vendored
Normal file
@@ -0,0 +1,18 @@
|
||||
//
|
||||
// TerminalColorScheme.swift
|
||||
// libghostty-spm
|
||||
//
|
||||
|
||||
import GhosttyKit
|
||||
|
||||
public enum TerminalColorScheme: Sendable {
|
||||
case light
|
||||
case dark
|
||||
|
||||
var ghosttyValue: ghostty_color_scheme_e {
|
||||
switch self {
|
||||
case .light: GHOSTTY_COLOR_SCHEME_LIGHT
|
||||
case .dark: GHOSTTY_COLOR_SCHEME_DARK
|
||||
}
|
||||
}
|
||||
}
|
||||
360
vendor/libghostty-spm/Sources/GhosttyTerminal/Configuration/TerminalConfiguration.swift
vendored
Normal file
360
vendor/libghostty-spm/Sources/GhosttyTerminal/Configuration/TerminalConfiguration.swift
vendored
Normal file
@@ -0,0 +1,360 @@
|
||||
//
|
||||
// TerminalConfiguration.swift
|
||||
// libghostty-spm
|
||||
//
|
||||
// Created by Lakr233 on 2026/3/17.
|
||||
//
|
||||
|
||||
public enum TerminalCursorStyle: String, Sendable, Hashable {
|
||||
case block
|
||||
case bar
|
||||
case underline
|
||||
}
|
||||
|
||||
public enum TerminalConfigCommand: Sendable, Hashable {
|
||||
// Font
|
||||
case fontFamily(String)
|
||||
case fontSize(Float)
|
||||
case fontThicken(Bool)
|
||||
case fontThickenStrength(Int)
|
||||
|
||||
// Cursor
|
||||
case cursorStyle(TerminalCursorStyle)
|
||||
case cursorStyleBlink(Bool)
|
||||
case cursorColor(String)
|
||||
case cursorText(String)
|
||||
case cursorOpacity(Double)
|
||||
|
||||
// Colors
|
||||
case background(String)
|
||||
case foreground(String)
|
||||
case selectionBackground(String)
|
||||
case selectionForeground(String)
|
||||
case boldColor(String)
|
||||
case palette(index: Int, color: String)
|
||||
case minimumContrast(Double)
|
||||
|
||||
// Background
|
||||
case backgroundOpacity(Double)
|
||||
case backgroundBlur(Int)
|
||||
|
||||
// Layout
|
||||
case windowPaddingX(Int)
|
||||
case windowPaddingY(Int)
|
||||
|
||||
/// Escape hatch
|
||||
case custom(key: String, value: String)
|
||||
|
||||
var renderedLine: String {
|
||||
switch self {
|
||||
case let .fontFamily(value):
|
||||
"font-family = \(value)"
|
||||
|
||||
case let .fontSize(value):
|
||||
"font-size = \(value.formatted(.number.precision(.fractionLength(0 ... 2))))"
|
||||
|
||||
case let .fontThicken(enabled):
|
||||
"font-thicken = \(enabled)"
|
||||
|
||||
case let .fontThickenStrength(value):
|
||||
"font-thicken-strength = \(value)"
|
||||
|
||||
case let .cursorStyle(style):
|
||||
"cursor-style = \(style.rawValue)"
|
||||
|
||||
case let .cursorStyleBlink(enabled):
|
||||
"cursor-style-blink = \(enabled)"
|
||||
|
||||
case let .cursorColor(value):
|
||||
"cursor-color = \(value)"
|
||||
|
||||
case let .cursorText(value):
|
||||
"cursor-text = \(value)"
|
||||
|
||||
case let .cursorOpacity(value):
|
||||
"cursor-opacity = \(value.formatted(.number.precision(.fractionLength(0 ... 3))))"
|
||||
|
||||
case let .background(value):
|
||||
"background = \(value)"
|
||||
|
||||
case let .foreground(value):
|
||||
"foreground = \(value)"
|
||||
|
||||
case let .selectionBackground(value):
|
||||
"selection-background = \(value)"
|
||||
|
||||
case let .selectionForeground(value):
|
||||
"selection-foreground = \(value)"
|
||||
|
||||
case let .boldColor(value):
|
||||
"bold-color = \(value)"
|
||||
|
||||
case let .palette(index, color):
|
||||
"palette = \(index)=\(color)"
|
||||
|
||||
case let .minimumContrast(value):
|
||||
"minimum-contrast = \(value.formatted(.number.precision(.fractionLength(0 ... 2))))"
|
||||
|
||||
case let .backgroundOpacity(value):
|
||||
"background-opacity = \(value.formatted(.number.precision(.fractionLength(0 ... 3))))"
|
||||
|
||||
case let .backgroundBlur(value):
|
||||
"background-blur = \(value)"
|
||||
|
||||
case let .windowPaddingX(value):
|
||||
"window-padding-x = \(value)"
|
||||
|
||||
case let .windowPaddingY(value):
|
||||
"window-padding-y = \(value)"
|
||||
|
||||
case let .custom(key, value):
|
||||
"\(key) = \(value)"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public struct TerminalConfiguration: Sendable, Hashable {
|
||||
public struct Builder {
|
||||
var commands: [TerminalConfigCommand] = []
|
||||
|
||||
public init() {}
|
||||
|
||||
init(commands: [TerminalConfigCommand]) {
|
||||
self.commands = commands
|
||||
}
|
||||
|
||||
/// Font
|
||||
public mutating func withFontFamily(_ value: String) {
|
||||
commands.append(.fontFamily(value))
|
||||
}
|
||||
|
||||
public mutating func withFontSize(_ value: Float) {
|
||||
commands.append(.fontSize(value))
|
||||
}
|
||||
|
||||
public mutating func withFontThicken(_ enabled: Bool) {
|
||||
commands.append(.fontThicken(enabled))
|
||||
}
|
||||
|
||||
public mutating func withFontThickenStrength(_ value: Int) {
|
||||
commands.append(.fontThickenStrength(value))
|
||||
}
|
||||
|
||||
/// Cursor
|
||||
public mutating func withCursorStyle(_ style: TerminalCursorStyle) {
|
||||
commands.append(.cursorStyle(style))
|
||||
}
|
||||
|
||||
public mutating func withCursorStyleBlink(_ enabled: Bool) {
|
||||
commands.append(.cursorStyleBlink(enabled))
|
||||
}
|
||||
|
||||
public mutating func withCursorColor(_ value: String) {
|
||||
commands.append(.cursorColor(value))
|
||||
}
|
||||
|
||||
public mutating func withCursorText(_ value: String) {
|
||||
commands.append(.cursorText(value))
|
||||
}
|
||||
|
||||
public mutating func withCursorOpacity(_ value: Double) {
|
||||
commands.append(.cursorOpacity(value))
|
||||
}
|
||||
|
||||
/// Colors
|
||||
public mutating func withBackground(_ value: String) {
|
||||
commands.append(.background(value))
|
||||
}
|
||||
|
||||
public mutating func withForeground(_ value: String) {
|
||||
commands.append(.foreground(value))
|
||||
}
|
||||
|
||||
public mutating func withSelectionBackground(_ value: String) {
|
||||
commands.append(.selectionBackground(value))
|
||||
}
|
||||
|
||||
public mutating func withSelectionForeground(_ value: String) {
|
||||
commands.append(.selectionForeground(value))
|
||||
}
|
||||
|
||||
public mutating func withBoldColor(_ value: String) {
|
||||
commands.append(.boldColor(value))
|
||||
}
|
||||
|
||||
public mutating func withPalette(_ index: Int, color: String) {
|
||||
commands.append(.palette(index: index, color: color))
|
||||
}
|
||||
|
||||
public mutating func withMinimumContrast(_ value: Double) {
|
||||
commands.append(.minimumContrast(value))
|
||||
}
|
||||
|
||||
/// Background
|
||||
public mutating func withBackgroundOpacity(_ value: Double) {
|
||||
commands.append(.backgroundOpacity(value))
|
||||
}
|
||||
|
||||
public mutating func withBackgroundBlur(_ value: Int) {
|
||||
commands.append(.backgroundBlur(value))
|
||||
}
|
||||
|
||||
/// Layout
|
||||
public mutating func withWindowPaddingX(_ value: Int) {
|
||||
commands.append(.windowPaddingX(value))
|
||||
}
|
||||
|
||||
public mutating func withWindowPaddingY(_ value: Int) {
|
||||
commands.append(.windowPaddingY(value))
|
||||
}
|
||||
|
||||
/// Escape hatch
|
||||
public mutating func withCustom(_ key: String, _ value: String) {
|
||||
commands.append(.custom(key: key, value: value))
|
||||
}
|
||||
}
|
||||
|
||||
let commands: [TerminalConfigCommand]
|
||||
|
||||
public init() {
|
||||
commands = []
|
||||
}
|
||||
|
||||
public init(configure: (inout Builder) -> Void) {
|
||||
self.init(startingFrom: .init(), configure: configure)
|
||||
}
|
||||
|
||||
public init(
|
||||
startingFrom base: TerminalConfiguration,
|
||||
configure: (inout Builder) -> Void
|
||||
) {
|
||||
var builder = Builder(commands: base.commands)
|
||||
configure(&builder)
|
||||
commands = builder.commands
|
||||
}
|
||||
|
||||
public func appending(_ command: TerminalConfigCommand) -> TerminalConfiguration {
|
||||
TerminalConfiguration(commands: commands + [command])
|
||||
}
|
||||
|
||||
// MARK: - Font
|
||||
|
||||
public func fontFamily(_ value: String) -> TerminalConfiguration {
|
||||
appending(.fontFamily(value))
|
||||
}
|
||||
|
||||
public func fontSize(_ value: Float) -> TerminalConfiguration {
|
||||
appending(.fontSize(value))
|
||||
}
|
||||
|
||||
public func fontThicken(_ enabled: Bool) -> TerminalConfiguration {
|
||||
appending(.fontThicken(enabled))
|
||||
}
|
||||
|
||||
public func fontThickenStrength(_ value: Int) -> TerminalConfiguration {
|
||||
appending(.fontThickenStrength(value))
|
||||
}
|
||||
|
||||
// MARK: - Cursor
|
||||
|
||||
public func cursorStyle(_ style: TerminalCursorStyle) -> TerminalConfiguration {
|
||||
appending(.cursorStyle(style))
|
||||
}
|
||||
|
||||
public func cursorStyleBlink(_ enabled: Bool) -> TerminalConfiguration {
|
||||
appending(.cursorStyleBlink(enabled))
|
||||
}
|
||||
|
||||
public func cursorColor(_ value: String) -> TerminalConfiguration {
|
||||
appending(.cursorColor(value))
|
||||
}
|
||||
|
||||
public func cursorText(_ value: String) -> TerminalConfiguration {
|
||||
appending(.cursorText(value))
|
||||
}
|
||||
|
||||
public func cursorOpacity(_ value: Double) -> TerminalConfiguration {
|
||||
appending(.cursorOpacity(value))
|
||||
}
|
||||
|
||||
// MARK: - Colors
|
||||
|
||||
public func background(_ value: String) -> TerminalConfiguration {
|
||||
appending(.background(value))
|
||||
}
|
||||
|
||||
public func foreground(_ value: String) -> TerminalConfiguration {
|
||||
appending(.foreground(value))
|
||||
}
|
||||
|
||||
public func selectionBackground(_ value: String) -> TerminalConfiguration {
|
||||
appending(.selectionBackground(value))
|
||||
}
|
||||
|
||||
public func selectionForeground(_ value: String) -> TerminalConfiguration {
|
||||
appending(.selectionForeground(value))
|
||||
}
|
||||
|
||||
public func boldColor(_ value: String) -> TerminalConfiguration {
|
||||
appending(.boldColor(value))
|
||||
}
|
||||
|
||||
public func palette(_ index: Int, color: String) -> TerminalConfiguration {
|
||||
appending(.palette(index: index, color: color))
|
||||
}
|
||||
|
||||
public func minimumContrast(_ value: Double) -> TerminalConfiguration {
|
||||
appending(.minimumContrast(value))
|
||||
}
|
||||
|
||||
// MARK: - Background
|
||||
|
||||
public func backgroundOpacity(_ value: Double) -> TerminalConfiguration {
|
||||
appending(.backgroundOpacity(value))
|
||||
}
|
||||
|
||||
public func backgroundBlur(_ value: Int) -> TerminalConfiguration {
|
||||
appending(.backgroundBlur(value))
|
||||
}
|
||||
|
||||
// MARK: - Layout
|
||||
|
||||
public func windowPaddingX(_ value: Int) -> TerminalConfiguration {
|
||||
appending(.windowPaddingX(value))
|
||||
}
|
||||
|
||||
public func windowPaddingY(_ value: Int) -> TerminalConfiguration {
|
||||
appending(.windowPaddingY(value))
|
||||
}
|
||||
|
||||
// MARK: - Escape Hatch
|
||||
|
||||
public func custom(_ key: String, _ value: String) -> TerminalConfiguration {
|
||||
appending(.custom(key: key, value: value))
|
||||
}
|
||||
|
||||
// MARK: - Defaults
|
||||
|
||||
public static let `default` = TerminalConfiguration { builder in
|
||||
builder.withCursorStyle(.block)
|
||||
builder.withCursorStyleBlink(true)
|
||||
#if os(iOS)
|
||||
builder.withFontSize(10)
|
||||
#else
|
||||
builder.withFontSize(14)
|
||||
#endif
|
||||
builder.withFontThicken(true)
|
||||
}
|
||||
|
||||
public var rendered: String {
|
||||
commands.map(\.renderedLine).joined(separator: "\n")
|
||||
}
|
||||
|
||||
var isEmpty: Bool {
|
||||
commands.isEmpty
|
||||
}
|
||||
|
||||
init(commands: [TerminalConfigCommand]) {
|
||||
self.commands = commands
|
||||
}
|
||||
}
|
||||
70
vendor/libghostty-spm/Sources/GhosttyTerminal/Configuration/TerminalTheme+Defaults.swift
vendored
Normal file
70
vendor/libghostty-spm/Sources/GhosttyTerminal/Configuration/TerminalTheme+Defaults.swift
vendored
Normal file
@@ -0,0 +1,70 @@
|
||||
//
|
||||
// TerminalTheme+Defaults.swift
|
||||
// libghostty-spm
|
||||
//
|
||||
|
||||
public extension TerminalTheme {
|
||||
/// Afterglow (dark) + Alabaster (light) default theme.
|
||||
static let `default` = TerminalTheme(
|
||||
light: .alabaster,
|
||||
dark: .afterglow
|
||||
)
|
||||
}
|
||||
|
||||
public extension TerminalConfiguration {
|
||||
/// Alabaster — light terminal theme.
|
||||
///
|
||||
/// Background: #F7F7F7, Foreground: #000000
|
||||
static let alabaster = TerminalConfiguration { builder in
|
||||
builder.withBackground("F7F7F7")
|
||||
builder.withForeground("000000")
|
||||
builder.withCursorColor("007ACC")
|
||||
builder.withSelectionBackground("C9D0D9")
|
||||
// Normal colors (0-7)
|
||||
builder.withPalette(0, color: "#000000")
|
||||
builder.withPalette(1, color: "#AA3731")
|
||||
builder.withPalette(2, color: "#448C27")
|
||||
builder.withPalette(3, color: "#CB8800")
|
||||
builder.withPalette(4, color: "#325CC0")
|
||||
builder.withPalette(5, color: "#7A3E9D")
|
||||
builder.withPalette(6, color: "#0083B2")
|
||||
builder.withPalette(7, color: "#F7F7F7")
|
||||
// Bright colors (8-15)
|
||||
builder.withPalette(8, color: "#777777")
|
||||
builder.withPalette(9, color: "#F03E31")
|
||||
builder.withPalette(10, color: "#60CB00")
|
||||
builder.withPalette(11, color: "#FFBC5D")
|
||||
builder.withPalette(12, color: "#007ACC")
|
||||
builder.withPalette(13, color: "#E64CE6")
|
||||
builder.withPalette(14, color: "#00AACB")
|
||||
builder.withPalette(15, color: "#F7F7F7")
|
||||
}
|
||||
|
||||
/// Afterglow — dark terminal theme.
|
||||
///
|
||||
/// Background: #212121, Foreground: #D0D0D0
|
||||
static let afterglow = TerminalConfiguration { builder in
|
||||
builder.withBackground("212121")
|
||||
builder.withForeground("D0D0D0")
|
||||
builder.withCursorColor("D0D0D0")
|
||||
builder.withSelectionBackground("303030")
|
||||
// Normal colors (0-7)
|
||||
builder.withPalette(0, color: "#151515")
|
||||
builder.withPalette(1, color: "#AC4142")
|
||||
builder.withPalette(2, color: "#7E8E50")
|
||||
builder.withPalette(3, color: "#E4B567")
|
||||
builder.withPalette(4, color: "#6C99BB")
|
||||
builder.withPalette(5, color: "#9F4E86")
|
||||
builder.withPalette(6, color: "#7DD5CF")
|
||||
builder.withPalette(7, color: "#D0D0D0")
|
||||
// Bright colors (8-15)
|
||||
builder.withPalette(8, color: "#505050")
|
||||
builder.withPalette(9, color: "#AC4142")
|
||||
builder.withPalette(10, color: "#7E8E50")
|
||||
builder.withPalette(11, color: "#E4B567")
|
||||
builder.withPalette(12, color: "#6C99BB")
|
||||
builder.withPalette(13, color: "#9F4E86")
|
||||
builder.withPalette(14, color: "#7DD5CF")
|
||||
builder.withPalette(15, color: "#F5F5F5")
|
||||
}
|
||||
}
|
||||
32
vendor/libghostty-spm/Sources/GhosttyTerminal/Configuration/TerminalTheme.swift
vendored
Normal file
32
vendor/libghostty-spm/Sources/GhosttyTerminal/Configuration/TerminalTheme.swift
vendored
Normal file
@@ -0,0 +1,32 @@
|
||||
//
|
||||
// TerminalTheme.swift
|
||||
// libghostty-spm
|
||||
//
|
||||
// Created by Lakr233 on 2026/3/17.
|
||||
//
|
||||
|
||||
public struct TerminalTheme: Sendable, Hashable {
|
||||
public var light: TerminalConfiguration
|
||||
public var dark: TerminalConfiguration
|
||||
|
||||
public init(
|
||||
light: TerminalConfiguration = .init(),
|
||||
dark: TerminalConfiguration = .init()
|
||||
) {
|
||||
self.light = light
|
||||
self.dark = dark
|
||||
}
|
||||
|
||||
var isEmpty: Bool {
|
||||
light.isEmpty && dark.isEmpty
|
||||
}
|
||||
|
||||
func configuration(for colorScheme: TerminalColorScheme) -> TerminalConfiguration {
|
||||
switch colorScheme {
|
||||
case .light:
|
||||
light
|
||||
case .dark:
|
||||
dark
|
||||
}
|
||||
}
|
||||
}
|
||||
197
vendor/libghostty-spm/Sources/GhosttyTerminal/Controller/TerminalController+Callbacks.swift
vendored
Normal file
197
vendor/libghostty-spm/Sources/GhosttyTerminal/Controller/TerminalController+Callbacks.swift
vendored
Normal file
@@ -0,0 +1,197 @@
|
||||
//
|
||||
// TerminalController+Callbacks.swift
|
||||
// libghostty-spm
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import GhosttyKit
|
||||
|
||||
#if canImport(UIKit)
|
||||
import UIKit
|
||||
#elseif canImport(AppKit)
|
||||
import AppKit
|
||||
#endif
|
||||
|
||||
private enum TerminalCallbacks {
|
||||
static func wakeup(userdata: UnsafeMutableRawPointer?) {
|
||||
guard let userdata else { return }
|
||||
let controller = Unmanaged<TerminalController>.fromOpaque(userdata)
|
||||
.takeUnretainedValue()
|
||||
terminalRunOnMain {
|
||||
controller.handleWakeup()
|
||||
}
|
||||
}
|
||||
|
||||
static func action(
|
||||
appPtr: ghostty_app_t?,
|
||||
target: ghostty_target_s,
|
||||
action: ghostty_action_s
|
||||
) -> Bool {
|
||||
guard let appPtr else { return false }
|
||||
guard ghostty_app_userdata(appPtr) != nil else { return false }
|
||||
guard target.tag == GHOSTTY_TARGET_SURFACE else { return false }
|
||||
guard let surfacePtr = target.target.surface else { return false }
|
||||
guard let bridgePtr = ghostty_surface_userdata(surfacePtr) else { return false }
|
||||
|
||||
let bridge = Unmanaged<TerminalCallbackBridge>
|
||||
.fromOpaque(bridgePtr)
|
||||
.takeUnretainedValue()
|
||||
terminalRunOnMain {
|
||||
bridge.handleAction(action)
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
static func closeSurface(
|
||||
userdata: UnsafeMutableRawPointer?,
|
||||
processAlive: Bool
|
||||
) {
|
||||
guard let userdata else { return }
|
||||
let bridge = Unmanaged<TerminalCallbackBridge>
|
||||
.fromOpaque(userdata)
|
||||
.takeUnretainedValue()
|
||||
terminalRunOnMain {
|
||||
bridge.handleClose(processAlive: processAlive)
|
||||
}
|
||||
}
|
||||
|
||||
static func writeClipboard(
|
||||
userdata _: UnsafeMutableRawPointer?,
|
||||
clipboard _: ghostty_clipboard_e,
|
||||
contents: UnsafePointer<ghostty_clipboard_content_s>?,
|
||||
contentsLen: Int,
|
||||
confirm _: Bool
|
||||
) {
|
||||
guard contentsLen > 0 else { return }
|
||||
guard let content = contents?.pointee else { return }
|
||||
guard let data = content.data else { return }
|
||||
let string = String(cString: data)
|
||||
|
||||
#if canImport(UIKit)
|
||||
UIPasteboard.general.string = string
|
||||
#elseif canImport(AppKit)
|
||||
let pasteboard = NSPasteboard.general
|
||||
pasteboard.clearContents()
|
||||
pasteboard.setString(string, forType: .string)
|
||||
#endif
|
||||
}
|
||||
|
||||
static func readClipboard(
|
||||
userdata: UnsafeMutableRawPointer?,
|
||||
clipboard _: ghostty_clipboard_e,
|
||||
opaquePtr: UnsafeMutableRawPointer?
|
||||
) -> Bool {
|
||||
guard let userdata, let opaquePtr else { return false }
|
||||
|
||||
let bridge = Unmanaged<TerminalCallbackBridge>
|
||||
.fromOpaque(userdata)
|
||||
.takeUnretainedValue()
|
||||
guard let surface = bridge.rawSurface else { return false }
|
||||
|
||||
#if canImport(UIKit)
|
||||
let string = UIPasteboard.general.string
|
||||
#elseif canImport(AppKit)
|
||||
let string = NSPasteboard.general.string(forType: .string)
|
||||
#endif
|
||||
|
||||
guard let string else {
|
||||
TerminalDebugLog.log(.input, "clipboard paste read empty")
|
||||
return false
|
||||
}
|
||||
TerminalDebugLog.log(
|
||||
.input,
|
||||
"clipboard paste read bytes=\(string.utf8.count) lines=\(TerminalInputText.lineCount(in: string))"
|
||||
)
|
||||
string.withCString { cString in
|
||||
ghostty_surface_complete_clipboard_request(surface, cString, opaquePtr, false)
|
||||
}
|
||||
TerminalDebugLog.log(.input, "clipboard paste complete")
|
||||
return true
|
||||
}
|
||||
|
||||
static func confirmReadClipboard(
|
||||
userdata: UnsafeMutableRawPointer?,
|
||||
string: UnsafePointer<CChar>?,
|
||||
opaquePtr: UnsafeMutableRawPointer?,
|
||||
request: ghostty_clipboard_request_e
|
||||
) {
|
||||
guard let userdata, let string, let opaquePtr else { return }
|
||||
|
||||
let bridge = Unmanaged<TerminalCallbackBridge>
|
||||
.fromOpaque(userdata)
|
||||
.takeUnretainedValue()
|
||||
guard let surface = bridge.rawSurface else { return }
|
||||
|
||||
let text = String(cString: string)
|
||||
TerminalDebugLog.log(
|
||||
.input,
|
||||
"clipboard paste confirm request=\(request.rawValue) bytes=\(text.utf8.count) lines=\(TerminalInputText.lineCount(in: text))"
|
||||
)
|
||||
text.withCString { cString in
|
||||
ghostty_surface_complete_clipboard_request(surface, cString, opaquePtr, true)
|
||||
}
|
||||
TerminalDebugLog.log(.input, "clipboard paste confirmed")
|
||||
}
|
||||
}
|
||||
|
||||
func terminalControllerWakeupCallback(userdata: UnsafeMutableRawPointer?) {
|
||||
TerminalCallbacks.wakeup(userdata: userdata)
|
||||
}
|
||||
|
||||
func terminalControllerActionCallback(
|
||||
appPtr: ghostty_app_t?,
|
||||
target: ghostty_target_s,
|
||||
action: ghostty_action_s
|
||||
) -> Bool {
|
||||
TerminalCallbacks.action(appPtr: appPtr, target: target, action: action)
|
||||
}
|
||||
|
||||
func terminalControllerCloseSurfaceCallback(
|
||||
userdata: UnsafeMutableRawPointer?,
|
||||
processAlive: Bool
|
||||
) {
|
||||
TerminalCallbacks.closeSurface(userdata: userdata, processAlive: processAlive)
|
||||
}
|
||||
|
||||
func terminalControllerWriteClipboardCallback(
|
||||
userdata: UnsafeMutableRawPointer?,
|
||||
clipboard: ghostty_clipboard_e,
|
||||
contents: UnsafePointer<ghostty_clipboard_content_s>?,
|
||||
contentsLen: Int,
|
||||
confirm: Bool
|
||||
) {
|
||||
TerminalCallbacks.writeClipboard(
|
||||
userdata: userdata,
|
||||
clipboard: clipboard,
|
||||
contents: contents,
|
||||
contentsLen: contentsLen,
|
||||
confirm: confirm
|
||||
)
|
||||
}
|
||||
|
||||
func terminalControllerReadClipboardCallback(
|
||||
userdata: UnsafeMutableRawPointer?,
|
||||
clipboard: ghostty_clipboard_e,
|
||||
opaquePtr: UnsafeMutableRawPointer?
|
||||
) -> Bool {
|
||||
TerminalCallbacks.readClipboard(
|
||||
userdata: userdata,
|
||||
clipboard: clipboard,
|
||||
opaquePtr: opaquePtr
|
||||
)
|
||||
}
|
||||
|
||||
func terminalControllerConfirmReadClipboardCallback(
|
||||
userdata: UnsafeMutableRawPointer?,
|
||||
string: UnsafePointer<CChar>?,
|
||||
opaquePtr: UnsafeMutableRawPointer?,
|
||||
request: ghostty_clipboard_request_e
|
||||
) {
|
||||
TerminalCallbacks.confirmReadClipboard(
|
||||
userdata: userdata,
|
||||
string: string,
|
||||
opaquePtr: opaquePtr,
|
||||
request: request
|
||||
)
|
||||
}
|
||||
218
vendor/libghostty-spm/Sources/GhosttyTerminal/Controller/TerminalController+Config.swift
vendored
Normal file
218
vendor/libghostty-spm/Sources/GhosttyTerminal/Controller/TerminalController+Config.swift
vendored
Normal file
@@ -0,0 +1,218 @@
|
||||
//
|
||||
// TerminalController+Config.swift
|
||||
// libghostty-spm
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import GhosttyKit
|
||||
|
||||
extension TerminalController {
|
||||
@discardableResult
|
||||
public func updateConfigSource(_ source: ConfigSource) -> Bool {
|
||||
guard source != configSource else { return true }
|
||||
|
||||
switch Self.prepareConfig(source: source) {
|
||||
case let .success(value):
|
||||
applyPreparedConfigToRuntime(value, source: source)
|
||||
return true
|
||||
|
||||
case let .failure(issue):
|
||||
lastConfigurationIssue = issue.description
|
||||
Self.reportConfigurationIssue(issue.description)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func applyResolvedConfig(
|
||||
_ resolved: (source: ConfigSource, contents: String),
|
||||
willChange: (() -> Void)?,
|
||||
applyState: () -> Void = {}
|
||||
) -> Bool {
|
||||
guard resolved.source != configSource else {
|
||||
// ObservableObject subscribers expect will-change semantics.
|
||||
willChange?()
|
||||
applyState()
|
||||
renderedConfigContents = resolved.contents
|
||||
return true
|
||||
}
|
||||
|
||||
switch Self.prepareConfig(source: resolved.source) {
|
||||
case let .success(prepared):
|
||||
// Notify after validation succeeds, but before committed state
|
||||
// changes become visible through computed TerminalViewState APIs.
|
||||
willChange?()
|
||||
applyState()
|
||||
applyPreparedConfigToRuntime(prepared, source: resolved.source)
|
||||
return true
|
||||
|
||||
case let .failure(issue):
|
||||
lastConfigurationIssue = issue.description
|
||||
Self.reportConfigurationIssue(issue.description)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
private func applyPreparedConfigToRuntime(_ prepared: PreparedConfig, source: ConfigSource) {
|
||||
let previousConfig = config
|
||||
let previousManagedConfigURL = managedConfigURL
|
||||
let nextConfig = prepared.rawValue
|
||||
|
||||
if let app {
|
||||
ghostty_app_update_config(app, nextConfig)
|
||||
}
|
||||
|
||||
for bridge in retainedBridges {
|
||||
guard let surface = bridge.rawSurface else { continue }
|
||||
ghostty_surface_update_config(surface, nextConfig)
|
||||
}
|
||||
|
||||
applyPreparedConfig(prepared, source: source)
|
||||
|
||||
if let previousConfig {
|
||||
ghostty_config_free(previousConfig)
|
||||
}
|
||||
|
||||
if let previousManagedConfigURL, previousManagedConfigURL != managedConfigURL {
|
||||
try? FileManager.default.removeItem(at: previousManagedConfigURL)
|
||||
}
|
||||
}
|
||||
|
||||
func applyInitialConfig(source: ConfigSource) {
|
||||
switch Self.prepareConfig(source: source) {
|
||||
case let .success(prepared):
|
||||
applyPreparedConfig(prepared, source: source)
|
||||
|
||||
case let .failure(issue):
|
||||
lastConfigurationIssue = issue.description
|
||||
Self.reportConfigurationIssue(issue.description)
|
||||
|
||||
guard source != .none else { return }
|
||||
guard case let .success(fallback) = Self.prepareConfig(source: ConfigSource.none) else {
|
||||
return
|
||||
}
|
||||
applyPreparedConfig(fallback, source: .none)
|
||||
}
|
||||
}
|
||||
|
||||
func createApp() {
|
||||
guard let cfg = config else { return }
|
||||
|
||||
let userdata = Unmanaged.passUnretained(self).toOpaque()
|
||||
|
||||
var runtimeConfig = ghostty_runtime_config_s()
|
||||
runtimeConfig.userdata = userdata
|
||||
runtimeConfig.supports_selection_clipboard = true
|
||||
runtimeConfig.wakeup_cb = terminalControllerWakeupCallback
|
||||
runtimeConfig.action_cb = terminalControllerActionCallback
|
||||
runtimeConfig.close_surface_cb = terminalControllerCloseSurfaceCallback
|
||||
runtimeConfig.write_clipboard_cb = terminalControllerWriteClipboardCallback
|
||||
runtimeConfig.read_clipboard_cb = terminalControllerReadClipboardCallback
|
||||
runtimeConfig.confirm_read_clipboard_cb = terminalControllerConfirmReadClipboardCallback
|
||||
|
||||
app = ghostty_app_new(&runtimeConfig, cfg)
|
||||
}
|
||||
|
||||
private static func prepareConfig(
|
||||
source: ConfigSource
|
||||
) -> Result<PreparedConfig, ConfigurationIssue> {
|
||||
let resolvedContents: String
|
||||
let configPath: String
|
||||
let managedConfigURL: URL?
|
||||
|
||||
switch source {
|
||||
case .none:
|
||||
resolvedContents = defaultRenderedConfig
|
||||
switch writeManagedConfig(contents: resolvedContents) {
|
||||
case let .success(url):
|
||||
managedConfigURL = url
|
||||
configPath = url.path
|
||||
case let .failure(issue):
|
||||
return .failure(issue)
|
||||
}
|
||||
|
||||
case let .generated(contents):
|
||||
resolvedContents = contents
|
||||
switch writeManagedConfig(contents: contents) {
|
||||
case let .success(url):
|
||||
managedConfigURL = url
|
||||
configPath = url.path
|
||||
case let .failure(issue):
|
||||
return .failure(issue)
|
||||
}
|
||||
|
||||
case let .file(path):
|
||||
do {
|
||||
resolvedContents = try String(contentsOfFile: path, encoding: .utf8)
|
||||
} catch {
|
||||
return .failure(ConfigurationIssue("failed to load ghostty config template: \(error)"))
|
||||
}
|
||||
managedConfigURL = nil
|
||||
configPath = path
|
||||
}
|
||||
|
||||
guard let rawValue = ghostty_config_new() else {
|
||||
if let managedConfigURL {
|
||||
try? FileManager.default.removeItem(at: managedConfigURL)
|
||||
}
|
||||
return .failure(ConfigurationIssue("ghostty_config_new returned nil"))
|
||||
}
|
||||
|
||||
ghostty_config_load_file(rawValue, configPath)
|
||||
ghostty_config_finalize(rawValue)
|
||||
|
||||
let diagnostics = configDiagnostics(from: rawValue)
|
||||
guard diagnostics.isEmpty else {
|
||||
ghostty_config_free(rawValue)
|
||||
if let managedConfigURL {
|
||||
try? FileManager.default.removeItem(at: managedConfigURL)
|
||||
}
|
||||
return .failure(
|
||||
ConfigurationIssue("ghostty config diagnostics: \(diagnostics.joined(separator: " | "))")
|
||||
)
|
||||
}
|
||||
|
||||
return .success(
|
||||
PreparedConfig(
|
||||
rawValue: rawValue,
|
||||
managedConfigURL: managedConfigURL,
|
||||
renderedContents: resolvedContents
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
private static func writeManagedConfig(contents: String) -> Result<URL, ConfigurationIssue> {
|
||||
let url = FileManager.default.temporaryDirectory
|
||||
.appendingPathComponent("ghostty-config-\(UUID().uuidString)")
|
||||
.appendingPathExtension("conf")
|
||||
|
||||
do {
|
||||
try contents.write(to: url, atomically: true, encoding: .utf8)
|
||||
return .success(url)
|
||||
} catch {
|
||||
return .failure(ConfigurationIssue("failed to write generated ghostty config: \(error)"))
|
||||
}
|
||||
}
|
||||
|
||||
private static func configDiagnostics(from config: ghostty_config_t) -> [String] {
|
||||
let count = ghostty_config_diagnostics_count(config)
|
||||
guard count > 0 else { return [] }
|
||||
|
||||
return (0 ..< count).compactMap { index in
|
||||
let diagnostic = ghostty_config_get_diagnostic(config, index)
|
||||
guard let message = diagnostic.message else { return nil }
|
||||
return String(cString: message)
|
||||
}
|
||||
}
|
||||
|
||||
private static func reportConfigurationIssue(_ message: String) {
|
||||
NSLog("GhosttyTerminal configuration issue: %@", message)
|
||||
}
|
||||
|
||||
private func applyPreparedConfig(_ prepared: PreparedConfig, source: ConfigSource) {
|
||||
config = prepared.rawValue
|
||||
managedConfigURL = prepared.managedConfigURL
|
||||
renderedConfigContents = prepared.renderedContents
|
||||
configSource = source
|
||||
lastConfigurationIssue = nil
|
||||
}
|
||||
}
|
||||
145
vendor/libghostty-spm/Sources/GhosttyTerminal/Controller/TerminalController+Surface.swift
vendored
Normal file
145
vendor/libghostty-spm/Sources/GhosttyTerminal/Controller/TerminalController+Surface.swift
vendored
Normal file
@@ -0,0 +1,145 @@
|
||||
//
|
||||
// TerminalController+Surface.swift
|
||||
// libghostty-spm
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import GhosttyKit
|
||||
|
||||
extension TerminalController {
|
||||
/// Creates a new Ghostty surface with the given configuration.
|
||||
///
|
||||
/// The `platformSetup` closure lets the caller fill in
|
||||
/// platform-specific fields (`platform_tag`, `platform`, `scale_factor`)
|
||||
/// on the raw surface config struct before the surface is created.
|
||||
func createSurface(
|
||||
bridge: TerminalCallbackBridge,
|
||||
configuration: TerminalSurfaceOptions,
|
||||
platformSetup: (inout ghostty_surface_config_s) -> Void
|
||||
) -> ghostty_surface_t? {
|
||||
guard let app else { return nil }
|
||||
|
||||
var surfaceConfig = ghostty_surface_config_new()
|
||||
surfaceConfig.userdata = Unmanaged.passUnretained(bridge).toOpaque()
|
||||
surfaceConfig.context = configuration.context.ghosttyValue
|
||||
configureBackend(&surfaceConfig, from: configuration)
|
||||
|
||||
if let fontSize = configuration.fontSize {
|
||||
surfaceConfig.font_size = fontSize
|
||||
}
|
||||
|
||||
// Like `working_directory` below, the pointers only need to outlive
|
||||
// `ghostty_surface_new`, which copies the values during surface init.
|
||||
return withEnvVarEntries(configuration.envVars) { entries, count in
|
||||
surfaceConfig.env_vars = entries
|
||||
surfaceConfig.env_var_count = count
|
||||
return finalizeSurface(
|
||||
app: app,
|
||||
bridge: bridge,
|
||||
configuration: configuration,
|
||||
config: &surfaceConfig,
|
||||
workingDirectory: configuration.workingDirectory,
|
||||
platformSetup: platformSetup
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/// Runs `body` with a C representation of `envVars` (`ghostty_env_var_s`
|
||||
/// entries) that stays valid for the duration of the call.
|
||||
private func withEnvVarEntries<T>(
|
||||
_ envVars: [String: String],
|
||||
_ body: (UnsafeMutablePointer<ghostty_env_var_s>?, Int) -> T
|
||||
) -> T {
|
||||
guard !envVars.isEmpty else { return body(nil, 0) }
|
||||
let strings: [(key: UnsafeMutablePointer<CChar>, value: UnsafeMutablePointer<CChar>)] =
|
||||
envVars.map { (strdup($0.key), strdup($0.value)) }
|
||||
defer {
|
||||
for entry in strings {
|
||||
free(entry.key)
|
||||
free(entry.value)
|
||||
}
|
||||
}
|
||||
var entries = strings.map { ghostty_env_var_s(key: $0.key, value: $0.value) }
|
||||
return entries.withUnsafeMutableBufferPointer { buffer in
|
||||
body(buffer.baseAddress, buffer.count)
|
||||
}
|
||||
}
|
||||
|
||||
func retain(_ bridge: TerminalCallbackBridge) {
|
||||
retainedBridges.append(bridge)
|
||||
}
|
||||
|
||||
func remove(_ bridge: TerminalCallbackBridge) {
|
||||
retainedBridges.removeAll { $0 === bridge }
|
||||
}
|
||||
|
||||
var retainedBridgeCount: Int {
|
||||
retainedBridges.count
|
||||
}
|
||||
|
||||
private func configureBackend(
|
||||
_ config: inout ghostty_surface_config_s,
|
||||
from options: TerminalSurfaceOptions
|
||||
) {
|
||||
guard case let .inMemory(session) = options.backend else {
|
||||
config.backend = GHOSTTY_SURFACE_IO_BACKEND_EXEC
|
||||
return
|
||||
}
|
||||
|
||||
config.backend = GHOSTTY_SURFACE_IO_BACKEND_HOST_MANAGED
|
||||
config.receive_userdata = Unmanaged.passUnretained(session).toOpaque()
|
||||
config.receive_buffer = InMemoryTerminalSession.receiveBufferCallback
|
||||
config.receive_resize = InMemoryTerminalSession.receiveResizeCallback
|
||||
}
|
||||
|
||||
private func finalizeSurface(
|
||||
app: ghostty_app_t,
|
||||
bridge: TerminalCallbackBridge,
|
||||
configuration: TerminalSurfaceOptions,
|
||||
config: inout ghostty_surface_config_s,
|
||||
workingDirectory: String?,
|
||||
platformSetup: (inout ghostty_surface_config_s) -> Void
|
||||
) -> ghostty_surface_t? {
|
||||
guard let workingDirectory else {
|
||||
return buildSurface(
|
||||
app: app,
|
||||
bridge: bridge,
|
||||
configuration: configuration,
|
||||
config: &config,
|
||||
platformSetup: platformSetup
|
||||
)
|
||||
}
|
||||
|
||||
return workingDirectory.withCString { ptr in
|
||||
config.working_directory = ptr
|
||||
return buildSurface(
|
||||
app: app,
|
||||
bridge: bridge,
|
||||
configuration: configuration,
|
||||
config: &config,
|
||||
platformSetup: platformSetup
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private func buildSurface(
|
||||
app: ghostty_app_t,
|
||||
bridge: TerminalCallbackBridge,
|
||||
configuration: TerminalSurfaceOptions,
|
||||
config: inout ghostty_surface_config_s,
|
||||
platformSetup: (inout ghostty_surface_config_s) -> Void
|
||||
) -> ghostty_surface_t? {
|
||||
platformSetup(&config)
|
||||
guard let surface = ghostty_surface_new(app, &config) else {
|
||||
return nil
|
||||
}
|
||||
|
||||
retain(bridge)
|
||||
|
||||
if case let .inMemory(session) = configuration.backend {
|
||||
session.setSurface(surface)
|
||||
}
|
||||
|
||||
return surface
|
||||
}
|
||||
}
|
||||
313
vendor/libghostty-spm/Sources/GhosttyTerminal/Controller/TerminalController.swift
vendored
Normal file
313
vendor/libghostty-spm/Sources/GhosttyTerminal/Controller/TerminalController.swift
vendored
Normal file
@@ -0,0 +1,313 @@
|
||||
//
|
||||
// TerminalController.swift
|
||||
// libghostty-spm
|
||||
//
|
||||
// Created by Lakr233 on 2026/3/16.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import GhosttyKit
|
||||
|
||||
#if canImport(UIKit)
|
||||
import UIKit
|
||||
#elseif canImport(AppKit)
|
||||
import AppKit
|
||||
#endif
|
||||
|
||||
/// Manages the Ghostty app lifecycle, configuration loading, and surface
|
||||
/// creation.
|
||||
///
|
||||
/// `TerminalController` is the **single source of truth** for terminal
|
||||
/// configuration, including the base config, per-session overrides, theme
|
||||
/// colors, and the active color scheme. When any of these change the
|
||||
/// controller re-resolves the effective config and pushes it to ghostty.
|
||||
@MainActor
|
||||
public final class TerminalController {
|
||||
struct PreparedConfig {
|
||||
let rawValue: ghostty_config_t
|
||||
let managedConfigURL: URL?
|
||||
let renderedContents: String
|
||||
}
|
||||
|
||||
struct ConfigurationIssue: Error, CustomStringConvertible {
|
||||
let description: String
|
||||
|
||||
init(_ description: String) {
|
||||
self.description = description
|
||||
}
|
||||
}
|
||||
|
||||
public enum ConfigSource: Sendable, Hashable {
|
||||
case none
|
||||
case file(String)
|
||||
case generated(String)
|
||||
}
|
||||
|
||||
public static let shared = TerminalController()
|
||||
|
||||
static let defaultRenderedConfig = TerminalConfiguration.default.rendered
|
||||
private static var runtimeInitialized = false
|
||||
|
||||
nonisolated(unsafe) var app: ghostty_app_t?
|
||||
nonisolated(unsafe) var config: ghostty_config_t?
|
||||
var retainedBridges: [TerminalCallbackBridge] = []
|
||||
var configSource: ConfigSource
|
||||
var managedConfigURL: URL?
|
||||
var renderedConfigContents: String = TerminalController.defaultRenderedConfig
|
||||
|
||||
public internal(set) var lastConfigurationIssue: String?
|
||||
var onWakeup: (() -> Void)?
|
||||
var shouldProcessWakeup: (() -> Bool)?
|
||||
|
||||
// MARK: - Config Resolution State
|
||||
|
||||
/// The base config before theme/colorScheme are applied.
|
||||
private let baseConfigSource: ConfigSource
|
||||
private var baseConfigTemplate: String = ""
|
||||
|
||||
/// Per-session configuration overrides (e.g. font size changes).
|
||||
public private(set) var terminalConfiguration: TerminalConfiguration
|
||||
|
||||
/// Color theme (light + dark variants).
|
||||
public private(set) var theme: TerminalTheme
|
||||
|
||||
/// The currently active color scheme.
|
||||
public private(set) var effectiveColorScheme: TerminalColorScheme = .light
|
||||
|
||||
// MARK: - Public Accessors
|
||||
|
||||
public var currentConfigSource: ConfigSource {
|
||||
configSource
|
||||
}
|
||||
|
||||
public var renderedConfig: String {
|
||||
renderedConfigContents
|
||||
}
|
||||
|
||||
// MARK: - Initializers
|
||||
|
||||
/// Creates a controller with the default terminal configuration.
|
||||
public convenience init() {
|
||||
self.init(configuration: .default)
|
||||
}
|
||||
|
||||
/// Creates a controller with a fully custom configuration.
|
||||
public convenience init(
|
||||
configuration: TerminalConfiguration,
|
||||
theme: TerminalTheme = .default
|
||||
) {
|
||||
self.init(
|
||||
configSource: .generated(configuration.rendered),
|
||||
theme: theme
|
||||
)
|
||||
}
|
||||
|
||||
/// Creates a controller by composing additional commands on top of
|
||||
/// the default configuration.
|
||||
///
|
||||
/// TerminalController {
|
||||
/// $0.withBackgroundOpacity(0)
|
||||
/// $0.withCustom("keybind", "super+k=text:\\x0c")
|
||||
/// }
|
||||
public convenience init(
|
||||
theme: TerminalTheme = .default,
|
||||
configure: (inout TerminalConfiguration.Builder) -> Void
|
||||
) {
|
||||
self.init(
|
||||
configuration: TerminalConfiguration(
|
||||
startingFrom: .default,
|
||||
configure: configure
|
||||
),
|
||||
theme: theme
|
||||
)
|
||||
}
|
||||
|
||||
/// Creates a controller that loads its configuration from a file.
|
||||
public convenience init(
|
||||
configFilePath: String?,
|
||||
theme: TerminalTheme = .default
|
||||
) {
|
||||
guard let configFilePath else {
|
||||
self.init(configSource: .none, theme: theme)
|
||||
return
|
||||
}
|
||||
self.init(configSource: .file(configFilePath), theme: theme)
|
||||
}
|
||||
|
||||
/// Low-level initialiser for full control over the config source.
|
||||
public init(
|
||||
configSource: ConfigSource = .none,
|
||||
theme: TerminalTheme = .default,
|
||||
terminalConfiguration: TerminalConfiguration = .init()
|
||||
) {
|
||||
Self.initializeRuntimeIfNeeded()
|
||||
|
||||
baseConfigSource = configSource
|
||||
self.theme = theme
|
||||
self.terminalConfiguration = terminalConfiguration
|
||||
self.configSource = configSource
|
||||
|
||||
// Load the base config (without theme) so ghostty validates it.
|
||||
applyInitialConfig(source: configSource)
|
||||
baseConfigTemplate = renderedConfigContents
|
||||
|
||||
// Now apply theme on top and push to ghostty.
|
||||
reconfigure()
|
||||
createApp()
|
||||
}
|
||||
|
||||
// MARK: - Color Scheme
|
||||
|
||||
/// Updates the active color scheme and reconfigures the terminal.
|
||||
///
|
||||
/// Called by platform views when the OS appearance changes. This is
|
||||
/// the only method views need to call — the controller handles all
|
||||
/// config resolution internally.
|
||||
public func setColorScheme(_ scheme: TerminalColorScheme) {
|
||||
setColorScheme(scheme, willChange: nil)
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
func setColorScheme(
|
||||
_ scheme: TerminalColorScheme,
|
||||
willChange: (() -> Void)?
|
||||
) -> Bool {
|
||||
let previous = effectiveColorScheme
|
||||
guard scheme != previous else {
|
||||
if let app {
|
||||
ghostty_app_set_color_scheme(app, scheme.ghosttyValue)
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
let resolved = resolveEffectiveConfig(colorScheme: scheme)
|
||||
guard applyResolvedConfig(
|
||||
resolved,
|
||||
willChange: willChange,
|
||||
applyState: { effectiveColorScheme = scheme }
|
||||
) else {
|
||||
return false
|
||||
}
|
||||
|
||||
if let app {
|
||||
ghostty_app_set_color_scheme(app, scheme.ghosttyValue)
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// MARK: - Theme
|
||||
|
||||
/// Updates the theme and reconfigures the terminal.
|
||||
@discardableResult
|
||||
public func setTheme(_ theme: TerminalTheme) -> Bool {
|
||||
setTheme(theme, willChange: nil)
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
func setTheme(
|
||||
_ theme: TerminalTheme,
|
||||
willChange: (() -> Void)?
|
||||
) -> Bool {
|
||||
guard theme != self.theme else { return false }
|
||||
let resolved = resolveEffectiveConfig(theme: theme)
|
||||
return applyResolvedConfig(
|
||||
resolved,
|
||||
willChange: willChange,
|
||||
applyState: { self.theme = theme }
|
||||
)
|
||||
}
|
||||
|
||||
// MARK: - Terminal Configuration
|
||||
|
||||
/// Updates per-session configuration overrides and reconfigures.
|
||||
@discardableResult
|
||||
public func setTerminalConfiguration(
|
||||
_ terminalConfiguration: TerminalConfiguration
|
||||
) -> Bool {
|
||||
setTerminalConfiguration(terminalConfiguration, willChange: nil)
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
func setTerminalConfiguration(
|
||||
_ terminalConfiguration: TerminalConfiguration,
|
||||
willChange: (() -> Void)?
|
||||
) -> Bool {
|
||||
guard terminalConfiguration != self.terminalConfiguration else { return false }
|
||||
let resolved = resolveEffectiveConfig(terminalConfiguration: terminalConfiguration)
|
||||
return applyResolvedConfig(
|
||||
resolved,
|
||||
willChange: willChange,
|
||||
applyState: { self.terminalConfiguration = terminalConfiguration }
|
||||
)
|
||||
}
|
||||
|
||||
// MARK: - Config Resolution
|
||||
|
||||
@discardableResult
|
||||
private func reconfigure() -> Bool {
|
||||
applyResolvedConfig(resolveEffectiveConfig(), willChange: nil)
|
||||
}
|
||||
|
||||
private func resolveEffectiveConfig() -> (
|
||||
source: ConfigSource, contents: String
|
||||
) {
|
||||
resolveEffectiveConfig(
|
||||
theme: theme,
|
||||
terminalConfiguration: terminalConfiguration,
|
||||
colorScheme: effectiveColorScheme
|
||||
)
|
||||
}
|
||||
|
||||
private func resolveEffectiveConfig(
|
||||
theme: TerminalTheme? = nil,
|
||||
terminalConfiguration: TerminalConfiguration? = nil,
|
||||
colorScheme: TerminalColorScheme? = nil
|
||||
) -> (source: ConfigSource, contents: String) {
|
||||
let nextTheme = theme ?? self.theme
|
||||
let nextTerminalConfiguration = terminalConfiguration ?? self.terminalConfiguration
|
||||
let nextColorScheme = colorScheme ?? effectiveColorScheme
|
||||
let themeConfig = nextTheme.configuration(for: nextColorScheme)
|
||||
if nextTerminalConfiguration.isEmpty, themeConfig.isEmpty {
|
||||
return (baseConfigSource, baseConfigTemplate)
|
||||
}
|
||||
|
||||
let contents = GhosttyConfigRenderer.render(
|
||||
baseContents: baseConfigTemplate,
|
||||
configuration: nextTerminalConfiguration,
|
||||
theme: themeConfig
|
||||
)
|
||||
return (.generated(contents), contents)
|
||||
}
|
||||
|
||||
// MARK: - Tick
|
||||
|
||||
public func tick() {
|
||||
guard let app else { return }
|
||||
ghostty_app_tick(app)
|
||||
}
|
||||
|
||||
func handleWakeup() {
|
||||
guard shouldProcessWakeup?() ?? true else {
|
||||
TerminalDebugLog.log(.lifecycle, "wakeup suspended")
|
||||
return
|
||||
}
|
||||
|
||||
tick()
|
||||
onWakeup?()
|
||||
}
|
||||
|
||||
private static func initializeRuntimeIfNeeded() {
|
||||
guard !runtimeInitialized else { return }
|
||||
runtimeInitialized = true
|
||||
ghostty_init(0, nil)
|
||||
}
|
||||
|
||||
deinit {
|
||||
if let app { ghostty_app_free(app) }
|
||||
if let config { ghostty_config_free(config) }
|
||||
if let managedConfigURL {
|
||||
try? FileManager.default.removeItem(at: managedConfigURL)
|
||||
}
|
||||
}
|
||||
}
|
||||
304
vendor/libghostty-spm/Sources/GhosttyTerminal/Debug/TerminalDebugLog.swift
vendored
Normal file
304
vendor/libghostty-spm/Sources/GhosttyTerminal/Debug/TerminalDebugLog.swift
vendored
Normal file
@@ -0,0 +1,304 @@
|
||||
import Foundation
|
||||
import GhosttyKit
|
||||
|
||||
public struct TerminalDebugCategory: OptionSet, Sendable {
|
||||
public let rawValue: UInt16
|
||||
|
||||
public init(rawValue: UInt16) {
|
||||
self.rawValue = rawValue
|
||||
}
|
||||
|
||||
public static let lifecycle = Self(rawValue: 1 << 0)
|
||||
public static let metrics = Self(rawValue: 1 << 1)
|
||||
public static let input = Self(rawValue: 1 << 2)
|
||||
public static let output = Self(rawValue: 1 << 3)
|
||||
public static let ime = Self(rawValue: 1 << 4)
|
||||
public static let actions = Self(rawValue: 1 << 5)
|
||||
public static let render = Self(rawValue: 1 << 6)
|
||||
|
||||
public static let standard: Self = [
|
||||
.lifecycle,
|
||||
.metrics,
|
||||
.input,
|
||||
.output,
|
||||
.ime,
|
||||
.actions,
|
||||
]
|
||||
|
||||
public static let all: Self = [
|
||||
.standard,
|
||||
.render,
|
||||
]
|
||||
}
|
||||
|
||||
public enum TerminalDebugLog {
|
||||
public typealias Sink = @Sendable (String) -> Void
|
||||
|
||||
private struct Snapshot {
|
||||
let isEnabled: Bool
|
||||
let categories: TerminalDebugCategory
|
||||
let sink: Sink
|
||||
}
|
||||
|
||||
private final class Store: @unchecked Sendable {
|
||||
let lock = NSLock()
|
||||
var isEnabled = false
|
||||
var categories: TerminalDebugCategory = .standard
|
||||
var sink: Sink = { message in
|
||||
Swift.print(message)
|
||||
}
|
||||
}
|
||||
|
||||
private static let store = Store()
|
||||
|
||||
public static var isEnabled: Bool {
|
||||
get {
|
||||
withSnapshot { $0.isEnabled }
|
||||
}
|
||||
set {
|
||||
updateStore { $0.isEnabled = newValue }
|
||||
}
|
||||
}
|
||||
|
||||
public static var categories: TerminalDebugCategory {
|
||||
get {
|
||||
withSnapshot { $0.categories }
|
||||
}
|
||||
set {
|
||||
updateStore { $0.categories = newValue }
|
||||
}
|
||||
}
|
||||
|
||||
public static var sink: Sink {
|
||||
get {
|
||||
withSnapshot { $0.sink }
|
||||
}
|
||||
set {
|
||||
updateStore { $0.sink = newValue }
|
||||
}
|
||||
}
|
||||
|
||||
public static func enable(_ categories: TerminalDebugCategory = .standard) {
|
||||
updateStore {
|
||||
$0.isEnabled = true
|
||||
$0.categories = categories
|
||||
}
|
||||
}
|
||||
|
||||
public static func disable() {
|
||||
updateStore { $0.isEnabled = false }
|
||||
}
|
||||
|
||||
static func log(
|
||||
_ category: TerminalDebugCategory,
|
||||
_ message: @autoclosure () -> String
|
||||
) {
|
||||
let snapshot = snapshot()
|
||||
guard snapshot.isEnabled else { return }
|
||||
guard snapshot.categories.contains(category) else { return }
|
||||
snapshot.sink(
|
||||
"[GhosttyTerminal][\(timestamp())][\(label(for: category))] \(message())"
|
||||
)
|
||||
}
|
||||
|
||||
static func describe(_ string: String?, limit: Int = 96) -> String {
|
||||
guard let string else { return "nil" }
|
||||
return "\"\(escaped(string, limit: limit))\""
|
||||
}
|
||||
|
||||
static func describe(_ data: Data, limit: Int = 48) -> String {
|
||||
let preview = data.prefix(limit)
|
||||
let text = String(decoding: preview, as: UTF8.self)
|
||||
let hex = preview.map { String(format: "%02X", $0) }.joined(separator: " ")
|
||||
let suffix = data.count > limit ? "..." : ""
|
||||
return "bytes=\(data.count) utf8=\"\(escaped(text, limit: limit))\(suffix)\" hex=\(hex)\(suffix)"
|
||||
}
|
||||
|
||||
static func describe(_ range: NSRange) -> String {
|
||||
"{location=\(range.location), length=\(range.length)}"
|
||||
}
|
||||
|
||||
static func describe(_ action: ghostty_input_action_e) -> String {
|
||||
switch action {
|
||||
case GHOSTTY_ACTION_PRESS:
|
||||
"press"
|
||||
case GHOSTTY_ACTION_RELEASE:
|
||||
"release"
|
||||
case GHOSTTY_ACTION_REPEAT:
|
||||
"repeat"
|
||||
default:
|
||||
"unknown(\(action.rawValue))"
|
||||
}
|
||||
}
|
||||
|
||||
static func describe(_ state: ghostty_input_mouse_state_e) -> String {
|
||||
switch state {
|
||||
case GHOSTTY_MOUSE_PRESS:
|
||||
"press"
|
||||
case GHOSTTY_MOUSE_RELEASE:
|
||||
"release"
|
||||
default:
|
||||
"unknown(\(state.rawValue))"
|
||||
}
|
||||
}
|
||||
|
||||
static func describe(_ tag: ghostty_action_tag_e) -> String {
|
||||
switch tag {
|
||||
case GHOSTTY_ACTION_CELL_SIZE:
|
||||
"cell_size"
|
||||
case GHOSTTY_ACTION_SET_TITLE:
|
||||
"set_title"
|
||||
case GHOSTTY_ACTION_SET_TAB_TITLE:
|
||||
"set_tab_title"
|
||||
case GHOSTTY_ACTION_RING_BELL:
|
||||
"ring_bell"
|
||||
case GHOSTTY_ACTION_RENDER:
|
||||
"render"
|
||||
case GHOSTTY_ACTION_CONFIG_CHANGE:
|
||||
"config_change"
|
||||
case GHOSTTY_ACTION_RELOAD_CONFIG:
|
||||
"reload_config"
|
||||
default:
|
||||
"tag(\(tag.rawValue))"
|
||||
}
|
||||
}
|
||||
|
||||
private static func withSnapshot<T>(
|
||||
_ body: (Snapshot) -> T
|
||||
) -> T {
|
||||
body(snapshot())
|
||||
}
|
||||
|
||||
private static func snapshot() -> Snapshot {
|
||||
store.lock.lock()
|
||||
defer { store.lock.unlock() }
|
||||
return Snapshot(
|
||||
isEnabled: store.isEnabled,
|
||||
categories: store.categories,
|
||||
sink: store.sink
|
||||
)
|
||||
}
|
||||
|
||||
private static func updateStore(
|
||||
_ body: (Store) -> Void
|
||||
) {
|
||||
store.lock.lock()
|
||||
defer { store.lock.unlock() }
|
||||
body(store)
|
||||
}
|
||||
|
||||
private static func label(
|
||||
for category: TerminalDebugCategory
|
||||
) -> String {
|
||||
switch category {
|
||||
case .lifecycle:
|
||||
"lifecycle"
|
||||
case .metrics:
|
||||
"metrics"
|
||||
case .input:
|
||||
"input"
|
||||
case .output:
|
||||
"output"
|
||||
case .ime:
|
||||
"ime"
|
||||
case .actions:
|
||||
"actions"
|
||||
case .render:
|
||||
"render"
|
||||
default:
|
||||
"debug"
|
||||
}
|
||||
}
|
||||
|
||||
private static func timestamp() -> String {
|
||||
String(format: "%.3f", Date().timeIntervalSince1970)
|
||||
}
|
||||
|
||||
private static func escaped(
|
||||
_ string: String,
|
||||
limit: Int
|
||||
) -> String {
|
||||
var result = ""
|
||||
var emitted = 0
|
||||
for scalar in string.unicodeScalars {
|
||||
guard emitted < limit else {
|
||||
result.append("...")
|
||||
break
|
||||
}
|
||||
|
||||
switch scalar.value {
|
||||
case 0x09:
|
||||
result.append("\\t")
|
||||
case 0x0A:
|
||||
result.append("\\n")
|
||||
case 0x0D:
|
||||
result.append("\\r")
|
||||
case 0x1B:
|
||||
result.append("\\e")
|
||||
case 0x20 ..< 0x7F:
|
||||
result.append(Character(scalar))
|
||||
default:
|
||||
if scalar.value < 0x20 || scalar.value == 0x7F {
|
||||
result.append(String(format: "\\u{%02X}", Int(scalar.value)))
|
||||
} else {
|
||||
result.append(Character(scalar))
|
||||
}
|
||||
}
|
||||
|
||||
emitted += 1
|
||||
}
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
extension TerminalGridMetrics {
|
||||
var debugSummary: String {
|
||||
"cols=\(columns) rows=\(rows) pixels=\(widthPixels)x\(heightPixels) cell=\(cellWidthPixels)x\(cellHeightPixels)"
|
||||
}
|
||||
}
|
||||
|
||||
extension TerminalViewportMetrics {
|
||||
var debugSummary: String {
|
||||
"\(surfaceSize.debugSummary) scale=\(String(format: "%.2f", scale))"
|
||||
}
|
||||
}
|
||||
|
||||
extension TerminalSessionBackend {
|
||||
var debugSummary: String {
|
||||
switch self {
|
||||
case .exec:
|
||||
"exec"
|
||||
case .inMemory:
|
||||
"in-memory"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension TerminalSurfaceOptions {
|
||||
var debugSummary: String {
|
||||
let fontSizeDescription = fontSize.map { String($0) } ?? "nil"
|
||||
return "backend=\(backend.debugSummary) fontSize=\(fontSizeDescription) workingDirectory=\(workingDirectory ?? "nil") context=\(context.debugSummary)"
|
||||
}
|
||||
}
|
||||
|
||||
extension TerminalSurfaceContext {
|
||||
var debugSummary: String {
|
||||
switch self {
|
||||
case .window:
|
||||
"window"
|
||||
case .split:
|
||||
"split"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension TerminalHardwareKeyDelivery {
|
||||
var debugSummary: String {
|
||||
switch self {
|
||||
case let .ghostty(key):
|
||||
"ghostty(\(key.rawValue))"
|
||||
case let .data(data):
|
||||
"data(\(TerminalDebugLog.describe(data)))"
|
||||
}
|
||||
}
|
||||
}
|
||||
291
vendor/libghostty-spm/Sources/GhosttyTerminal/InMemory/InMemoryTerminalSession.swift
vendored
Normal file
291
vendor/libghostty-spm/Sources/GhosttyTerminal/InMemory/InMemoryTerminalSession.swift
vendored
Normal file
@@ -0,0 +1,291 @@
|
||||
//
|
||||
// InMemoryTerminalSession.swift
|
||||
// libghostty-spm
|
||||
//
|
||||
// Created by Lakr233 on 2026/3/16.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import GhosttyKit
|
||||
|
||||
public final class InMemoryTerminalSession: @unchecked Sendable {
|
||||
private static let slowSurfaceWriteThreshold: TimeInterval = 0.5
|
||||
|
||||
private let resizeLock = NSLock()
|
||||
private let surfaceAccess: InMemoryTerminalSurfaceAccess
|
||||
private var lastResize: InMemoryTerminalViewport?
|
||||
private let writeHandler: @Sendable (Data) -> Void
|
||||
private let resizeHandler: @Sendable (InMemoryTerminalViewport) -> Void
|
||||
|
||||
public init(
|
||||
write: @escaping @Sendable (Data) -> Void,
|
||||
resize: @escaping @Sendable (InMemoryTerminalViewport) -> Void
|
||||
) {
|
||||
writeHandler = write
|
||||
resizeHandler = resize
|
||||
surfaceAccess = InMemoryTerminalSurfaceAccess(
|
||||
write: Self.writeToSurface,
|
||||
processExit: Self.reportProcessExit
|
||||
)
|
||||
}
|
||||
|
||||
init(
|
||||
write: @escaping @Sendable (Data) -> Void,
|
||||
resize: @escaping @Sendable (InMemoryTerminalViewport) -> Void,
|
||||
surfaceWrite: @escaping InMemoryTerminalSurfaceAccess.Write,
|
||||
processExit: @escaping InMemoryTerminalSurfaceAccess.ProcessExit =
|
||||
InMemoryTerminalSession.reportProcessExit
|
||||
) {
|
||||
writeHandler = write
|
||||
resizeHandler = resize
|
||||
surfaceAccess = InMemoryTerminalSurfaceAccess(
|
||||
write: surfaceWrite,
|
||||
processExit: processExit
|
||||
)
|
||||
}
|
||||
|
||||
// MARK: - Surface Lifecycle
|
||||
|
||||
func setSurface(_ surface: ghostty_surface_t?) {
|
||||
surfaceAccess.setSurface(surface)
|
||||
TerminalDebugLog.log(
|
||||
.lifecycle,
|
||||
"in-memory session surface=\(surface == nil ? "nil" : "set")"
|
||||
)
|
||||
}
|
||||
|
||||
func clearSurface(ifMatches expectedSurface: ghostty_surface_t?) {
|
||||
guard surfaceAccess.clearSurface(ifMatches: expectedSurface) else {
|
||||
TerminalDebugLog.log(
|
||||
.lifecycle,
|
||||
"in-memory session clear skipped expected=\(expectedSurface == nil ? "nil" : "set") current=\(surfaceAccess.currentSurface == nil ? "nil" : "set")"
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
TerminalDebugLog.log(.lifecycle, "in-memory session surface=nil matched")
|
||||
}
|
||||
|
||||
var currentSurface: ghostty_surface_t? {
|
||||
surfaceAccess.currentSurface
|
||||
}
|
||||
|
||||
// MARK: - Viewport Read
|
||||
|
||||
/// Returns the active viewport as a UTF-8 string, or `nil` if no surface
|
||||
/// is attached. Lines are separated by `\n`. The `ghostty_text_s`
|
||||
/// lifecycle (allocate via `ghostty_surface_read_text`, free via
|
||||
/// `ghostty_surface_free_text`) is fully encapsulated — callers never
|
||||
/// touch the C buffer.
|
||||
///
|
||||
/// Selection grammar: `(VIEWPORT, TOP_LEFT)` to `(VIEWPORT, BOTTOM_RIGHT)`
|
||||
/// with `rectangle: false` (linear flow). This reads exactly the visible
|
||||
/// rows and ignores scrollback. Empty viewports return an empty string.
|
||||
///
|
||||
/// Thread-safe: keeps the surface alive for the duration of the read,
|
||||
/// preventing access against a surface mid-replacement.
|
||||
public func readViewportText() -> String? {
|
||||
surfaceAccess.withCurrentSurface { surface in
|
||||
let topLeft = ghostty_point_s(
|
||||
tag: GHOSTTY_POINT_VIEWPORT,
|
||||
coord: GHOSTTY_POINT_COORD_TOP_LEFT,
|
||||
x: 0,
|
||||
y: 0
|
||||
)
|
||||
let bottomRight = ghostty_point_s(
|
||||
tag: GHOSTTY_POINT_VIEWPORT,
|
||||
coord: GHOSTTY_POINT_COORD_BOTTOM_RIGHT,
|
||||
x: 0,
|
||||
y: 0
|
||||
)
|
||||
let selection = ghostty_selection_s(
|
||||
top_left: topLeft,
|
||||
bottom_right: bottomRight,
|
||||
rectangle: false
|
||||
)
|
||||
|
||||
var out = ghostty_text_s()
|
||||
guard ghostty_surface_read_text(surface, selection, &out) else {
|
||||
return nil
|
||||
}
|
||||
defer { ghostty_surface_free_text(surface, &out) }
|
||||
|
||||
guard let textPtr = out.text, out.text_len > 0 else {
|
||||
return ""
|
||||
}
|
||||
let bytes = UnsafeBufferPointer(start: textPtr, count: Int(out.text_len))
|
||||
.map { UInt8(bitPattern: $0) }
|
||||
return String(decoding: bytes, as: UTF8.self)
|
||||
} ?? nil
|
||||
}
|
||||
|
||||
func updateViewport(_ size: TerminalGridMetrics) {
|
||||
TerminalDebugLog.log(.metrics, "in-memory viewport update \(size.debugSummary)")
|
||||
dispatchResize(InMemoryTerminalViewport(
|
||||
columns: size.columns,
|
||||
rows: size.rows,
|
||||
widthPixels: size.widthPixels,
|
||||
heightPixels: size.heightPixels,
|
||||
cellWidthPixels: size.cellWidthPixels,
|
||||
cellHeightPixels: size.cellHeightPixels
|
||||
))
|
||||
}
|
||||
|
||||
// MARK: - Receiving Data
|
||||
|
||||
/// Enqueue data for the terminal from the host backend.
|
||||
///
|
||||
/// Writes are processed in order on a per-session serial queue so parsing
|
||||
/// cannot block the caller or the main thread.
|
||||
public func receive(_ data: Data) {
|
||||
guard surfaceAccess.enqueueWrite(data) else {
|
||||
TerminalDebugLog.log(
|
||||
.output,
|
||||
"terminal <- host dropped \(TerminalDebugLog.describe(data))"
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
TerminalDebugLog.log(
|
||||
.output,
|
||||
"terminal <- host \(TerminalDebugLog.describe(data))"
|
||||
)
|
||||
}
|
||||
|
||||
/// Feed a UTF-8 string into the terminal from the host backend.
|
||||
public func receive(_ string: String) {
|
||||
guard let data = string.data(using: .utf8) else { return }
|
||||
receive(data)
|
||||
}
|
||||
|
||||
/// Inject input bytes directly into the host-side consumer.
|
||||
///
|
||||
/// This bypasses `ghostty_surface_key` translation and is intended for
|
||||
/// control sequences that the in-memory backend must interpret itself.
|
||||
public func sendInput(_ data: Data) {
|
||||
TerminalDebugLog.log(
|
||||
.input,
|
||||
"host <- direct input \(TerminalDebugLog.describe(data))"
|
||||
)
|
||||
writeHandler(data)
|
||||
}
|
||||
|
||||
// MARK: - Process Exit
|
||||
|
||||
/// Enqueue a host-managed process exit after all previously received data.
|
||||
public func finish(exitCode: UInt32, runtimeMilliseconds: UInt64) {
|
||||
guard surfaceAccess.enqueueProcessExit(
|
||||
exitCode: exitCode,
|
||||
runtimeMilliseconds: runtimeMilliseconds
|
||||
) else {
|
||||
TerminalDebugLog.log(
|
||||
.lifecycle,
|
||||
"process exit ignored: missing surface exitCode=\(exitCode) runtimeMs=\(runtimeMilliseconds)"
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
TerminalDebugLog.log(
|
||||
.lifecycle,
|
||||
"process exit exitCode=\(exitCode) runtimeMs=\(runtimeMilliseconds)"
|
||||
)
|
||||
}
|
||||
|
||||
// MARK: - C Callbacks
|
||||
|
||||
static let receiveBufferCallback: ghostty_surface_receive_buffer_cb = { userdata, ptr, len in
|
||||
guard let userdata, let ptr else { return }
|
||||
let session = Unmanaged<InMemoryTerminalSession>
|
||||
.fromOpaque(userdata)
|
||||
.takeUnretainedValue()
|
||||
let data = Data(bytes: ptr, count: len)
|
||||
TerminalDebugLog.log(
|
||||
.input,
|
||||
"host <- terminal \(TerminalDebugLog.describe(data))"
|
||||
)
|
||||
session.writeHandler(data)
|
||||
}
|
||||
|
||||
static let receiveResizeCallback: ghostty_surface_receive_resize_cb = { userdata, cols, rows, widthPx, heightPx in
|
||||
guard let userdata else { return }
|
||||
let session = Unmanaged<InMemoryTerminalSession>
|
||||
.fromOpaque(userdata)
|
||||
.takeUnretainedValue()
|
||||
TerminalDebugLog.log(
|
||||
.metrics,
|
||||
"receive resize cols=\(cols) rows=\(rows) pixels=\(widthPx)x\(heightPx)"
|
||||
)
|
||||
session.dispatchResize(InMemoryTerminalViewport(
|
||||
columns: cols,
|
||||
rows: rows,
|
||||
widthPixels: widthPx,
|
||||
heightPixels: heightPx
|
||||
))
|
||||
}
|
||||
|
||||
private func dispatchResize(_ resize: InMemoryTerminalViewport) {
|
||||
resizeLock.lock()
|
||||
let mergedResize = mergedResize(resize)
|
||||
guard mergedResize != lastResize else {
|
||||
resizeLock.unlock()
|
||||
TerminalDebugLog.log(
|
||||
.metrics,
|
||||
"resize unchanged cols=\(mergedResize.columns) rows=\(mergedResize.rows) pixels=\(mergedResize.widthPixels)x\(mergedResize.heightPixels) cell=\(mergedResize.cellWidthPixels)x\(mergedResize.cellHeightPixels)"
|
||||
)
|
||||
return
|
||||
}
|
||||
lastResize = mergedResize
|
||||
resizeLock.unlock()
|
||||
|
||||
TerminalDebugLog.log(
|
||||
.metrics,
|
||||
"resize dispatched cols=\(mergedResize.columns) rows=\(mergedResize.rows) pixels=\(mergedResize.widthPixels)x\(mergedResize.heightPixels) cell=\(mergedResize.cellWidthPixels)x\(mergedResize.cellHeightPixels)"
|
||||
)
|
||||
resizeHandler(mergedResize)
|
||||
}
|
||||
|
||||
private func mergedResize(_ resize: InMemoryTerminalViewport) -> InMemoryTerminalViewport {
|
||||
guard let lastResize else { return resize }
|
||||
|
||||
return InMemoryTerminalViewport(
|
||||
columns: resize.columns,
|
||||
rows: resize.rows,
|
||||
widthPixels: resize.widthPixels == 0 ? lastResize.widthPixels : resize.widthPixels,
|
||||
heightPixels: resize.heightPixels == 0 ? lastResize.heightPixels : resize.heightPixels,
|
||||
cellWidthPixels: resize.cellWidthPixels == 0 ? lastResize.cellWidthPixels : resize.cellWidthPixels,
|
||||
cellHeightPixels: resize.cellHeightPixels == 0 ? lastResize.cellHeightPixels : resize.cellHeightPixels
|
||||
)
|
||||
}
|
||||
|
||||
func waitForPendingOutput() {
|
||||
surfaceAccess.waitForPendingOutput()
|
||||
}
|
||||
|
||||
private static func writeToSurface(_ surface: ghostty_surface_t, _ data: Data) {
|
||||
let start = ProcessInfo.processInfo.systemUptime
|
||||
defer {
|
||||
let duration = ProcessInfo.processInfo.systemUptime - start
|
||||
if duration >= slowSurfaceWriteThreshold {
|
||||
TerminalDebugLog.log(
|
||||
.output,
|
||||
"surface write slow bytes=\(data.count) duration=\(String(format: "%.3f", duration))s"
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
data.withUnsafeBytes { buffer in
|
||||
guard let ptr = buffer.baseAddress?.assumingMemoryBound(to: UInt8.self) else {
|
||||
return
|
||||
}
|
||||
ghostty_surface_write_buffer(surface, ptr, UInt(buffer.count))
|
||||
}
|
||||
}
|
||||
|
||||
private static func reportProcessExit(
|
||||
_ surface: ghostty_surface_t,
|
||||
_ exitCode: UInt32,
|
||||
_ runtimeMilliseconds: UInt64
|
||||
) {
|
||||
ghostty_surface_process_exit(surface, exitCode, runtimeMilliseconds)
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user