commit c5bd543fe0f9859371efda61d49fde10b4b0f9ba Author: Lawrence Chen <54008264+lawrencecchen@users.noreply.github.com> Date: Thu Jan 22 01:16:24 2026 -0800 Initial commit: macOS terminal app with vertical tabs using libghostty Features: - Vertical tabs sidebar with SwiftUI - Terminal emulation via GhosttyKit.xcframework (libghostty) - Keyboard shortcuts: Cmd+T/N, Ctrl+Shift+` (new tab), Cmd+W (close), Cmd+Shift+[/], Ctrl+Tab (navigation), Cmd+1-9 (jump to tab) - Reads Ghostty config from ~/Library/Application Support/com.mitchellh.ghostty/config - Metal-based rendering diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..1fce88aa --- /dev/null +++ b/.gitignore @@ -0,0 +1,14 @@ +# Xcode +.build/ +DerivedData/ +*.xcuserstate +xcuserdata/ + +# macOS +.DS_Store + +# Swift Package Manager +.swiftpm/ + +# GhosttyKit binary (rebuild from /tmp/ghostty with zig build) +GhosttyKit.xcframework/ diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 00000000..f6835f96 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,47 @@ +# GhosttyTabs + +A macOS terminal app with vertical tabs, using libghostty (GhosttyKit.xcframework) for terminal emulation. + +## User Preferences + +- **Always use Release builds** when building and launching for testing +- Build libghostty with `-Doptimize=ReleaseFast` for performance + +## Development + +### Build and launch (Release) +```bash +cd /Users/lawrencechen/fun/cmux-terminal/GhosttyTabs +pkill -9 GhosttyTabs 2>/dev/null +xcodebuild -scheme GhosttyTabs -configuration Release build +open ~/Library/Developer/Xcode/DerivedData/GhosttyTabs-cbjivvtpirygxbbgqlpdpiiyjnwh/Build/Products/Release/GhosttyTabs.app +``` + +### Rebuild libghostty (optimized) +```bash +cd /tmp/ghostty +zig build -Demit-xcframework=true -Dxcframework-target=native -Doptimize=ReleaseFast +cp -R /tmp/ghostty/macos/GhosttyKit.xcframework /Users/lawrencechen/fun/cmux-terminal/GhosttyTabs/ +``` + +### Project structure +- `Sources/` - Swift source files + - `GhosttyTabsApp.swift` - App entry point with keyboard shortcuts + - `ContentView.swift` - Main UI with vertical tabs sidebar + - `TabManager.swift` - Tab state management + - `GhosttyTerminalView.swift` - libghostty terminal integration + - `GhosttyConfig.swift` - Ghostty config parser +- `GhosttyKit.xcframework/` - libghostty static library +- `ghostty.h` - Ghostty C API header +- `GhosttyTabs-Bridging-Header.h` - Swift bridging header + +### Keyboard Shortcuts +- `Cmd+T` / `Cmd+N` / `Ctrl+Shift+`` - New tab +- `Cmd+W` - Close tab +- `Cmd+Shift+]` / `Ctrl+Tab` - Next tab +- `Cmd+Shift+[` / `Ctrl+Shift+Tab` - Previous tab +- `Cmd+1-9` - Jump to tab by number + +### Config +Reads user's Ghostty config from: +`~/Library/Application Support/com.mitchellh.ghostty/config` diff --git a/GhosttyTabs-Bridging-Header.h b/GhosttyTabs-Bridging-Header.h new file mode 100644 index 00000000..5d5dbded --- /dev/null +++ b/GhosttyTabs-Bridging-Header.h @@ -0,0 +1 @@ +#import "ghostty.h" diff --git a/GhosttyTabs.xcodeproj/project.pbxproj b/GhosttyTabs.xcodeproj/project.pbxproj new file mode 100644 index 00000000..6ec58b58 --- /dev/null +++ b/GhosttyTabs.xcodeproj/project.pbxproj @@ -0,0 +1,317 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 56; + objects = { + +/* Begin PBXBuildFile section */ + A5001001 /* GhosttyTabsApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5001011 /* GhosttyTabsApp.swift */; }; + A5001002 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5001012 /* ContentView.swift */; }; + A5001003 /* TabManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5001013 /* TabManager.swift */; }; + A5001004 /* GhosttyConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5001014 /* GhosttyConfig.swift */; }; + A5001005 /* GhosttyTerminalView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5001015 /* GhosttyTerminalView.swift */; }; + A5001006 /* GhosttyKit.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = A5001016 /* GhosttyKit.xcframework */; }; +/* End PBXBuildFile section */ + +/* Begin PBXCopyFilesBuildPhase section */ + A5001020 /* Embed Frameworks */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 10; + files = ( + ); + name = "Embed Frameworks"; + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXCopyFilesBuildPhase section */ + +/* Begin PBXFileReference section */ + A5001000 /* GhosttyTabs.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = GhosttyTabs.app; sourceTree = BUILT_PRODUCTS_DIR; }; + A5001011 /* GhosttyTabsApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GhosttyTabsApp.swift; sourceTree = ""; }; + A5001012 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = ""; }; + A5001013 /* TabManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TabManager.swift; sourceTree = ""; }; + A5001014 /* GhosttyConfig.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GhosttyConfig.swift; sourceTree = ""; }; + A5001015 /* GhosttyTerminalView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GhosttyTerminalView.swift; sourceTree = ""; }; + A5001016 /* GhosttyKit.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; path = GhosttyKit.xcframework; sourceTree = ""; }; + A5001017 /* ghostty.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ghostty.h; sourceTree = ""; }; + A5001018 /* GhosttyTabs-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "GhosttyTabs-Bridging-Header.h"; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + A5001030 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + A5001006 /* GhosttyKit.xcframework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + A5001040 = { + isa = PBXGroup; + children = ( + A5001041 /* Sources */, + A5001016 /* GhosttyKit.xcframework */, + A5001017 /* ghostty.h */, + A5001018 /* GhosttyTabs-Bridging-Header.h */, + A5001042 /* Products */, + ); + sourceTree = ""; + }; + A5001041 /* Sources */ = { + isa = PBXGroup; + children = ( + A5001011 /* GhosttyTabsApp.swift */, + A5001012 /* ContentView.swift */, + A5001013 /* TabManager.swift */, + A5001014 /* GhosttyConfig.swift */, + A5001015 /* GhosttyTerminalView.swift */, + ); + path = Sources; + sourceTree = ""; + }; + A5001042 /* Products */ = { + isa = PBXGroup; + children = ( + A5001000 /* GhosttyTabs.app */, + ); + name = Products; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + A5001050 /* GhosttyTabs */ = { + isa = PBXNativeTarget; + buildConfigurationList = A5001060 /* Build configuration list for PBXNativeTarget "GhosttyTabs" */; + buildPhases = ( + A5001051 /* Sources */, + A5001030 /* Frameworks */, + A5001020 /* Embed Frameworks */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = GhosttyTabs; + productName = GhosttyTabs; + productReference = A5001000 /* GhosttyTabs.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + A5001070 /* Project object */ = { + isa = PBXProject; + attributes = { + BuildIndependentTargetsInParallel = 1; + LastSwiftUpdateCheck = 1500; + LastUpgradeCheck = 1500; + }; + buildConfigurationList = A5001071 /* Build configuration list for PBXProject "GhosttyTabs" */; + compatibilityVersion = "Xcode 14.0"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = A5001040; + productRefGroup = A5001042 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + A5001050 /* GhosttyTabs */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXSourcesBuildPhase section */ + A5001051 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + A5001001 /* GhosttyTabsApp.swift in Sources */, + A5001002 /* ContentView.swift in Sources */, + A5001003 /* TabManager.swift in Sources */, + A5001004 /* GhosttyConfig.swift in Sources */, + A5001005 /* GhosttyTerminalView.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin XCBuildConfiguration section */ + A5001080 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + 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; + MACOSX_DEPLOYMENT_TARGET = 13.0; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = macosx; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + }; + name = Debug; + }; + A5001081 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + 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; + MACOSX_DEPLOYMENT_TARGET = 13.0; + MTL_ENABLE_DEBUG_INFO = NO; + MTL_FAST_MATH = YES; + SDKROOT = macosx; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + }; + name = Release; + }; + A5001082 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CODE_SIGN_ENTITLEMENTS = ""; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = ""; + ENABLE_HARDENED_RUNTIME = NO; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.developer-tools"; + INFOPLIST_KEY_NSHumanReadableCopyright = ""; + INFOPLIST_KEY_NSMainStoryboardFile = ""; + INFOPLIST_KEY_NSPrincipalClass = NSApplication; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + ); + MARKETING_VERSION = 1.0; + OTHER_LDFLAGS = ( + "-lc++", + "-framework", + Metal, + "-framework", + QuartzCore, + "-framework", + IOSurface, + "-framework", + UniformTypeIdentifiers, + "-framework", + Carbon, + ); + PRODUCT_BUNDLE_IDENTIFIER = com.ghosttytabs.app; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_OBJC_BRIDGING_HEADER = "GhosttyTabs-Bridging-Header.h"; + SWIFT_VERSION = 5.0; + }; + name = Debug; + }; + A5001083 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CODE_SIGN_ENTITLEMENTS = ""; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = ""; + ENABLE_HARDENED_RUNTIME = NO; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.developer-tools"; + INFOPLIST_KEY_NSHumanReadableCopyright = ""; + INFOPLIST_KEY_NSMainStoryboardFile = ""; + INFOPLIST_KEY_NSPrincipalClass = NSApplication; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + ); + MARKETING_VERSION = 1.0; + OTHER_LDFLAGS = ( + "-lc++", + "-framework", + Metal, + "-framework", + QuartzCore, + "-framework", + IOSurface, + "-framework", + UniformTypeIdentifiers, + "-framework", + Carbon, + ); + PRODUCT_BUNDLE_IDENTIFIER = com.ghosttytabs.app; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_OBJC_BRIDGING_HEADER = "GhosttyTabs-Bridging-Header.h"; + SWIFT_VERSION = 5.0; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + A5001060 /* Build configuration list for PBXNativeTarget "GhosttyTabs" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + A5001082 /* Debug */, + A5001083 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + A5001071 /* Build configuration list for PBXProject "GhosttyTabs" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + A5001080 /* Debug */, + A5001081 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = A5001070 /* Project object */; +} diff --git a/Package.resolved b/Package.resolved new file mode 100644 index 00000000..0572c520 --- /dev/null +++ b/Package.resolved @@ -0,0 +1,23 @@ +{ + "pins" : [ + { + "identity" : "swift-argument-parser", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-argument-parser", + "state" : { + "revision" : "c5d11a805e765f52ba34ec7284bd4fcd6ba68615", + "version" : "1.7.0" + } + }, + { + "identity" : "swiftterm", + "kind" : "remoteSourceControl", + "location" : "https://github.com/migueldeicaza/SwiftTerm.git", + "state" : { + "revision" : "0b8d99bd19b694df44e1ccaa3891309719d34330", + "version" : "1.5.1" + } + } + ], + "version" : 2 +} diff --git a/Package.swift b/Package.swift new file mode 100644 index 00000000..a02b7770 --- /dev/null +++ b/Package.swift @@ -0,0 +1,22 @@ +// swift-tools-version:5.9 +import PackageDescription + +let package = Package( + name: "GhosttyTabs", + platforms: [ + .macOS(.v13) + ], + products: [ + .executable(name: "GhosttyTabs", targets: ["GhosttyTabs"]) + ], + dependencies: [ + .package(url: "https://github.com/migueldeicaza/SwiftTerm.git", from: "1.2.0") + ], + targets: [ + .executableTarget( + name: "GhosttyTabs", + dependencies: ["SwiftTerm"], + path: "Sources" + ) + ] +) diff --git a/Sources/ContentView.swift b/Sources/ContentView.swift new file mode 100644 index 00000000..95c07504 --- /dev/null +++ b/Sources/ContentView.swift @@ -0,0 +1,120 @@ +import SwiftUI + +struct ContentView: View { + @EnvironmentObject var tabManager: TabManager + @State private var sidebarWidth: CGFloat = 200 + + var body: some View { + HStack(spacing: 0) { + // Vertical Tabs Sidebar + VerticalTabsSidebar(sidebarWidth: sidebarWidth) + .frame(width: sidebarWidth) + + // Divider + Rectangle() + .fill(Color(nsColor: .separatorColor)) + .frame(width: 1) + + // Terminal Content + if let selectedId = tabManager.selectedTabId, + let tab = tabManager.tabs.first(where: { $0.id == selectedId }) { + GhosttyTerminalView() + .id(tab.id) + } else { + Color(nsColor: .windowBackgroundColor) + } + } + .frame(minWidth: 800, minHeight: 600) + .background(Color(nsColor: .windowBackgroundColor)) + } +} + +struct VerticalTabsSidebar: View { + @EnvironmentObject var tabManager: TabManager + let sidebarWidth: CGFloat + + var body: some View { + VStack(spacing: 0) { + // Header with title + HStack { + Text("Tabs") + .font(.headline) + .foregroundColor(.secondary) + Spacer() + Button(action: { tabManager.addTab() }) { + Image(systemName: "plus") + .font(.system(size: 12, weight: .medium)) + } + .buttonStyle(.plain) + .foregroundColor(.secondary) + } + .padding(.horizontal, 12) + .padding(.vertical, 10) + + Divider() + + // Tab List + ScrollView { + LazyVStack(spacing: 2) { + ForEach(tabManager.tabs) { tab in + TabItemView(tab: tab) + } + } + .padding(.vertical, 4) + } + + Spacer() + } + .background(Color(nsColor: .controlBackgroundColor)) + } +} + +struct TabItemView: View { + @EnvironmentObject var tabManager: TabManager + @ObservedObject var tab: Tab + @State private var isHovering = false + + var isSelected: Bool { + tabManager.selectedTabId == tab.id + } + + var body: some View { + HStack(spacing: 8) { + Image(systemName: "terminal") + .font(.system(size: 12)) + .foregroundColor(isSelected ? .white : .secondary) + + Text(tab.title) + .font(.system(size: 12)) + .foregroundColor(isSelected ? .white : .primary) + .lineLimit(1) + .truncationMode(.tail) + + Spacer() + + if isHovering || isSelected { + Button(action: { tabManager.closeTab(tab) }) { + Image(systemName: "xmark") + .font(.system(size: 9, weight: .medium)) + .foregroundColor(isSelected ? .white.opacity(0.7) : .secondary) + } + .buttonStyle(.plain) + .opacity(tabManager.tabs.count > 1 ? 1 : 0) + } + } + .padding(.horizontal, 10) + .padding(.vertical, 8) + .background( + RoundedRectangle(cornerRadius: 6) + .fill(isSelected ? Color.accentColor : (isHovering ? Color(nsColor: .controlBackgroundColor).opacity(0.5) : Color.clear)) + ) + .padding(.horizontal, 6) + .contentShape(Rectangle()) + .onTapGesture { + tabManager.selectTab(tab) + } + .onHover { hovering in + isHovering = hovering + } + } +} diff --git a/Sources/GhosttyConfig.swift b/Sources/GhosttyConfig.swift new file mode 100644 index 00000000..b4dc27ef --- /dev/null +++ b/Sources/GhosttyConfig.swift @@ -0,0 +1,143 @@ +import Foundation +import AppKit + +struct GhosttyConfig { + var fontFamily: String = "Menlo" + var fontSize: CGFloat = 12 + var theme: String? + var workingDirectory: String? + var scrollbackLimit: Int = 10000 + + // Colors (from theme or config) + var backgroundColor: NSColor = NSColor(hex: "#272822")! + var foregroundColor: NSColor = NSColor(hex: "#fdfff1")! + var cursorColor: NSColor = NSColor(hex: "#c0c1b5")! + var cursorTextColor: NSColor = NSColor(hex: "#8d8e82")! + var selectionBackground: NSColor = NSColor(hex: "#57584f")! + var selectionForeground: NSColor = NSColor(hex: "#fdfff1")! + + // Palette colors (0-15) + var palette: [Int: NSColor] = [:] + + static func load() -> GhosttyConfig { + var config = GhosttyConfig() + + // Load user config + let configPath = NSString(string: "~/Library/Application Support/com.mitchellh.ghostty/config").expandingTildeInPath + if let contents = try? String(contentsOfFile: configPath, encoding: .utf8) { + config.parse(contents) + } + + // Load theme if specified + if let themeName = config.theme { + config.loadTheme(themeName) + } + + return config + } + + mutating func parse(_ contents: String) { + let lines = contents.components(separatedBy: .newlines) + for line in lines { + let trimmed = line.trimmingCharacters(in: .whitespaces) + if trimmed.isEmpty || trimmed.hasPrefix("#") { + continue + } + + let parts = trimmed.split(separator: "=", maxSplits: 1) + if parts.count == 2 { + let key = parts[0].trimmingCharacters(in: .whitespaces) + let value = parts[1].trimmingCharacters(in: .whitespaces).trimmingCharacters(in: CharacterSet(charactersIn: "\"")) + + switch key { + case "font-family": + fontFamily = value + case "font-size": + if let size = Double(value) { + fontSize = CGFloat(size) + } + case "theme": + theme = value + case "working-directory": + workingDirectory = value + case "scrollback-limit": + if let limit = Int(value) { + scrollbackLimit = limit + } + case "background": + if let color = NSColor(hex: value) { + backgroundColor = color + } + case "foreground": + if let color = NSColor(hex: value) { + foregroundColor = color + } + case "cursor-color": + if let color = NSColor(hex: value) { + cursorColor = color + } + case "cursor-text": + if let color = NSColor(hex: value) { + cursorTextColor = color + } + case "selection-background": + if let color = NSColor(hex: value) { + selectionBackground = color + } + case "selection-foreground": + if let color = NSColor(hex: value) { + selectionForeground = color + } + case "palette": + // Parse palette entries like "0=#272822" + let paletteParts = value.split(separator: "=", maxSplits: 1) + if paletteParts.count == 2, + let index = Int(paletteParts[0]), + let color = NSColor(hex: String(paletteParts[1])) { + palette[index] = color + } + default: + break + } + } + } + } + + mutating func loadTheme(_ name: String) { + // Try to load from Ghostty app resources + let themePaths = [ + "/Applications/Ghostty.app/Contents/Resources/ghostty/themes/\(name)", + NSString(string: "~/.config/ghostty/themes/\(name)").expandingTildeInPath + ] + + for path in themePaths { + if let contents = try? String(contentsOfFile: path, encoding: .utf8) { + parse(contents) + return + } + } + } +} + +extension NSColor { + convenience init?(hex: String) { + var hexSanitized = hex.trimmingCharacters(in: .whitespacesAndNewlines) + hexSanitized = hexSanitized.replacingOccurrences(of: "#", with: "") + + var rgb: UInt64 = 0 + guard Scanner(string: hexSanitized).scanHexInt64(&rgb) else { + return nil + } + + let r, g, b: CGFloat + if hexSanitized.count == 6 { + r = CGFloat((rgb & 0xFF0000) >> 16) / 255.0 + g = CGFloat((rgb & 0x00FF00) >> 8) / 255.0 + b = CGFloat(rgb & 0x0000FF) / 255.0 + } else { + return nil + } + + self.init(red: r, green: g, blue: b, alpha: 1.0) + } +} diff --git a/Sources/GhosttyTabsApp.swift b/Sources/GhosttyTabsApp.swift new file mode 100644 index 00000000..f8408c7c --- /dev/null +++ b/Sources/GhosttyTabsApp.swift @@ -0,0 +1,74 @@ +import SwiftUI + +@main +struct GhosttyTabsApp: App { + @StateObject private var tabManager = TabManager() + + var body: some Scene { + WindowGroup { + ContentView() + .environmentObject(tabManager) + } + .windowStyle(.hiddenTitleBar) + .commands { + // New tab commands + CommandGroup(replacing: .newItem) { + Button("New Tab") { + tabManager.addTab() + } + .keyboardShortcut("t", modifiers: .command) + + Button("New Tab") { + tabManager.addTab() + } + .keyboardShortcut("n", modifiers: .command) + + Button("New Tab") { + tabManager.addTab() + } + .keyboardShortcut("`", modifiers: [.control, .shift]) + } + + // Close tab + CommandGroup(after: .newItem) { + Button("Close Tab") { + tabManager.closeCurrentTab() + } + .keyboardShortcut("w", modifiers: .command) + } + + // Tab navigation + CommandGroup(after: .toolbar) { + Button("Next Tab") { + tabManager.selectNextTab() + } + .keyboardShortcut("]", modifiers: [.command, .shift]) + + Button("Previous Tab") { + tabManager.selectPreviousTab() + } + .keyboardShortcut("[", modifiers: [.command, .shift]) + + Button("Next Tab") { + tabManager.selectNextTab() + } + .keyboardShortcut(.tab, modifiers: .control) + + Button("Previous Tab") { + tabManager.selectPreviousTab() + } + .keyboardShortcut(.tab, modifiers: [.control, .shift]) + + Divider() + + // Cmd+1 through Cmd+9 for tab selection + ForEach(1...9, id: \.self) { number in + Button("Tab \(number)") { + tabManager.selectTab(at: number - 1) + } + .keyboardShortcut(KeyEquivalent(Character("\(number)")), modifiers: .command) + } + } + } + } +} diff --git a/Sources/GhosttyTerminalView.swift b/Sources/GhosttyTerminalView.swift new file mode 100644 index 00000000..60408494 --- /dev/null +++ b/Sources/GhosttyTerminalView.swift @@ -0,0 +1,383 @@ +import SwiftUI +import AppKit +import Metal +import QuartzCore + +// Minimal Ghostty wrapper for terminal rendering +// This uses libghostty (GhosttyKit.xcframework) for actual terminal emulation + +// MARK: - Ghostty App Singleton + +class GhosttyApp { + static let shared = GhosttyApp() + + private(set) var app: ghostty_app_t? + private(set) var config: ghostty_config_t? + + private init() { + initializeGhostty() + } + + private func initializeGhostty() { + // Initialize Ghostty library first + let result = ghostty_init(0, nil) + if result != GHOSTTY_SUCCESS { + print("Failed to initialize ghostty: \(result)") + return + } + + // Load config + config = ghostty_config_new() + guard let config = config else { + print("Failed to create ghostty config") + return + } + + // Load default config + ghostty_config_load_default_files(config) + ghostty_config_finalize(config) + + // Create runtime config with callbacks + var runtimeConfig = ghostty_runtime_config_s() + runtimeConfig.userdata = Unmanaged.passUnretained(self).toOpaque() + runtimeConfig.supports_selection_clipboard = true + runtimeConfig.wakeup_cb = { userdata in + DispatchQueue.main.async { + // Wakeup - trigger redraw if needed + } + } + runtimeConfig.action_cb = { app, target, action in + // Handle actions + return false + } + runtimeConfig.read_clipboard_cb = { userdata, location, state in + // Read clipboard + } + runtimeConfig.write_clipboard_cb = { userdata, location, content, len, confirm in + // Write clipboard + if let content = content { + let data = Data(bytes: content, count: Int(len)) + if let string = String(data: data, encoding: .utf8) { + DispatchQueue.main.async { + NSPasteboard.general.clearContents() + NSPasteboard.general.setString(string, forType: .string) + } + } + } + } + runtimeConfig.close_surface_cb = { userdata, processAlive in + // Surface closed + } + + // Create app + app = ghostty_app_new(&runtimeConfig, config) + if app == nil { + print("Failed to create ghostty app") + } + } + + func tick() { + guard let app = app else { return } + ghostty_app_tick(app) + } +} + +// MARK: - Ghostty Surface View + +class GhosttyNSView: NSView { + private var surface: ghostty_surface_t? + private var displayLink: CVDisplayLink? + private var metalDevice: MTLDevice? + private var surfaceCreated = false + + override func makeBackingLayer() -> CALayer { + let metalLayer = CAMetalLayer() + metalLayer.device = MTLCreateSystemDefaultDevice() + metalLayer.pixelFormat = .bgra8Unorm + metalLayer.framebufferOnly = true + metalLayer.isOpaque = true + metalLayer.contentsScale = NSScreen.main?.backingScaleFactor ?? 2.0 + self.metalDevice = metalLayer.device + return metalLayer + } + + override init(frame frameRect: NSRect) { + super.init(frame: frameRect) + setup() + } + + required init?(coder: NSCoder) { + super.init(coder: coder) + setup() + } + + private func setup() { + wantsLayer = true + layerContentsRedrawPolicy = .duringViewResize + } + + private func createSurfaceIfNeeded() { + // Only create once and when we have a valid size + guard !surfaceCreated else { return } + guard bounds.width > 0 && bounds.height > 0 else { return } + guard window != nil else { return } + + surfaceCreated = true + createSurface() + } + + private func createSurface() { + guard let app = GhosttyApp.shared.app else { + print("Ghostty app not initialized") + return + } + + let scale = window?.screen?.backingScaleFactor ?? NSScreen.main?.backingScaleFactor ?? 2.0 + + // Update Metal layer with initial size + if let metalLayer = layer as? CAMetalLayer { + metalLayer.contentsScale = scale + metalLayer.drawableSize = CGSize( + width: bounds.width * scale, + height: bounds.height * scale + ) + } + + var surfaceConfig = ghostty_surface_config_new() + surfaceConfig.platform_tag = GHOSTTY_PLATFORM_MACOS + + // Pass this view to ghostty + surfaceConfig.platform.macos.nsview = Unmanaged.passUnretained(self).toOpaque() + + // Set scale factor + surfaceConfig.scale_factor = scale + + surfaceConfig.context = GHOSTTY_SURFACE_CONTEXT_WINDOW + + // Create the surface + surface = ghostty_surface_new(app, &surfaceConfig) + + if surface == nil { + print("Failed to create ghostty surface") + return + } + + // Set initial size immediately after creation + ghostty_surface_set_size( + surface, + UInt32(bounds.width * scale), + UInt32(bounds.height * scale) + ) + + // Setup display link for rendering + setupDisplayLink() + } + + private func setupDisplayLink() { + var link: CVDisplayLink? + CVDisplayLinkCreateWithActiveCGDisplays(&link) + guard let displayLink = link else { return } + + self.displayLink = displayLink + + let callback: CVDisplayLinkOutputCallback = { displayLink, inNow, inOutputTime, flagsIn, flagsOut, displayLinkContext -> CVReturn in + DispatchQueue.main.async { + GhosttyApp.shared.tick() + } + return kCVReturnSuccess + } + + CVDisplayLinkSetOutputCallback(displayLink, callback, nil) + CVDisplayLinkStart(displayLink) + } + + override func viewDidMoveToWindow() { + super.viewDidMoveToWindow() + if window != nil { + createSurfaceIfNeeded() + updateSurfaceSize() + } + } + + override func setFrameSize(_ newSize: NSSize) { + super.setFrameSize(newSize) + createSurfaceIfNeeded() + updateSurfaceSize() + } + + override func layout() { + super.layout() + createSurfaceIfNeeded() + } + + private func updateSurfaceSize() { + guard let surface = surface else { return } + let scale = window?.screen?.backingScaleFactor ?? 2.0 + + // Update Metal layer + if let metalLayer = layer as? CAMetalLayer { + metalLayer.contentsScale = scale + metalLayer.drawableSize = CGSize( + width: bounds.width * scale, + height: bounds.height * scale + ) + } + + ghostty_surface_set_size( + surface, + UInt32(bounds.width * scale), + UInt32(bounds.height * scale) + ) + } + + // MARK: - Input Handling + + override var acceptsFirstResponder: Bool { true } + + private func ghosttyCharacters(from event: NSEvent) -> String? { + guard let chars = event.characters, !chars.isEmpty else { return nil } + for scalar in chars.unicodeScalars where scalar.value < 0x20 { + return nil + } + return chars + } + + override func keyDown(with event: NSEvent) { + guard let surface = surface else { + super.keyDown(with: event) + return + } + + interpretKeyEvents([event]) + + var keyEvent = ghostty_input_key_s() + keyEvent.action = GHOSTTY_ACTION_PRESS + keyEvent.keycode = UInt32(event.keyCode) + keyEvent.mods = modsFromEvent(event) + keyEvent.consumed_mods = GHOSTTY_MODS_NONE + keyEvent.composing = false + + if let text = ghosttyCharacters(from: event) { + text.withCString { ptr in + keyEvent.text = ptr + _ = ghostty_surface_key(surface, keyEvent) + } + } else { + keyEvent.text = nil + _ = ghostty_surface_key(surface, keyEvent) + } + } + + override func keyUp(with event: NSEvent) { + guard let surface = surface else { + super.keyUp(with: event) + return + } + + var keyEvent = ghostty_input_key_s() + keyEvent.action = GHOSTTY_ACTION_RELEASE + keyEvent.keycode = UInt32(event.keyCode) + keyEvent.mods = modsFromEvent(event) + keyEvent.consumed_mods = GHOSTTY_MODS_NONE + keyEvent.text = nil + keyEvent.composing = false + _ = ghostty_surface_key(surface, keyEvent) + } + + override func flagsChanged(with event: NSEvent) { + guard let surface = surface else { + super.flagsChanged(with: event) + return + } + + var keyEvent = ghostty_input_key_s() + keyEvent.action = GHOSTTY_ACTION_PRESS + keyEvent.keycode = UInt32(event.keyCode) + keyEvent.mods = modsFromEvent(event) + keyEvent.consumed_mods = GHOSTTY_MODS_NONE + keyEvent.text = nil + keyEvent.composing = false + _ = ghostty_surface_key(surface, keyEvent) + } + + private func modsFromEvent(_ event: NSEvent) -> ghostty_input_mods_e { + var mods = GHOSTTY_MODS_NONE.rawValue + if event.modifierFlags.contains(.shift) { mods |= GHOSTTY_MODS_SHIFT.rawValue } + if event.modifierFlags.contains(.control) { mods |= GHOSTTY_MODS_CTRL.rawValue } + if event.modifierFlags.contains(.option) { mods |= GHOSTTY_MODS_ALT.rawValue } + if event.modifierFlags.contains(.command) { mods |= GHOSTTY_MODS_SUPER.rawValue } + return ghostty_input_mods_e(rawValue: mods) + } + + // MARK: - Mouse Handling + + override func mouseDown(with event: NSEvent) { + window?.makeFirstResponder(self) + guard let surface = surface else { return } + let point = convert(event.locationInWindow, from: nil) + ghostty_surface_mouse_pos(surface, point.x, bounds.height - point.y, modsFromEvent(event)) + _ = ghostty_surface_mouse_button(surface, GHOSTTY_MOUSE_PRESS, GHOSTTY_MOUSE_LEFT, modsFromEvent(event)) + } + + override func mouseUp(with event: NSEvent) { + guard let surface = surface else { return } + _ = ghostty_surface_mouse_button(surface, GHOSTTY_MOUSE_RELEASE, GHOSTTY_MOUSE_LEFT, modsFromEvent(event)) + } + + override func mouseMoved(with event: NSEvent) { + guard let surface = surface else { return } + let point = convert(event.locationInWindow, from: nil) + ghostty_surface_mouse_pos(surface, point.x, bounds.height - point.y, modsFromEvent(event)) + } + + override func mouseDragged(with event: NSEvent) { + guard let surface = surface else { return } + let point = convert(event.locationInWindow, from: nil) + ghostty_surface_mouse_pos(surface, point.x, bounds.height - point.y, modsFromEvent(event)) + } + + override func scrollWheel(with event: NSEvent) { + guard let surface = surface else { return } + var mods: Int32 = 0 + if event.modifierFlags.contains(.shift) { mods |= Int32(GHOSTTY_MODS_SHIFT.rawValue) } + if event.modifierFlags.contains(.control) { mods |= Int32(GHOSTTY_MODS_CTRL.rawValue) } + if event.modifierFlags.contains(.option) { mods |= Int32(GHOSTTY_MODS_ALT.rawValue) } + if event.modifierFlags.contains(.command) { mods |= Int32(GHOSTTY_MODS_SUPER.rawValue) } + + ghostty_surface_mouse_scroll( + surface, + event.scrollingDeltaX, + event.scrollingDeltaY, + ghostty_input_scroll_mods_t(mods) + ) + } + + deinit { + if let displayLink = displayLink { + CVDisplayLinkStop(displayLink) + } + if let surface = surface { + ghostty_surface_free(surface) + } + } +} + +// MARK: - SwiftUI Wrapper + +struct GhosttyTerminalView: NSViewRepresentable { + func makeNSView(context: Context) -> GhosttyNSView { + let view = GhosttyNSView(frame: .zero) + // Focus after view is in window + DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { + view.window?.makeFirstResponder(view) + } + return view + } + + func updateNSView(_ nsView: GhosttyNSView, context: Context) { + // Focus on tab switch + DispatchQueue.main.async { + nsView.window?.makeFirstResponder(nsView) + } + } +} diff --git a/Sources/TabManager.swift b/Sources/TabManager.swift new file mode 100644 index 00000000..ac0e431f --- /dev/null +++ b/Sources/TabManager.swift @@ -0,0 +1,73 @@ +import SwiftUI +import Foundation + +class Tab: Identifiable, ObservableObject { + let id = UUID() + @Published var title: String + @Published var currentDirectory: String + + init(title: String = "Terminal") { + self.title = title + self.currentDirectory = FileManager.default.homeDirectoryForCurrentUser.path + } +} + +class TabManager: ObservableObject { + @Published var tabs: [Tab] = [] + @Published var selectedTabId: UUID? + + init() { + addTab() + } + + func addTab() { + let newTab = Tab(title: "Terminal \(tabs.count + 1)") + tabs.append(newTab) + selectedTabId = newTab.id + } + + func closeTab(_ tab: Tab) { + guard tabs.count > 1 else { return } + + if let index = tabs.firstIndex(where: { $0.id == tab.id }) { + tabs.remove(at: index) + + if selectedTabId == tab.id { + if index > 0 { + selectedTabId = tabs[index - 1].id + } else { + selectedTabId = tabs.first?.id + } + } + } + } + + func closeCurrentTab() { + guard let selectedId = selectedTabId, + let tab = tabs.first(where: { $0.id == selectedId }) else { return } + closeTab(tab) + } + + func selectTab(_ tab: Tab) { + selectedTabId = tab.id + } + + func selectNextTab() { + guard let currentId = selectedTabId, + let currentIndex = tabs.firstIndex(where: { $0.id == currentId }) else { return } + let nextIndex = (currentIndex + 1) % tabs.count + selectedTabId = tabs[nextIndex].id + } + + func selectPreviousTab() { + guard let currentId = selectedTabId, + let currentIndex = tabs.firstIndex(where: { $0.id == currentId }) else { return } + let prevIndex = (currentIndex - 1 + tabs.count) % tabs.count + selectedTabId = tabs[prevIndex].id + } + + func selectTab(at index: Int) { + guard index >= 0 && index < tabs.count else { return } + selectedTabId = tabs[index].id + } +} diff --git a/Sources/TerminalView.swift b/Sources/TerminalView.swift new file mode 100644 index 00000000..58d896a3 --- /dev/null +++ b/Sources/TerminalView.swift @@ -0,0 +1,287 @@ +import SwiftUI +import SwiftTerm +import AppKit + +// Helper to create SwiftTerm Color from hex +extension SwiftTerm.Color { + convenience init(hex: String) { + var hexSanitized = hex.trimmingCharacters(in: .whitespacesAndNewlines) + hexSanitized = hexSanitized.replacingOccurrences(of: "#", with: "") + + var rgb: UInt64 = 0 + Scanner(string: hexSanitized).scanHexInt64(&rgb) + + let r = UInt16((rgb & 0xFF0000) >> 16) + let g = UInt16((rgb & 0x00FF00) >> 8) + let b = UInt16(rgb & 0x0000FF) + + // Convert 8-bit to 16-bit + self.init(red: r * 257, green: g * 257, blue: b * 257) + } +} + +struct TerminalContainerView: View { + @ObservedObject var tab: Tab + let config: GhosttyConfig + + init(tab: Tab, config: GhosttyConfig = GhosttyConfig.load()) { + self.tab = tab + self.config = config + } + + var body: some View { + SwiftTermView(tab: tab, config: config) + .background(Color(config.backgroundColor)) + } +} + +// Custom wrapper to handle first responder and layout +class FocusableTerminalView: NSView { + var terminalView: LocalProcessTerminalView? + private var scroller: NSScroller? + private var fadeTimer: Timer? + private var scrollMonitor: Any? + private var lastScrollerValue: Double = 0 + + override var acceptsFirstResponder: Bool { true } + + override func becomeFirstResponder() -> Bool { + if let tv = terminalView { + DispatchQueue.main.async { + self.window?.makeFirstResponder(tv) + } + } + return true + } + + override func mouseDown(with event: NSEvent) { + window?.makeFirstResponder(terminalView) + super.mouseDown(with: event) + } + + override func layout() { + super.layout() + if let tv = terminalView, bounds.size.width > 0, bounds.size.height > 0 { + tv.setFrameSize(bounds.size) + setupScrollerTracking(in: tv) + } + } + + override func viewDidMoveToWindow() { + super.viewDidMoveToWindow() + if window != nil, let tv = terminalView, bounds.size.width > 0 { + tv.setFrameSize(bounds.size) + setupScrollerTracking(in: tv) + setupScrollMonitor() + } + } + + override func viewWillMove(toWindow newWindow: NSWindow?) { + super.viewWillMove(toWindow: newWindow) + if newWindow == nil, let monitor = scrollMonitor { + NSEvent.removeMonitor(monitor) + scrollMonitor = nil + } + } + + private func setupScrollerTracking(in view: NSView) { + if scroller == nil { + for subview in view.subviews { + if let s = subview as? NSScroller { + scroller = s + s.alphaValue = 0 // Start hidden + lastScrollerValue = s.doubleValue + break + } + } + } + } + + private func setupScrollMonitor() { + guard scrollMonitor == nil else { return } + + // Monitor scroll wheel events + scrollMonitor = NSEvent.addLocalMonitorForEvents(matching: .scrollWheel) { [weak self] event in + if let self = self, + let window = self.window, + event.window == window { + self.showScrollerTemporarily() + } + return event + } + } + + func showScrollerTemporarily() { + guard let scroller = scroller else { return } + + // Show scroller + NSAnimationContext.runAnimationGroup { context in + context.duration = 0.15 + scroller.animator().alphaValue = 1 + } + + // Cancel existing timer + fadeTimer?.invalidate() + + // Fade out after 1.5 seconds of no scrolling + fadeTimer = Timer.scheduledTimer(withTimeInterval: 1.5, repeats: false) { [weak self] _ in + NSAnimationContext.runAnimationGroup { context in + context.duration = 0.3 + self?.scroller?.animator().alphaValue = 0 + } + } + } +} + +struct SwiftTermView: NSViewRepresentable { + @ObservedObject var tab: Tab + let config: GhosttyConfig + + func makeNSView(context: Context) -> FocusableTerminalView { + let containerView = FocusableTerminalView() + containerView.wantsLayer = true + + let terminalView = LocalProcessTerminalView(frame: CGRect(x: 0, y: 0, width: 800, height: 600)) + + // Use autoresizingMask instead of Auto Layout for SwiftTerm compatibility + terminalView.autoresizingMask = [.width, .height] + + // Apply Ghostty config colors + terminalView.nativeForegroundColor = config.foregroundColor + terminalView.nativeBackgroundColor = config.backgroundColor + + // Set cursor color to match Ghostty + terminalView.caretColor = config.cursorColor + terminalView.caretTextColor = config.cursorTextColor + + // Set selection colors + terminalView.selectedTextBackgroundColor = config.selectionBackground + + // Apply ANSI palette colors + applyPalette(to: terminalView, config: config) + + // Configure font from config + if let font = NSFont(name: config.fontFamily, size: config.fontSize) { + terminalView.font = font + } else { + terminalView.font = NSFont.monospacedSystemFont(ofSize: config.fontSize, weight: .regular) + } + + // Set terminal delegate (only processDelegate, not terminalDelegate which breaks input) + terminalView.processDelegate = context.coordinator + context.coordinator.terminalView = terminalView + context.coordinator.containerView = containerView + + containerView.addSubview(terminalView) + containerView.terminalView = terminalView + + // Get shell path + let shell = ProcessInfo.processInfo.environment["SHELL"] ?? "/bin/zsh" + + // Determine working directory + let workingDir = config.workingDirectory ?? FileManager.default.homeDirectoryForCurrentUser.path + + // Build environment with working directory + var env = ProcessInfo.processInfo.environment + env["PWD"] = workingDir + + // Start the shell process + terminalView.startProcess( + executable: shell, + args: [], + environment: env.map { "\($0.key)=\($0.value)" }, + execName: "-" + (shell as NSString).lastPathComponent + ) + + // Change to working directory + terminalView.feed(text: "cd \"\(workingDir)\" && clear\n") + + + // Make first responder after a delay to ensure window is ready + DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { + containerView.window?.makeFirstResponder(terminalView) + } + + return containerView + } + + func updateNSView(_ nsView: FocusableTerminalView, context: Context) { + // When this view becomes visible (tab switch), make it first responder + DispatchQueue.main.async { + if let terminalView = nsView.terminalView { + nsView.window?.makeFirstResponder(terminalView) + } + } + } + + func makeCoordinator() -> Coordinator { + Coordinator(tab: tab) + } + + private func applyPalette(to terminalView: LocalProcessTerminalView, config: GhosttyConfig) { + // SwiftTerm uses installColors to set the ANSI color palette + // Build the color array (16 ANSI colors) + + // Default Monokai Classic palette hex values + let defaultPaletteHex: [String] = [ + "#272822", // 0 - black + "#f92672", // 1 - red + "#a6e22e", // 2 - green + "#e6db74", // 3 - yellow + "#fd971f", // 4 - blue (orange in Monokai) + "#ae81ff", // 5 - magenta + "#66d9ef", // 6 - cyan + "#fdfff1", // 7 - white + "#6e7066", // 8 - bright black + "#f92672", // 9 - bright red + "#a6e22e", // 10 - bright green + "#e6db74", // 11 - bright yellow + "#fd971f", // 12 - bright blue + "#ae81ff", // 13 - bright magenta + "#66d9ef", // 14 - bright cyan + "#fdfff1", // 15 - bright white + ] + + var colors: [SwiftTerm.Color] = [] + for i in 0..<16 { + colors.append(SwiftTerm.Color(hex: defaultPaletteHex[i])) + } + + // Install the ANSI colors + terminalView.installColors(colors) + } + + class Coordinator: NSObject, LocalProcessTerminalViewDelegate { + var tab: Tab + weak var terminalView: LocalProcessTerminalView? + weak var containerView: FocusableTerminalView? + + init(tab: Tab) { + self.tab = tab + } + + func sizeChanged(source: LocalProcessTerminalView, newCols: Int, newRows: Int) { + // Handle size change + } + + func setTerminalTitle(source: LocalProcessTerminalView, title: String) { + DispatchQueue.main.async { + if !title.isEmpty { + self.tab.title = title + } + } + } + + func hostCurrentDirectoryUpdate(source: TerminalView, directory: String?) { + if let dir = directory { + DispatchQueue.main.async { + self.tab.currentDirectory = dir + } + } + } + + func processTerminated(source: TerminalView, exitCode: Int32?) { + // Could close tab or show message + } + } +} diff --git a/ghostty.h b/ghostty.h new file mode 100644 index 00000000..3d397308 --- /dev/null +++ b/ghostty.h @@ -0,0 +1,1160 @@ +// Ghostty embedding API. The documentation for the embedding API is +// only within the Zig source files that define the implementations. This +// isn't meant to be a general purpose embedding API (yet) so there hasn't +// been documentation or example work beyond that. +// +// The only consumer of this API is the macOS app, but the API is built to +// be more general purpose. +#ifndef GHOSTTY_H +#define GHOSTTY_H + +#ifdef __cplusplus +extern "C" { +#endif + +#include +#include +#include +#include + +//------------------------------------------------------------------- +// Macros + +#define GHOSTTY_SUCCESS 0 + +//------------------------------------------------------------------- +// Types + +// Opaque types +typedef void* ghostty_app_t; +typedef void* ghostty_config_t; +typedef void* ghostty_surface_t; +typedef void* ghostty_inspector_t; + +// All the types below are fully defined and must be kept in sync with +// their Zig counterparts. Any changes to these types MUST have an associated +// Zig change. +typedef enum { + GHOSTTY_PLATFORM_INVALID, + GHOSTTY_PLATFORM_MACOS, + GHOSTTY_PLATFORM_IOS, +} ghostty_platform_e; + +typedef enum { + GHOSTTY_CLIPBOARD_STANDARD, + GHOSTTY_CLIPBOARD_SELECTION, +} ghostty_clipboard_e; + +typedef struct { + const char *mime; + const char *data; +} ghostty_clipboard_content_s; + +typedef enum { + GHOSTTY_CLIPBOARD_REQUEST_PASTE, + GHOSTTY_CLIPBOARD_REQUEST_OSC_52_READ, + GHOSTTY_CLIPBOARD_REQUEST_OSC_52_WRITE, +} ghostty_clipboard_request_e; + +typedef enum { + GHOSTTY_MOUSE_RELEASE, + GHOSTTY_MOUSE_PRESS, +} ghostty_input_mouse_state_e; + +typedef enum { + GHOSTTY_MOUSE_UNKNOWN, + GHOSTTY_MOUSE_LEFT, + GHOSTTY_MOUSE_RIGHT, + GHOSTTY_MOUSE_MIDDLE, + GHOSTTY_MOUSE_FOUR, + GHOSTTY_MOUSE_FIVE, + GHOSTTY_MOUSE_SIX, + GHOSTTY_MOUSE_SEVEN, + GHOSTTY_MOUSE_EIGHT, + GHOSTTY_MOUSE_NINE, + GHOSTTY_MOUSE_TEN, + GHOSTTY_MOUSE_ELEVEN, +} ghostty_input_mouse_button_e; + +typedef enum { + GHOSTTY_MOUSE_MOMENTUM_NONE, + GHOSTTY_MOUSE_MOMENTUM_BEGAN, + GHOSTTY_MOUSE_MOMENTUM_STATIONARY, + GHOSTTY_MOUSE_MOMENTUM_CHANGED, + GHOSTTY_MOUSE_MOMENTUM_ENDED, + GHOSTTY_MOUSE_MOMENTUM_CANCELLED, + GHOSTTY_MOUSE_MOMENTUM_MAY_BEGIN, +} ghostty_input_mouse_momentum_e; + +typedef enum { + GHOSTTY_COLOR_SCHEME_LIGHT = 0, + GHOSTTY_COLOR_SCHEME_DARK = 1, +} ghostty_color_scheme_e; + +// This is a packed struct (see src/input/mouse.zig) but the C standard +// afaik doesn't let us reliably define packed structs so we build it up +// from scratch. +typedef int ghostty_input_scroll_mods_t; + +typedef enum { + GHOSTTY_MODS_NONE = 0, + GHOSTTY_MODS_SHIFT = 1 << 0, + GHOSTTY_MODS_CTRL = 1 << 1, + GHOSTTY_MODS_ALT = 1 << 2, + GHOSTTY_MODS_SUPER = 1 << 3, + GHOSTTY_MODS_CAPS = 1 << 4, + GHOSTTY_MODS_NUM = 1 << 5, + GHOSTTY_MODS_SHIFT_RIGHT = 1 << 6, + GHOSTTY_MODS_CTRL_RIGHT = 1 << 7, + GHOSTTY_MODS_ALT_RIGHT = 1 << 8, + GHOSTTY_MODS_SUPER_RIGHT = 1 << 9, +} ghostty_input_mods_e; + +typedef enum { + GHOSTTY_BINDING_FLAGS_CONSUMED = 1 << 0, + GHOSTTY_BINDING_FLAGS_ALL = 1 << 1, + GHOSTTY_BINDING_FLAGS_GLOBAL = 1 << 2, + GHOSTTY_BINDING_FLAGS_PERFORMABLE = 1 << 3, +} ghostty_binding_flags_e; + +typedef enum { + GHOSTTY_ACTION_RELEASE, + GHOSTTY_ACTION_PRESS, + GHOSTTY_ACTION_REPEAT, +} ghostty_input_action_e; + +// Based on: https://www.w3.org/TR/uievents-code/ +typedef enum { + GHOSTTY_KEY_UNIDENTIFIED, + + // "Writing System Keys" § 3.1.1 + GHOSTTY_KEY_BACKQUOTE, + GHOSTTY_KEY_BACKSLASH, + GHOSTTY_KEY_BRACKET_LEFT, + GHOSTTY_KEY_BRACKET_RIGHT, + GHOSTTY_KEY_COMMA, + GHOSTTY_KEY_DIGIT_0, + GHOSTTY_KEY_DIGIT_1, + GHOSTTY_KEY_DIGIT_2, + GHOSTTY_KEY_DIGIT_3, + GHOSTTY_KEY_DIGIT_4, + GHOSTTY_KEY_DIGIT_5, + GHOSTTY_KEY_DIGIT_6, + GHOSTTY_KEY_DIGIT_7, + GHOSTTY_KEY_DIGIT_8, + GHOSTTY_KEY_DIGIT_9, + GHOSTTY_KEY_EQUAL, + GHOSTTY_KEY_INTL_BACKSLASH, + GHOSTTY_KEY_INTL_RO, + GHOSTTY_KEY_INTL_YEN, + GHOSTTY_KEY_A, + GHOSTTY_KEY_B, + GHOSTTY_KEY_C, + GHOSTTY_KEY_D, + GHOSTTY_KEY_E, + GHOSTTY_KEY_F, + GHOSTTY_KEY_G, + GHOSTTY_KEY_H, + GHOSTTY_KEY_I, + GHOSTTY_KEY_J, + GHOSTTY_KEY_K, + GHOSTTY_KEY_L, + GHOSTTY_KEY_M, + GHOSTTY_KEY_N, + GHOSTTY_KEY_O, + GHOSTTY_KEY_P, + GHOSTTY_KEY_Q, + GHOSTTY_KEY_R, + GHOSTTY_KEY_S, + GHOSTTY_KEY_T, + GHOSTTY_KEY_U, + GHOSTTY_KEY_V, + GHOSTTY_KEY_W, + GHOSTTY_KEY_X, + GHOSTTY_KEY_Y, + GHOSTTY_KEY_Z, + GHOSTTY_KEY_MINUS, + GHOSTTY_KEY_PERIOD, + GHOSTTY_KEY_QUOTE, + GHOSTTY_KEY_SEMICOLON, + GHOSTTY_KEY_SLASH, + + // "Functional Keys" § 3.1.2 + GHOSTTY_KEY_ALT_LEFT, + GHOSTTY_KEY_ALT_RIGHT, + GHOSTTY_KEY_BACKSPACE, + GHOSTTY_KEY_CAPS_LOCK, + GHOSTTY_KEY_CONTEXT_MENU, + GHOSTTY_KEY_CONTROL_LEFT, + GHOSTTY_KEY_CONTROL_RIGHT, + GHOSTTY_KEY_ENTER, + GHOSTTY_KEY_META_LEFT, + GHOSTTY_KEY_META_RIGHT, + GHOSTTY_KEY_SHIFT_LEFT, + GHOSTTY_KEY_SHIFT_RIGHT, + GHOSTTY_KEY_SPACE, + GHOSTTY_KEY_TAB, + GHOSTTY_KEY_CONVERT, + GHOSTTY_KEY_KANA_MODE, + GHOSTTY_KEY_NON_CONVERT, + + // "Control Pad Section" § 3.2 + GHOSTTY_KEY_DELETE, + GHOSTTY_KEY_END, + GHOSTTY_KEY_HELP, + GHOSTTY_KEY_HOME, + GHOSTTY_KEY_INSERT, + GHOSTTY_KEY_PAGE_DOWN, + GHOSTTY_KEY_PAGE_UP, + + // "Arrow Pad Section" § 3.3 + GHOSTTY_KEY_ARROW_DOWN, + GHOSTTY_KEY_ARROW_LEFT, + GHOSTTY_KEY_ARROW_RIGHT, + GHOSTTY_KEY_ARROW_UP, + + // "Numpad Section" § 3.4 + GHOSTTY_KEY_NUM_LOCK, + GHOSTTY_KEY_NUMPAD_0, + GHOSTTY_KEY_NUMPAD_1, + GHOSTTY_KEY_NUMPAD_2, + GHOSTTY_KEY_NUMPAD_3, + GHOSTTY_KEY_NUMPAD_4, + GHOSTTY_KEY_NUMPAD_5, + GHOSTTY_KEY_NUMPAD_6, + GHOSTTY_KEY_NUMPAD_7, + GHOSTTY_KEY_NUMPAD_8, + GHOSTTY_KEY_NUMPAD_9, + GHOSTTY_KEY_NUMPAD_ADD, + GHOSTTY_KEY_NUMPAD_BACKSPACE, + GHOSTTY_KEY_NUMPAD_CLEAR, + GHOSTTY_KEY_NUMPAD_CLEAR_ENTRY, + GHOSTTY_KEY_NUMPAD_COMMA, + GHOSTTY_KEY_NUMPAD_DECIMAL, + GHOSTTY_KEY_NUMPAD_DIVIDE, + GHOSTTY_KEY_NUMPAD_ENTER, + GHOSTTY_KEY_NUMPAD_EQUAL, + GHOSTTY_KEY_NUMPAD_MEMORY_ADD, + GHOSTTY_KEY_NUMPAD_MEMORY_CLEAR, + GHOSTTY_KEY_NUMPAD_MEMORY_RECALL, + GHOSTTY_KEY_NUMPAD_MEMORY_STORE, + GHOSTTY_KEY_NUMPAD_MEMORY_SUBTRACT, + GHOSTTY_KEY_NUMPAD_MULTIPLY, + GHOSTTY_KEY_NUMPAD_PAREN_LEFT, + GHOSTTY_KEY_NUMPAD_PAREN_RIGHT, + GHOSTTY_KEY_NUMPAD_SUBTRACT, + GHOSTTY_KEY_NUMPAD_SEPARATOR, + GHOSTTY_KEY_NUMPAD_UP, + GHOSTTY_KEY_NUMPAD_DOWN, + GHOSTTY_KEY_NUMPAD_RIGHT, + GHOSTTY_KEY_NUMPAD_LEFT, + GHOSTTY_KEY_NUMPAD_BEGIN, + GHOSTTY_KEY_NUMPAD_HOME, + GHOSTTY_KEY_NUMPAD_END, + GHOSTTY_KEY_NUMPAD_INSERT, + GHOSTTY_KEY_NUMPAD_DELETE, + GHOSTTY_KEY_NUMPAD_PAGE_UP, + GHOSTTY_KEY_NUMPAD_PAGE_DOWN, + + // "Function Section" § 3.5 + GHOSTTY_KEY_ESCAPE, + GHOSTTY_KEY_F1, + GHOSTTY_KEY_F2, + GHOSTTY_KEY_F3, + GHOSTTY_KEY_F4, + GHOSTTY_KEY_F5, + GHOSTTY_KEY_F6, + GHOSTTY_KEY_F7, + GHOSTTY_KEY_F8, + GHOSTTY_KEY_F9, + GHOSTTY_KEY_F10, + GHOSTTY_KEY_F11, + GHOSTTY_KEY_F12, + GHOSTTY_KEY_F13, + GHOSTTY_KEY_F14, + GHOSTTY_KEY_F15, + GHOSTTY_KEY_F16, + GHOSTTY_KEY_F17, + GHOSTTY_KEY_F18, + GHOSTTY_KEY_F19, + GHOSTTY_KEY_F20, + GHOSTTY_KEY_F21, + GHOSTTY_KEY_F22, + GHOSTTY_KEY_F23, + GHOSTTY_KEY_F24, + GHOSTTY_KEY_F25, + GHOSTTY_KEY_FN, + GHOSTTY_KEY_FN_LOCK, + GHOSTTY_KEY_PRINT_SCREEN, + GHOSTTY_KEY_SCROLL_LOCK, + GHOSTTY_KEY_PAUSE, + + // "Media Keys" § 3.6 + GHOSTTY_KEY_BROWSER_BACK, + GHOSTTY_KEY_BROWSER_FAVORITES, + GHOSTTY_KEY_BROWSER_FORWARD, + GHOSTTY_KEY_BROWSER_HOME, + GHOSTTY_KEY_BROWSER_REFRESH, + GHOSTTY_KEY_BROWSER_SEARCH, + GHOSTTY_KEY_BROWSER_STOP, + GHOSTTY_KEY_EJECT, + GHOSTTY_KEY_LAUNCH_APP_1, + GHOSTTY_KEY_LAUNCH_APP_2, + GHOSTTY_KEY_LAUNCH_MAIL, + GHOSTTY_KEY_MEDIA_PLAY_PAUSE, + GHOSTTY_KEY_MEDIA_SELECT, + GHOSTTY_KEY_MEDIA_STOP, + GHOSTTY_KEY_MEDIA_TRACK_NEXT, + GHOSTTY_KEY_MEDIA_TRACK_PREVIOUS, + GHOSTTY_KEY_POWER, + GHOSTTY_KEY_SLEEP, + GHOSTTY_KEY_AUDIO_VOLUME_DOWN, + GHOSTTY_KEY_AUDIO_VOLUME_MUTE, + GHOSTTY_KEY_AUDIO_VOLUME_UP, + GHOSTTY_KEY_WAKE_UP, + + // "Legacy, Non-standard, and Special Keys" § 3.7 + GHOSTTY_KEY_COPY, + GHOSTTY_KEY_CUT, + GHOSTTY_KEY_PASTE, +} ghostty_input_key_e; + +typedef struct { + ghostty_input_action_e action; + ghostty_input_mods_e mods; + ghostty_input_mods_e consumed_mods; + uint32_t keycode; + const char* text; + uint32_t unshifted_codepoint; + bool composing; +} ghostty_input_key_s; + +typedef enum { + GHOSTTY_TRIGGER_PHYSICAL, + GHOSTTY_TRIGGER_UNICODE, + GHOSTTY_TRIGGER_CATCH_ALL, +} ghostty_input_trigger_tag_e; + +typedef union { + ghostty_input_key_e translated; + ghostty_input_key_e physical; + uint32_t unicode; + // catch_all has no payload +} ghostty_input_trigger_key_u; + +typedef struct { + ghostty_input_trigger_tag_e tag; + ghostty_input_trigger_key_u key; + ghostty_input_mods_e mods; +} ghostty_input_trigger_s; + +typedef struct { + const char* action_key; + const char* action; + const char* title; + const char* description; +} ghostty_command_s; + +typedef enum { + GHOSTTY_BUILD_MODE_DEBUG, + GHOSTTY_BUILD_MODE_RELEASE_SAFE, + GHOSTTY_BUILD_MODE_RELEASE_FAST, + GHOSTTY_BUILD_MODE_RELEASE_SMALL, +} ghostty_build_mode_e; + +typedef struct { + ghostty_build_mode_e build_mode; + const char* version; + uintptr_t version_len; +} ghostty_info_s; + +typedef struct { + const char* message; +} ghostty_diagnostic_s; + +typedef struct { + const char* ptr; + uintptr_t len; + bool sentinel; +} ghostty_string_s; + +typedef struct { + double tl_px_x; + double tl_px_y; + uint32_t offset_start; + uint32_t offset_len; + const char* text; + uintptr_t text_len; +} ghostty_text_s; + +typedef enum { + GHOSTTY_POINT_ACTIVE, + GHOSTTY_POINT_VIEWPORT, + GHOSTTY_POINT_SCREEN, + GHOSTTY_POINT_SURFACE, +} ghostty_point_tag_e; + +typedef enum { + GHOSTTY_POINT_COORD_EXACT, + GHOSTTY_POINT_COORD_TOP_LEFT, + GHOSTTY_POINT_COORD_BOTTOM_RIGHT, +} ghostty_point_coord_e; + +typedef struct { + ghostty_point_tag_e tag; + ghostty_point_coord_e coord; + uint32_t x; + uint32_t y; +} ghostty_point_s; + +typedef struct { + ghostty_point_s top_left; + ghostty_point_s bottom_right; + bool rectangle; +} ghostty_selection_s; + +typedef struct { + const char* key; + const char* value; +} ghostty_env_var_s; + +typedef struct { + void* nsview; +} ghostty_platform_macos_s; + +typedef struct { + void* uiview; +} ghostty_platform_ios_s; + +typedef union { + ghostty_platform_macos_s macos; + ghostty_platform_ios_s ios; +} ghostty_platform_u; + +typedef enum { + GHOSTTY_SURFACE_CONTEXT_WINDOW = 0, + GHOSTTY_SURFACE_CONTEXT_TAB = 1, + GHOSTTY_SURFACE_CONTEXT_SPLIT = 2, +} ghostty_surface_context_e; + +typedef struct { + ghostty_platform_e platform_tag; + ghostty_platform_u platform; + void* userdata; + double scale_factor; + float font_size; + const char* working_directory; + const char* command; + ghostty_env_var_s* env_vars; + size_t env_var_count; + const char* initial_input; + bool wait_after_command; + ghostty_surface_context_e context; +} ghostty_surface_config_s; + +typedef struct { + uint16_t columns; + uint16_t rows; + uint32_t width_px; + uint32_t height_px; + uint32_t cell_width_px; + uint32_t cell_height_px; +} ghostty_surface_size_s; + +// Config types + +// config.Color +typedef struct { + uint8_t r; + uint8_t g; + uint8_t b; +} ghostty_config_color_s; + +// config.ColorList +typedef struct { + const ghostty_config_color_s* colors; + size_t len; +} ghostty_config_color_list_s; + +// config.RepeatableCommand +typedef struct { + const ghostty_command_s* commands; + size_t len; +} ghostty_config_command_list_s; + +// config.Palette +typedef struct { + ghostty_config_color_s colors[256]; +} ghostty_config_palette_s; + +// config.QuickTerminalSize +typedef enum { + GHOSTTY_QUICK_TERMINAL_SIZE_NONE, + GHOSTTY_QUICK_TERMINAL_SIZE_PERCENTAGE, + GHOSTTY_QUICK_TERMINAL_SIZE_PIXELS, +} ghostty_quick_terminal_size_tag_e; + +typedef union { + float percentage; + uint32_t pixels; +} ghostty_quick_terminal_size_value_u; + +typedef struct { + ghostty_quick_terminal_size_tag_e tag; + ghostty_quick_terminal_size_value_u value; +} ghostty_quick_terminal_size_s; + +typedef struct { + ghostty_quick_terminal_size_s primary; + ghostty_quick_terminal_size_s secondary; +} ghostty_config_quick_terminal_size_s; + +// apprt.Target.Key +typedef enum { + GHOSTTY_TARGET_APP, + GHOSTTY_TARGET_SURFACE, +} ghostty_target_tag_e; + +typedef union { + ghostty_surface_t surface; +} ghostty_target_u; + +typedef struct { + ghostty_target_tag_e tag; + ghostty_target_u target; +} ghostty_target_s; + +// apprt.action.SplitDirection +typedef enum { + GHOSTTY_SPLIT_DIRECTION_RIGHT, + GHOSTTY_SPLIT_DIRECTION_DOWN, + GHOSTTY_SPLIT_DIRECTION_LEFT, + GHOSTTY_SPLIT_DIRECTION_UP, +} ghostty_action_split_direction_e; + +// apprt.action.GotoSplit +typedef enum { + GHOSTTY_GOTO_SPLIT_PREVIOUS, + GHOSTTY_GOTO_SPLIT_NEXT, + GHOSTTY_GOTO_SPLIT_UP, + GHOSTTY_GOTO_SPLIT_LEFT, + GHOSTTY_GOTO_SPLIT_DOWN, + GHOSTTY_GOTO_SPLIT_RIGHT, +} ghostty_action_goto_split_e; + +// apprt.action.GotoWindow +typedef enum { + GHOSTTY_GOTO_WINDOW_PREVIOUS, + GHOSTTY_GOTO_WINDOW_NEXT, +} ghostty_action_goto_window_e; + +// apprt.action.ResizeSplit.Direction +typedef enum { + GHOSTTY_RESIZE_SPLIT_UP, + GHOSTTY_RESIZE_SPLIT_DOWN, + GHOSTTY_RESIZE_SPLIT_LEFT, + GHOSTTY_RESIZE_SPLIT_RIGHT, +} ghostty_action_resize_split_direction_e; + +// apprt.action.ResizeSplit +typedef struct { + uint16_t amount; + ghostty_action_resize_split_direction_e direction; +} ghostty_action_resize_split_s; + +// apprt.action.MoveTab +typedef struct { + ssize_t amount; +} ghostty_action_move_tab_s; + +// apprt.action.GotoTab +typedef enum { + GHOSTTY_GOTO_TAB_PREVIOUS = -1, + GHOSTTY_GOTO_TAB_NEXT = -2, + GHOSTTY_GOTO_TAB_LAST = -3, +} ghostty_action_goto_tab_e; + +// apprt.action.Fullscreen +typedef enum { + GHOSTTY_FULLSCREEN_NATIVE, + GHOSTTY_FULLSCREEN_NON_NATIVE, + GHOSTTY_FULLSCREEN_NON_NATIVE_VISIBLE_MENU, + GHOSTTY_FULLSCREEN_NON_NATIVE_PADDED_NOTCH, +} ghostty_action_fullscreen_e; + +// apprt.action.FloatWindow +typedef enum { + GHOSTTY_FLOAT_WINDOW_ON, + GHOSTTY_FLOAT_WINDOW_OFF, + GHOSTTY_FLOAT_WINDOW_TOGGLE, +} ghostty_action_float_window_e; + +// apprt.action.SecureInput +typedef enum { + GHOSTTY_SECURE_INPUT_ON, + GHOSTTY_SECURE_INPUT_OFF, + GHOSTTY_SECURE_INPUT_TOGGLE, +} ghostty_action_secure_input_e; + +// apprt.action.Inspector +typedef enum { + GHOSTTY_INSPECTOR_TOGGLE, + GHOSTTY_INSPECTOR_SHOW, + GHOSTTY_INSPECTOR_HIDE, +} ghostty_action_inspector_e; + +// apprt.action.QuitTimer +typedef enum { + GHOSTTY_QUIT_TIMER_START, + GHOSTTY_QUIT_TIMER_STOP, +} ghostty_action_quit_timer_e; + +// apprt.action.Readonly +typedef enum { + GHOSTTY_READONLY_OFF, + GHOSTTY_READONLY_ON, +} ghostty_action_readonly_e; + +// apprt.action.DesktopNotification.C +typedef struct { + const char* title; + const char* body; +} ghostty_action_desktop_notification_s; + +// apprt.action.SetTitle.C +typedef struct { + const char* title; +} ghostty_action_set_title_s; + +// apprt.action.PromptTitle +typedef enum { + GHOSTTY_PROMPT_TITLE_SURFACE, + GHOSTTY_PROMPT_TITLE_TAB, +} ghostty_action_prompt_title_e; + +// apprt.action.Pwd.C +typedef struct { + const char* pwd; +} ghostty_action_pwd_s; + +// terminal.MouseShape +typedef enum { + GHOSTTY_MOUSE_SHAPE_DEFAULT, + GHOSTTY_MOUSE_SHAPE_CONTEXT_MENU, + GHOSTTY_MOUSE_SHAPE_HELP, + GHOSTTY_MOUSE_SHAPE_POINTER, + GHOSTTY_MOUSE_SHAPE_PROGRESS, + GHOSTTY_MOUSE_SHAPE_WAIT, + GHOSTTY_MOUSE_SHAPE_CELL, + GHOSTTY_MOUSE_SHAPE_CROSSHAIR, + GHOSTTY_MOUSE_SHAPE_TEXT, + GHOSTTY_MOUSE_SHAPE_VERTICAL_TEXT, + GHOSTTY_MOUSE_SHAPE_ALIAS, + GHOSTTY_MOUSE_SHAPE_COPY, + GHOSTTY_MOUSE_SHAPE_MOVE, + GHOSTTY_MOUSE_SHAPE_NO_DROP, + GHOSTTY_MOUSE_SHAPE_NOT_ALLOWED, + GHOSTTY_MOUSE_SHAPE_GRAB, + GHOSTTY_MOUSE_SHAPE_GRABBING, + GHOSTTY_MOUSE_SHAPE_ALL_SCROLL, + GHOSTTY_MOUSE_SHAPE_COL_RESIZE, + GHOSTTY_MOUSE_SHAPE_ROW_RESIZE, + GHOSTTY_MOUSE_SHAPE_N_RESIZE, + GHOSTTY_MOUSE_SHAPE_E_RESIZE, + GHOSTTY_MOUSE_SHAPE_S_RESIZE, + GHOSTTY_MOUSE_SHAPE_W_RESIZE, + GHOSTTY_MOUSE_SHAPE_NE_RESIZE, + GHOSTTY_MOUSE_SHAPE_NW_RESIZE, + GHOSTTY_MOUSE_SHAPE_SE_RESIZE, + GHOSTTY_MOUSE_SHAPE_SW_RESIZE, + GHOSTTY_MOUSE_SHAPE_EW_RESIZE, + GHOSTTY_MOUSE_SHAPE_NS_RESIZE, + GHOSTTY_MOUSE_SHAPE_NESW_RESIZE, + GHOSTTY_MOUSE_SHAPE_NWSE_RESIZE, + GHOSTTY_MOUSE_SHAPE_ZOOM_IN, + GHOSTTY_MOUSE_SHAPE_ZOOM_OUT, +} ghostty_action_mouse_shape_e; + +// apprt.action.MouseVisibility +typedef enum { + GHOSTTY_MOUSE_VISIBLE, + GHOSTTY_MOUSE_HIDDEN, +} ghostty_action_mouse_visibility_e; + +// apprt.action.MouseOverLink +typedef struct { + const char* url; + size_t len; +} ghostty_action_mouse_over_link_s; + +// apprt.action.SizeLimit +typedef struct { + uint32_t min_width; + uint32_t min_height; + uint32_t max_width; + uint32_t max_height; +} ghostty_action_size_limit_s; + +// apprt.action.InitialSize +typedef struct { + uint32_t width; + uint32_t height; +} ghostty_action_initial_size_s; + +// apprt.action.CellSize +typedef struct { + uint32_t width; + uint32_t height; +} ghostty_action_cell_size_s; + +// renderer.Health +typedef enum { + GHOSTTY_RENDERER_HEALTH_OK, + GHOSTTY_RENDERER_HEALTH_UNHEALTHY, +} ghostty_action_renderer_health_e; + +// apprt.action.KeySequence +typedef struct { + bool active; + ghostty_input_trigger_s trigger; +} ghostty_action_key_sequence_s; + +// apprt.action.KeyTable.Tag +typedef enum { + GHOSTTY_KEY_TABLE_ACTIVATE, + GHOSTTY_KEY_TABLE_DEACTIVATE, + GHOSTTY_KEY_TABLE_DEACTIVATE_ALL, +} ghostty_action_key_table_tag_e; + +// apprt.action.KeyTable.CValue +typedef union { + struct { + const char *name; + size_t len; + } activate; +} ghostty_action_key_table_u; + +// apprt.action.KeyTable.C +typedef struct { + ghostty_action_key_table_tag_e tag; + ghostty_action_key_table_u value; +} ghostty_action_key_table_s; + +// apprt.action.ColorKind +typedef enum { + GHOSTTY_ACTION_COLOR_KIND_FOREGROUND = -1, + GHOSTTY_ACTION_COLOR_KIND_BACKGROUND = -2, + GHOSTTY_ACTION_COLOR_KIND_CURSOR = -3, +} ghostty_action_color_kind_e; + +// apprt.action.ColorChange +typedef struct { + ghostty_action_color_kind_e kind; + uint8_t r; + uint8_t g; + uint8_t b; +} ghostty_action_color_change_s; + +// apprt.action.ConfigChange +typedef struct { + ghostty_config_t config; +} ghostty_action_config_change_s; + +// apprt.action.ReloadConfig +typedef struct { + bool soft; +} ghostty_action_reload_config_s; + +// apprt.action.OpenUrlKind +typedef enum { + GHOSTTY_ACTION_OPEN_URL_KIND_UNKNOWN, + GHOSTTY_ACTION_OPEN_URL_KIND_TEXT, + GHOSTTY_ACTION_OPEN_URL_KIND_HTML, +} ghostty_action_open_url_kind_e; + +// apprt.action.OpenUrl.C +typedef struct { + ghostty_action_open_url_kind_e kind; + const char* url; + uintptr_t len; +} ghostty_action_open_url_s; + +// apprt.action.CloseTabMode +typedef enum { + GHOSTTY_ACTION_CLOSE_TAB_MODE_THIS, + GHOSTTY_ACTION_CLOSE_TAB_MODE_OTHER, + GHOSTTY_ACTION_CLOSE_TAB_MODE_RIGHT, +} ghostty_action_close_tab_mode_e; + +// apprt.surface.Message.ChildExited +typedef struct { + uint32_t exit_code; + uint64_t timetime_ms; +} ghostty_surface_message_childexited_s; + +// terminal.osc.Command.ProgressReport.State +typedef enum { + GHOSTTY_PROGRESS_STATE_REMOVE, + GHOSTTY_PROGRESS_STATE_SET, + GHOSTTY_PROGRESS_STATE_ERROR, + GHOSTTY_PROGRESS_STATE_INDETERMINATE, + GHOSTTY_PROGRESS_STATE_PAUSE, +} ghostty_action_progress_report_state_e; + +// terminal.osc.Command.ProgressReport.C +typedef struct { + ghostty_action_progress_report_state_e state; + // -1 if no progress was reported, otherwise 0-100 indicating percent + // completeness. + int8_t progress; +} ghostty_action_progress_report_s; + +// apprt.action.CommandFinished.C +typedef struct { + // -1 if no exit code was reported, otherwise 0-255 + int16_t exit_code; + // number of nanoseconds that command was running for + uint64_t duration; +} ghostty_action_command_finished_s; + +// apprt.action.StartSearch.C +typedef struct { + const char* needle; +} ghostty_action_start_search_s; + +// apprt.action.SearchTotal +typedef struct { + ssize_t total; +} ghostty_action_search_total_s; + +// apprt.action.SearchSelected +typedef struct { + ssize_t selected; +} ghostty_action_search_selected_s; + +// terminal.Scrollbar +typedef struct { + uint64_t total; + uint64_t offset; + uint64_t len; +} ghostty_action_scrollbar_s; + +// apprt.Action.Key +typedef enum { + GHOSTTY_ACTION_QUIT, + GHOSTTY_ACTION_NEW_WINDOW, + GHOSTTY_ACTION_NEW_TAB, + GHOSTTY_ACTION_CLOSE_TAB, + GHOSTTY_ACTION_NEW_SPLIT, + GHOSTTY_ACTION_CLOSE_ALL_WINDOWS, + GHOSTTY_ACTION_TOGGLE_MAXIMIZE, + GHOSTTY_ACTION_TOGGLE_FULLSCREEN, + GHOSTTY_ACTION_TOGGLE_TAB_OVERVIEW, + GHOSTTY_ACTION_TOGGLE_WINDOW_DECORATIONS, + GHOSTTY_ACTION_TOGGLE_QUICK_TERMINAL, + GHOSTTY_ACTION_TOGGLE_COMMAND_PALETTE, + GHOSTTY_ACTION_TOGGLE_VISIBILITY, + GHOSTTY_ACTION_TOGGLE_BACKGROUND_OPACITY, + GHOSTTY_ACTION_MOVE_TAB, + GHOSTTY_ACTION_GOTO_TAB, + GHOSTTY_ACTION_GOTO_SPLIT, + GHOSTTY_ACTION_GOTO_WINDOW, + GHOSTTY_ACTION_RESIZE_SPLIT, + GHOSTTY_ACTION_EQUALIZE_SPLITS, + GHOSTTY_ACTION_TOGGLE_SPLIT_ZOOM, + GHOSTTY_ACTION_PRESENT_TERMINAL, + GHOSTTY_ACTION_SIZE_LIMIT, + GHOSTTY_ACTION_RESET_WINDOW_SIZE, + GHOSTTY_ACTION_INITIAL_SIZE, + GHOSTTY_ACTION_CELL_SIZE, + GHOSTTY_ACTION_SCROLLBAR, + GHOSTTY_ACTION_RENDER, + GHOSTTY_ACTION_INSPECTOR, + GHOSTTY_ACTION_SHOW_GTK_INSPECTOR, + GHOSTTY_ACTION_RENDER_INSPECTOR, + GHOSTTY_ACTION_DESKTOP_NOTIFICATION, + GHOSTTY_ACTION_SET_TITLE, + GHOSTTY_ACTION_PROMPT_TITLE, + GHOSTTY_ACTION_PWD, + GHOSTTY_ACTION_MOUSE_SHAPE, + GHOSTTY_ACTION_MOUSE_VISIBILITY, + GHOSTTY_ACTION_MOUSE_OVER_LINK, + GHOSTTY_ACTION_RENDERER_HEALTH, + GHOSTTY_ACTION_OPEN_CONFIG, + GHOSTTY_ACTION_QUIT_TIMER, + GHOSTTY_ACTION_FLOAT_WINDOW, + GHOSTTY_ACTION_SECURE_INPUT, + GHOSTTY_ACTION_KEY_SEQUENCE, + GHOSTTY_ACTION_KEY_TABLE, + GHOSTTY_ACTION_COLOR_CHANGE, + GHOSTTY_ACTION_RELOAD_CONFIG, + GHOSTTY_ACTION_CONFIG_CHANGE, + GHOSTTY_ACTION_CLOSE_WINDOW, + GHOSTTY_ACTION_RING_BELL, + GHOSTTY_ACTION_UNDO, + GHOSTTY_ACTION_REDO, + GHOSTTY_ACTION_CHECK_FOR_UPDATES, + GHOSTTY_ACTION_OPEN_URL, + GHOSTTY_ACTION_SHOW_CHILD_EXITED, + GHOSTTY_ACTION_PROGRESS_REPORT, + GHOSTTY_ACTION_SHOW_ON_SCREEN_KEYBOARD, + GHOSTTY_ACTION_COMMAND_FINISHED, + GHOSTTY_ACTION_START_SEARCH, + GHOSTTY_ACTION_END_SEARCH, + GHOSTTY_ACTION_SEARCH_TOTAL, + GHOSTTY_ACTION_SEARCH_SELECTED, + GHOSTTY_ACTION_READONLY, +} ghostty_action_tag_e; + +typedef union { + ghostty_action_split_direction_e new_split; + ghostty_action_fullscreen_e toggle_fullscreen; + ghostty_action_move_tab_s move_tab; + ghostty_action_goto_tab_e goto_tab; + ghostty_action_goto_split_e goto_split; + ghostty_action_goto_window_e goto_window; + ghostty_action_resize_split_s resize_split; + ghostty_action_size_limit_s size_limit; + ghostty_action_initial_size_s initial_size; + ghostty_action_cell_size_s cell_size; + ghostty_action_scrollbar_s scrollbar; + ghostty_action_inspector_e inspector; + ghostty_action_desktop_notification_s desktop_notification; + ghostty_action_set_title_s set_title; + ghostty_action_prompt_title_e prompt_title; + ghostty_action_pwd_s pwd; + ghostty_action_mouse_shape_e mouse_shape; + ghostty_action_mouse_visibility_e mouse_visibility; + ghostty_action_mouse_over_link_s mouse_over_link; + ghostty_action_renderer_health_e renderer_health; + ghostty_action_quit_timer_e quit_timer; + ghostty_action_float_window_e float_window; + ghostty_action_secure_input_e secure_input; + ghostty_action_key_sequence_s key_sequence; + ghostty_action_key_table_s key_table; + ghostty_action_color_change_s color_change; + ghostty_action_reload_config_s reload_config; + ghostty_action_config_change_s config_change; + ghostty_action_open_url_s open_url; + ghostty_action_close_tab_mode_e close_tab_mode; + ghostty_surface_message_childexited_s child_exited; + ghostty_action_progress_report_s progress_report; + ghostty_action_command_finished_s command_finished; + ghostty_action_start_search_s start_search; + ghostty_action_search_total_s search_total; + ghostty_action_search_selected_s search_selected; + ghostty_action_readonly_e readonly; +} ghostty_action_u; + +typedef struct { + ghostty_action_tag_e tag; + ghostty_action_u action; +} ghostty_action_s; + +typedef void (*ghostty_runtime_wakeup_cb)(void*); +typedef void (*ghostty_runtime_read_clipboard_cb)(void*, + ghostty_clipboard_e, + void*); +typedef void (*ghostty_runtime_confirm_read_clipboard_cb)( + void*, + const char*, + void*, + ghostty_clipboard_request_e); +typedef void (*ghostty_runtime_write_clipboard_cb)(void*, + ghostty_clipboard_e, + const ghostty_clipboard_content_s*, + size_t, + bool); +typedef void (*ghostty_runtime_close_surface_cb)(void*, bool); +typedef bool (*ghostty_runtime_action_cb)(ghostty_app_t, + ghostty_target_s, + ghostty_action_s); + +typedef struct { + void* userdata; + bool supports_selection_clipboard; + ghostty_runtime_wakeup_cb wakeup_cb; + ghostty_runtime_action_cb action_cb; + ghostty_runtime_read_clipboard_cb read_clipboard_cb; + ghostty_runtime_confirm_read_clipboard_cb confirm_read_clipboard_cb; + ghostty_runtime_write_clipboard_cb write_clipboard_cb; + ghostty_runtime_close_surface_cb close_surface_cb; +} ghostty_runtime_config_s; + +// apprt.ipc.Target.Key +typedef enum { + GHOSTTY_IPC_TARGET_CLASS, + GHOSTTY_IPC_TARGET_DETECT, +} ghostty_ipc_target_tag_e; + +typedef union { + char *klass; +} ghostty_ipc_target_u; + +typedef struct { + ghostty_ipc_target_tag_e tag; + ghostty_ipc_target_u target; +} chostty_ipc_target_s; + +// apprt.ipc.Action.NewWindow +typedef struct { + // This should be a null terminated list of strings. + const char **arguments; +} ghostty_ipc_action_new_window_s; + +typedef union { + ghostty_ipc_action_new_window_s new_window; +} ghostty_ipc_action_u; + +// apprt.ipc.Action.Key +typedef enum { + GHOSTTY_IPC_ACTION_NEW_WINDOW, +} ghostty_ipc_action_tag_e; + +//------------------------------------------------------------------- +// Published API + +int ghostty_init(uintptr_t, char**); +void ghostty_cli_try_action(void); +ghostty_info_s ghostty_info(void); +const char* ghostty_translate(const char*); +void ghostty_string_free(ghostty_string_s); + +ghostty_config_t ghostty_config_new(); +void ghostty_config_free(ghostty_config_t); +ghostty_config_t ghostty_config_clone(ghostty_config_t); +void ghostty_config_load_cli_args(ghostty_config_t); +void ghostty_config_load_file(ghostty_config_t, const char*); +void ghostty_config_load_default_files(ghostty_config_t); +void ghostty_config_load_recursive_files(ghostty_config_t); +void ghostty_config_finalize(ghostty_config_t); +bool ghostty_config_get(ghostty_config_t, void*, const char*, uintptr_t); +ghostty_input_trigger_s ghostty_config_trigger(ghostty_config_t, + const char*, + uintptr_t); +uint32_t ghostty_config_diagnostics_count(ghostty_config_t); +ghostty_diagnostic_s ghostty_config_get_diagnostic(ghostty_config_t, uint32_t); +ghostty_string_s ghostty_config_open_path(void); + +ghostty_app_t ghostty_app_new(const ghostty_runtime_config_s*, + ghostty_config_t); +void ghostty_app_free(ghostty_app_t); +void ghostty_app_tick(ghostty_app_t); +void* ghostty_app_userdata(ghostty_app_t); +void ghostty_app_set_focus(ghostty_app_t, bool); +bool ghostty_app_key(ghostty_app_t, ghostty_input_key_s); +bool ghostty_app_key_is_binding(ghostty_app_t, ghostty_input_key_s); +void ghostty_app_keyboard_changed(ghostty_app_t); +void ghostty_app_open_config(ghostty_app_t); +void ghostty_app_update_config(ghostty_app_t, ghostty_config_t); +bool ghostty_app_needs_confirm_quit(ghostty_app_t); +bool ghostty_app_has_global_keybinds(ghostty_app_t); +void ghostty_app_set_color_scheme(ghostty_app_t, ghostty_color_scheme_e); + +ghostty_surface_config_s ghostty_surface_config_new(); + +ghostty_surface_t ghostty_surface_new(ghostty_app_t, + const ghostty_surface_config_s*); +void ghostty_surface_free(ghostty_surface_t); +void* ghostty_surface_userdata(ghostty_surface_t); +ghostty_app_t ghostty_surface_app(ghostty_surface_t); +ghostty_surface_config_s ghostty_surface_inherited_config(ghostty_surface_t, ghostty_surface_context_e); +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_refresh(ghostty_surface_t); +void ghostty_surface_draw(ghostty_surface_t); +void ghostty_surface_set_content_scale(ghostty_surface_t, double, double); +void ghostty_surface_set_focus(ghostty_surface_t, bool); +void ghostty_surface_set_occlusion(ghostty_surface_t, bool); +void ghostty_surface_set_size(ghostty_surface_t, uint32_t, uint32_t); +ghostty_surface_size_s ghostty_surface_size(ghostty_surface_t); +void ghostty_surface_set_color_scheme(ghostty_surface_t, + ghostty_color_scheme_e); +ghostty_input_mods_e ghostty_surface_key_translation_mods(ghostty_surface_t, + ghostty_input_mods_e); +bool ghostty_surface_key(ghostty_surface_t, ghostty_input_key_s); +bool ghostty_surface_key_is_binding(ghostty_surface_t, + ghostty_input_key_s, + ghostty_binding_flags_e*); +void ghostty_surface_text(ghostty_surface_t, const char*, uintptr_t); +void ghostty_surface_preedit(ghostty_surface_t, const char*, uintptr_t); +bool ghostty_surface_mouse_captured(ghostty_surface_t); +bool ghostty_surface_mouse_button(ghostty_surface_t, + ghostty_input_mouse_state_e, + ghostty_input_mouse_button_e, + ghostty_input_mods_e); +void ghostty_surface_mouse_pos(ghostty_surface_t, + double, + double, + ghostty_input_mods_e); +void ghostty_surface_mouse_scroll(ghostty_surface_t, + double, + double, + ghostty_input_scroll_mods_t); +void ghostty_surface_mouse_pressure(ghostty_surface_t, uint32_t, double); +void ghostty_surface_ime_point(ghostty_surface_t, double*, double*, double*, double*); +void ghostty_surface_request_close(ghostty_surface_t); +void ghostty_surface_split(ghostty_surface_t, ghostty_action_split_direction_e); +void ghostty_surface_split_focus(ghostty_surface_t, + ghostty_action_goto_split_e); +void ghostty_surface_split_resize(ghostty_surface_t, + ghostty_action_resize_split_direction_e, + uint16_t); +void ghostty_surface_split_equalize(ghostty_surface_t); +bool ghostty_surface_binding_action(ghostty_surface_t, const char*, uintptr_t); +void ghostty_surface_complete_clipboard_request(ghostty_surface_t, + const char*, + void*, + bool); +bool ghostty_surface_has_selection(ghostty_surface_t); +bool ghostty_surface_read_selection(ghostty_surface_t, ghostty_text_s*); +bool ghostty_surface_read_text(ghostty_surface_t, + ghostty_selection_s, + ghostty_text_s*); +void ghostty_surface_free_text(ghostty_surface_t, ghostty_text_s*); + +#ifdef __APPLE__ +void ghostty_surface_set_display_id(ghostty_surface_t, uint32_t); +void* ghostty_surface_quicklook_font(ghostty_surface_t); +bool ghostty_surface_quicklook_word(ghostty_surface_t, ghostty_text_s*); +#endif + +ghostty_inspector_t ghostty_surface_inspector(ghostty_surface_t); +void ghostty_inspector_free(ghostty_surface_t); +void ghostty_inspector_set_focus(ghostty_inspector_t, bool); +void ghostty_inspector_set_content_scale(ghostty_inspector_t, double, double); +void ghostty_inspector_set_size(ghostty_inspector_t, uint32_t, uint32_t); +void ghostty_inspector_mouse_button(ghostty_inspector_t, + ghostty_input_mouse_state_e, + ghostty_input_mouse_button_e, + ghostty_input_mods_e); +void ghostty_inspector_mouse_pos(ghostty_inspector_t, double, double); +void ghostty_inspector_mouse_scroll(ghostty_inspector_t, + double, + double, + ghostty_input_scroll_mods_t); +void ghostty_inspector_key(ghostty_inspector_t, + ghostty_input_action_e, + ghostty_input_key_e, + ghostty_input_mods_e); +void ghostty_inspector_text(ghostty_inspector_t, const char*); + +#ifdef __APPLE__ +bool ghostty_inspector_metal_init(ghostty_inspector_t, void*); +void ghostty_inspector_metal_render(ghostty_inspector_t, void*, void*); +bool ghostty_inspector_metal_shutdown(ghostty_inspector_t); +#endif + +// APIs I'd like to get rid of eventually but are still needed for now. +// Don't use these unless you know what you're doing. +void ghostty_set_window_background_blur(ghostty_app_t, void*); + +// Benchmark API, if available. +bool ghostty_benchmark_cli(const char*, const char*); + +#ifdef __cplusplus +} +#endif + +#endif /* GHOSTTY_H */ diff --git a/scripts/rebuild.sh b/scripts/rebuild.sh new file mode 100755 index 00000000..b07fc262 --- /dev/null +++ b/scripts/rebuild.sh @@ -0,0 +1,18 @@ +#!/bin/bash +# Rebuild and restart GhosttyTabs app + +set -e + +cd "$(dirname "$0")/.." + +# Kill existing app if running +pkill -9 -f "GhosttyTabs" 2>/dev/null || true + +# Build +swift build + +# Copy to app bundle +cp .build/debug/GhosttyTabs .build/debug/GhosttyTabs.app/Contents/MacOS/ + +# Open the app +open .build/debug/GhosttyTabs.app