A SSH app for iOS built with GhosttyKit.
Terminal & Shell
- SSH terminal sessions: Full VT100/xterm emulation via GhosttyKit terminal APIs.
- PTY shell: Bidirectional async bridge (SSHPTYBridge actor) with runtime resize.
- Terminal rendering: Powered by GhosttyKit with Glassdeck-owned surface integration and lifecycle code.
- Render coalescing:
scheduleRender()with UIKitlayoutSubviewsbatching to consolidate rapid state changes. - Local scrollback: 10,000 lines default, configurable 1K–100K, Metal-accelerated viewport scrolling.
Connectivity
- Auto-reconnection: Exponential backoff (5 attempts, 1–30s delay, 2× multiplier), transient vs permanent failure classification.
- Session persistence & restore: JSON snapshots to UserDefaults, auto-restore on foreground, optional Core Location background keep-alive.
- Connection profiles: CRUD with JSON persistence, password or SSH key auth, notes, last-connected date.
- TOFU host key verification: Keychain-backed known_hosts, SHA-256 fingerprints, auto-trust new / reject mismatch.
Input & Hardware
- Hardware keyboard: 90+ UIKeyCommands (Ctrl+letter, arrows, function keys, Tab, Escape, PageUp/Down, Home/End).
- Touch/pointer input: UIPointerInteraction with I-beam cursor, full SGR mouse reporting, drag tracking.
- IME support (Experimental): UITextInput with marked text / composing flag.
Advanced Tools
- External display routing: Dedicated scene delegate, remote pointer overlay, display routing picker.
- Terminal settings: Per-display-target profiles (iPhone vs external monitor), 8 color schemes, font size, cursor style, bell.
Glassdeck/
├── Glassdeck/ iOS app: SwiftUI views, UIKit terminal surface, input handling
│ ├── App/ App entry, environment, delegates, animation demo
│ ├── Input/ Keyboard, pointer, IME input coordination
│ ├── Models/ Session management, persistence, credentials, background keep-alive
│ ├── Remote/ External display geometry, remote trackpad coordination
│ ├── Scenes/ Main + external display scene delegates
│ ├── SSH/ SSH session observable model
│ ├── Terminal/ GhosttySurface UIView, Metal renderer, SwiftUI wrapper
│ ├── Views/ All SwiftUI views (connections, terminal, settings, etc.)
│ └── Resources/ Assets.xcassets, AppIcon.icon
├── GlassdeckCore/ Shared library — the single source of truth for SSH, terminal, models
│ ├── Models/ ConnectionProfile, ConnectionStore, AppSettings, RemoteControlMode
│ ├── SSH/ SSHConnectionManager, SSHPTYBridge, SSHAuthenticator,
│ │ HostKeyVerifier, SSHReconnectManager, SSHKeyManager, etc.
│ └── Terminal/ GhosttyKitSurfaceIO, TerminalConfiguration (8 themes), TerminalIO, types
├── Frameworks/ GhosttyKit.xcframework (locally materialized dependency artifact)
├── GlassdeckApp.xcodeproj/ Checked-in native Xcode project
├── Scripts/ Build, run, test automation
├── Tools/
│ └── GlassdeckBuild/ Native host-side build/test orchestration runner
│ ├── Package.swift
│ └── Package.resolved
├── Tests/ Unit, UI, integration, performance tests
├── Vendor/ Forked swift-ssh-client dependency
├── Backlogs/ Code review findings and backlog tracking
├── AGENTS.md Repo-specific workflow rules for Codex and sub-agents
├── LICENSE MIT
├── README.md English documentation
└── README-ko.md Korean documentation
SwiftUI Views (ConnectionListView, SessionTabView, TerminalContainerView)
│
SessionManager (orchestrator — @MainActor, 1082 lines)
SessionLifecycleCoordinator (lifecycle events, persistence, restore)
│
┌────┼────────────────┐
│ │ │
SSH Layer Terminal UI Input Layer
SSHConnectionManager GhosttySurface (UIView) KeyboardInputHandler
SSHAuthenticator Metal renderer (CI) PointerInputHandler
SSHPTYBridge GhosttyKitSurfaceIO SessionKeyboardInputHost
HostKeyVerifier TerminalConfiguration RemoteTrackpadCoordinator
SSHReconnectManager
│
GlassdeckCore (shared library — only source of truth)
│
External: GhosttyKit · swift-ssh-client · SwiftNIO SSH · Swift Crypto
Data flow: User connects → SSHConnectionManager authenticates (password/key) → HostKeyVerifier checks TOFU → shell opened with PTY → SSHPTYBridge bridges shell↔terminal bidirectionally → GhosttyKitSurfaceIO passes VT stream to GhosttyKit → GhosttySurface renders via Metal → Input flows back through VT encoding → shell.
GhosttyKitSurfaceIO.swift— Swift adapter between terminal I/O and GhosttyKit callbacks.GhosttyTerminalView.swift— UIKit + Metal surface wrapper and input bridge.- Terminal behavior is powered by
GhosttyKit. - The vendored terminal framework should stay in sync with the managed Ghostty source state.
Frameworks/GhosttyKit.xcframework — local dependency artifact used by the checked-in
Xcode project. It is gitignored/untracked by default and materialized as needed.
Build or refresh from managed Ghostty source:
swift run --package-path Tools/GlassdeckBuild glassdeck-build deps ghosttyCommon profile:
--profile release-fastfor release-like artifacts.--profile debug(default).
Builds use the checked-in GlassdeckApp.xcodeproj and glassdeck-build runner:
swift run --package-path Tools/GlassdeckBuild glassdeck-build build --scheme appBuild & launch on iOS simulator:
swift run --package-path Tools/GlassdeckBuild glassdeck-build run --scheme appUnit tests on simulator:
swift run --package-path Tools/GlassdeckBuild glassdeck-build test --scheme unitLive SSH integration tests vs Docker (auto-starts container, runs smoke checks first):
swift run --package-path Tools/GlassdeckBuild glassdeck-build test --scheme unit --only-testing "GlassdeckAppTests/Integration.test"Terminal rendering performance tests vs live Docker SSH:
swift run --package-path Tools/GlassdeckBuild glassdeck-build test --scheme ui --only-testing "GlassdeckAppUITests/TerminalPerformance"Animation rendering performance tests (simulator):
swift run --package-path Tools/GlassdeckBuild glassdeck-build test --scheme ui --only-testing "GlassdeckAppUITests/Animation"Animation rendering performance tests (device) - requires DEVICE_ID:
swift run --package-path Tools/GlassdeckBuild glassdeck-build test --scheme ui --only-testing "GlassdeckAppUITests/Animation"UI tests with screenshot capture + artifact export:
swift run --package-path Tools/GlassdeckBuild glassdeck-build test --scheme ui --only-testing "GlassdeckAppUITests/..."Verify animations visually render (screenshot diff):
swift run --package-path Tools/GlassdeckBuild glassdeck-build test --scheme ui --only-testing "GlassdeckAppUITests/AnimationDemo"Result lookup and artifact inspection from runner output:
swift run --package-path Tools/GlassdeckBuild glassdeck-build artifacts --command build
swift run --package-path Tools/GlassdeckBuild glassdeck-build artifacts --command test| Flag | Description |
|---|---|
--worker <id> |
Isolate workspace artifacts for parallel workers |
--scheme <name> |
Select build/run/test scheme (app, unit, ui) |
--simulator <target> |
Simulator by name or exact UDID (iPhone 17 or 48B...) for run/test paths. Duplicate exact names now fail to avoid ambiguous device selection. |
--dry-run |
Print the computed xcodebuild/utility command only |
--only-testing <target> |
Forward xcodebuild test filtering flags |
Default: iPhone 17 on latest iOS runtime. Prefer explicit UDID with SIMULATOR_ID=<udid> or --simulator <udid> when scripts run in environments with duplicated names.
.build/glassdeck-build/logs/<command>/— raw command logs.build/glassdeck-build/results/<command>/— xcresult bundles.build/glassdeck-build/artifacts/<command>/latest/— latest exported artifacts and summary.build/glassdeck-build/derived-data/<worker>/— worker-scoped DerivedData reuse
Note: GlassdeckApp.xcodeproj is source-tracked; regenerate scripts are no longer part of normal workflow.
Canonical live test endpoint (replaces separate Raspberry Pi).
Requirements: Docker Desktop on Mac.
swift run --package-path Tools/GlassdeckBuild glassdeck-build docker up
swift run --package-path Tools/GlassdeckBuild glassdeck-build docker down- Port: 22222 (default)
- User: glassdeck
- Auth: Password + Key both enabled
Seeded home directory:
~/bin/health-check.sh~/testdata/preview.txt~/testdata/nested/dir/info.txt~/testdata/nano-target.txt~/upload-target/
Note: For physical iPhone testing, iPhone and Mac must be on the same LAN.
- Start Docker SSH:
swift run --package-path Tools/GlassdeckBuild glassdeck-build docker up - Or run full suite:
swift run --package-path Tools/GlassdeckBuild glassdeck-build test --scheme unit --only-testing "GlassdeckAppTests/Integration.test" - Launch app on simulator or iPhone
- Create profile with printed host/port, connect via password auth
- Create second profile or switch to SSH key auth
- Run
~/bin/health-check.sh,pwd,ls ~/testdata - Verify rendering, typing, paste, special keys, resize, disconnect, reconnect
- With external monitor + physical keyboard: route session, test Mouse/Cursor mode, two-finger scroll, View Local Terminal,
nano --mouse ~/testdata/nano-target.txt
| Dependency | Source | Purpose |
|---|---|---|
| GhosttyKit | Frameworks/GhosttyKit.xcframework (materialized locally, untracked) |
Terminal + rendering engine |
| swift-ssh-client | Vendor/ (fork) | High-level SSH client |
| swift-nio-ssh | SPM (≥0.9.0) | Low-level SSH protocol |
| swift-nio | SPM (≥2.65.0) | Async networking |
| Swift Crypto | (via NIO SSH) | Ed25519/P256 keys, SHA-256 |
| Core Location | System | Optional background keep-alive |
- GlassdeckCore is the only source of truth for shared SSH, key, model, and terminal logic.
- Terminal implementation is centered on GhosttyKit and a local host-side materialized xcframework.
glassdeck-build deps ghosttyrefreshes that dependency artifact when needed.- Docker SSH server is the canonical acceptance target.
MIT — see LICENSE.
