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
This commit is contained in:
Lawrence Chen 2026-01-22 01:16:24 -08:00
commit c5bd543fe0
14 changed files with 2682 additions and 0 deletions

73
Sources/TabManager.swift Normal file
View file

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