Drop a native, GPU-accelerated terminal into your SwiftUI app — in about five lines.
Termini wraps Ghostty's terminal engine in a small, SwiftUI-first
API. You get a real terminal surface (true colors, ligatures, Metal rendering) as an
ordinary View, plus ready-made state machines for the two things people actually do:
run a local shell (macOS) or connect over SSH (macOS + iOS).
The SwiftUI surface is kept deliberately small so the rendering backend can change later without breaking your app.
import SwiftUI
import Termini
struct ContentView: View {
@State private var workspace = TerminiLocalPTYWorkspace() // a local login shell
var body: some View {
TerminiTerminalView(controller: workspace.controller)
.task { workspace.start() }
.onDisappear { workspace.stop() }
}
}That's a working terminal. The rest of this README shows how to install it, run the demos, and go deeper.
- What you get
- Requirements
- Install
- Try the demos
- Pick your products
- How the pieces fit
- Repo layout
- Guides — appearance, SSH, custom transports, host verification, debugging
- Developing Termini itself
- Good to know
- License
- 🖥️ A terminal as a SwiftUI
View—TerminiTerminalView(controller:), drop it anywhere. - 🐚 Local shell on macOS —
TerminiLocalPTYWorkspaceruns aforkpty-backed login shell. - 🔌 SSH on macOS + iOS —
TerminiSSHWorkspaceover SwiftNIO/NIOSSH, with trust-on-first-use host keys. - 🎨 Themes & fonts —
TerminiTerminalAppearancefor reusable color/font profiles. - 🧩 Bring-your-own transport — wire
TerminiTerminalControllerto any byte stream you like. - 📦 Zero manual setup — SwiftPM downloads the prebuilt
GhosttyKit.xcframeworkfor you.
| macOS | 14+ |
| iOS | 17+ |
| Toolchain | Swift 5.9 / Xcode 15+ |
iOS is SSH-only: the sandbox blocks local
fork/PTY, so the local-shell APIs are macOS-only.
File → Add Package Dependencies… and enter:
https://github.com/arach/Termini.git
Then add the products you need to your target (see Pick your products).
// Package.swift
dependencies: [
.package(url: "https://github.com/arach/Termini.git", from: "0.1.0")
],
targets: [
.target(
name: "YourApp",
dependencies: [
.product(name: "Termini", package: "Termini"),
// add only if you need SSH:
.product(name: "TerminiSSH", package: "Termini"),
]
)
]On first build, SwiftPM downloads GhosttyKit from the project's GitHub Releases — no
manual framework wrangling. (Working on Termini itself? See
Developing Termini itself for the local-build override.)
macOS — a local login shell in a window:
swift run TerminiDemoiOS — generate the Xcode project with XcodeGen, then run the TerminiIOSDemo scheme:
xcodegen generate
open TerminiDemos.xcodeprojFor the iOS demo to auto-connect, set TERMBRIDGEKIT_SSH_* environment variables in the
scheme (see Environment variables).
Termini ships three SwiftPM products. Depend on the minimum your app shape needs:
| Your app | Depend on | Transport |
|---|---|---|
| macOS, local shell | Termini |
Local PTY via TerminiLocalPTYWorkspace |
| macOS, remote shell | Termini + TerminiSSH |
SSH via TerminiSSHWorkspace |
| iOS | Termini + TerminiSSH |
SSH only (iOS blocks local PTY) |
Termini holds the renderer, controller, appearance model, and the macOS local-PTY
transport — it depends on GhosttyKit only. TerminiSSH is the only product that
pulls in SwiftNIO / NIOSSH, so macOS-direct apps stay lean by skipping it.
TerminiTerminalView SwiftUI view — wraps the Ghostty surface
TerminiTerminalController Bridges the view to your transport layer
TerminiTerminalAppearance Theme + font sizing model
— in Termini (macOS local shell) —
TerminiLocalPTYWorkspace @Observable lifecycle for a local shell
TerminiLocalPTYProcess forkpty-backed process transport
— in TerminiSSH —
TerminiSSHWorkspace @Observable lifecycle for an SSH session
TerminiSSHSession Low-level NIOSSH client wired to the controller
TerminiConnectionConfig Validated SSH connection form model
The mental model: a Workspace owns a Controller, the TerminiTerminalView
renders that controller, and the controller talks to a transport (local PTY, SSH, or
your own). For most apps you only touch the Workspace and the View.
Sources/
Termini/ Renderer, controller, appearance, macOS local PTY
TerminiSSH/ SSH session, host-key handling, SSH workspace
Examples/
TerminiDemo/ macOS demo app (swift run TerminiDemo)
TerminiIOSDemo/ iOS demo app (xcodegen + Xcode)
Tests/ XCTest suites for both products
scripts/ GhosttyKit build / install / packaging helpers
vendor/ Local GhosttyKit override drop point (gitignored framework)
patches/ Ghostty patches for the embedding APIs
project.yml XcodeGen spec for the demo apps
Package.swift Products, targets, and the GhosttyKit binary target
Pass quick overrides inline:
TerminiTerminalView(
controller: workspace.controller,
showsSystemKeyboard: true,
fontSize: 13
)…or build a reusable theme/font profile:
let appearance = TerminiTerminalAppearance(
theme: .midnightBloom,
fontSize: 13,
fontFamily: "SF Mono"
)
TerminiTerminalView(controller: workspace.controller, appearance: appearance)let spec = TerminiProcessSpec(
executableURL: URL(fileURLWithPath: "/bin/zsh"),
arguments: ["-l"],
environment: [:],
workingDirectoryURL: URL(fileURLWithPath: NSHomeDirectory())
)
@State private var workspace = TerminiLocalPTYWorkspace(processSpec: spec)import SwiftUI
import Termini
import TerminiSSH
struct ContentView: View {
@State private var workspace = TerminiSSHWorkspace(
connection: .init(startupCommand: "tmux new -A -s myapp")
)
var body: some View {
VStack {
TerminiTerminalView(controller: workspace.controller)
Button(workspace.isConnected ? "Disconnect" : "Connect") {
Task { await workspace.toggleConnection() }
}
.disabled(!workspace.isConnected && !workspace.canConnect)
}
.task {
if workspace.loadEnvironmentConfigurationIfAvailable() {
await workspace.connect()
}
}
}
}Use TerminiTerminalController directly to wire up any byte stream:
@State private var controller = TerminiTerminalController()
myTransport.onData = { data in
controller.processRemoteOutput(data) // bytes in → screen
}
controller.onTransportWrite = { data in
myTransport.write(data) // keystrokes → transport
}
controller.onSizeChange = { size in
myTransport.resize(cols: size.columns, rows: size.rows)
}TerminiSSH uses trust-on-first-use by default:
- The first successful connection stores the server's
SHA256:fingerprint. - Later connections to the same
host:portmust present the same fingerprint. - Require a pre-trusted host with
.requireStoredHostKey. - Pin an explicit fingerprint with
hostKeyFingerprint. - Bypass checks with
.acceptAny— keep this a local-debug-only escape hatch.
Encrypted private keys are not supported.
| Variable | Effect |
|---|---|
TERMBRIDGEKIT_SSH_* |
Preloads SSH connection config (TerminiConnectionConfig.demoEnvironment() / workspace.loadEnvironmentConfigurationIfAvailable()). |
TERMBRIDGEKIT_DEBUG_INPUT=1 |
Logs keyboard and mouse events. |
The
TERMBRIDGEKIT_prefix is retained from the project's previous name (see Good to know).
Clone, then build and test like any SwiftPM package:
swift build
swift testBy default the package downloads a released GhosttyKit.xcframework. To test against your
own Ghostty checkout, drop a framework into
vendor/ghostty/macos/GhosttyKit.xcframework and Package.swift will prefer it
automatically. The helper scripts do the work:
# Install a framework you built elsewhere:
./scripts/install-ghosttykit.sh /path/to/GhosttyKit.xcframework
# Or rebuild from a checkout in vendor/ghostty and install in one step:
./scripts/build-ghosttykit.sh
# Build against a specific Ghostty ref first:
./scripts/build-ghosttykit.sh --fetch --ref <tag-or-commit>
# Package the installed framework as a SwiftPM release artifact:
./scripts/package-ghosttykit-release.shThe build helper applies the tracked patches for the pinned Ghostty release
from patches/ghostty/0.1.6/ before compiling. Override that source with
--patch-dir when porting Termini to another Ghostty revision. Release
packaging also verifies that the iOS Simulator slice is a universal arm64 +
x86_64 archive, so Intel-compatible artifacts cannot be published accidentally.
- Rename: Termini evolved from
TermBridgeKit(renamed at 0.1.0). The bundledGhosttyKit.xcframeworkis still hosted on the legacyarach/TermBridgeKitGitHub releases, and a few env-var names still carry theTERMBRIDGEKIT_prefix. - 0.2.0 product split: SSH moved into its own
TerminiSSHproduct so macOS-direct apps can ship the renderer + local shell without carrying SwiftNIO/NIOSSH. No renderer or SSH type was removed — existing SSH integrations just addimport TerminiSSHalongsideimport Termini. - Third-party code: see
THIRD_PARTY_NOTICES.mdfor bundled dependencies, including Ghostty.
Termini is released under the MIT License.
Termini stands on Ghostty — the bundled GhosttyKit.xcframework
is Ghostty's terminal engine, © Mitchell Hashimoto and the Ghostty contributors, also
MIT-licensed. See THIRD_PARTY_NOTICES.md for the full notice.