For SwiftUI developers

SwiftUI's shape. The terminal's rules.

If you write SwiftUI, you already write SwiftTUI: var body: some View,@State, @Binding, stacks, modifiers, focus, and animation carry over intact. What changes is deliberate: a small set of terminal-native rereadings, applied consistently, and explicitly recorded.

01

The promise

SwiftTUI mirrors SwiftUI's shape. Where an identical behavior does not fit a terminal experience, the default is terminal-native. The API stays SwiftUI-shaped. The framework implements the subsets of SwiftUI which map to high-value terminal use, and skips deprecated surfaces. For the APIs it does implement, semantic consistency is a hard constraint.

02

The cell is the unit of truth

Geometry is integer terminal cells: frame(width:height:) takes Int, and there is no CGFloat. Two named coordinate domains keep the model honest: the CellRect grid for layout and rasterization, and continuous cell space for gestures, hover, and Canvas. padding()'s default is one cell. A border occupies a cell too and defaults to insetplacement. Use outset placement to grow the frame instead of eating content.

SwiftUI habit
VStack(spacing: 12) {
  Text("Deploys").font(.title2)
  content
}
.padding()           // platform-adaptive points
.border(.secondary)  // overlay; no layout effect
SwiftTUI
VStack(spacing: 1) {
  Text("Deploys").bold()
  content
}
.padding()                  // one cell default.
.border()                   // default inset, border occupies a content cell
.border(placement: .outset) // convenience: border grows the frame

Why?: a terminal allocates whole cells; the geometry is inherently integer based., and a border that pretended to be a zero-cost overlay would eat your content.

03

State the authority

Navigation and presentation are strictly data-driven. There is deliberately no NavigationLink<Label, Destination> where Destination: View. Every screen stays derivable from, and mutable through, your app's data, which is what makes a terminal UI scriptable, testable, and deep-linkable.

SwiftUI habit (iOS 13 era API)
NavigationLink("Build 9") { BuildView(id: 9) }
SwiftTUI
Button("Build 9") { selectedBuild = .build(9) }

Why?: a UI that can be driven, replayed, and tested is one whose every state is data; controls that carry hidden navigation side effects break that contract.

04

Modern-only, portable data flow

Models are @Observable classes; the Combine-era ObservableObject / @Published / @StateObject family does not exist. Strict concurrency @MainActor annotations are visible in the API to allow the framework to safely run other parts of the view lifecycle in the background.

SwiftUI habit (Combine era)
final class DeployModel: ObservableObject {
  @Published var builds: [Build] = []
}

struct Dashboard: View {
  @StateObject private var model = DeployModel()
  ...
}
SwiftTUI
@MainActor @Observable
final class DeployModel {
  var builds: [Build] = []
}

struct Dashboard: View {
  @State private var model = DeployModel()
  ...
}

Why?: Strict concurrency enforced for views lets rendering leave the main actor without undefined behavior.

05

Keyboard-first interaction

Focus traversal is geometry-aware and wraps: there is no surrounding native UI for focus to escape to, so the chain cycles. Key bindings are authored with keyCommand rather than keyboardShortcut, and onKeyPress is reshaped for a terminal byte stream: complete key events, no down/up/repeat phases, because a terminal has none to observe.

Why?: the keyboard is the terminal's native input; the APIs are fit to it.

06

Deterministic chrome from value metadata

Tabs are declared as Tab("My Label", value: .myValue), Table takes column metadata with positional row cells, toolbar items are ToolbarItemConfig values, and Picker displays only label text. One stance, four surfaces: where SwiftUI resolves arbitrary label view trees into pixels, terminal chrome is most often text - so the APIs emphasize those overloads.

Why?: a tab strip, a column header, and a picker row are usually a line of text; value metadata renders them predictably where a label view tree cannot.

07

Terminal-native chrome, defined behavior

fullScreenCover is chromeless. background fills the view's bounds without bleeding. Borders occupy an explicit cell. The List focus highlight is row-shaped, not container-wide. Alerts and confirmation have a defined FIFO queue. Where something can fail, the API says so: environment verbs like \.openLinkAction and \.resetFocus return Bool, and the runtime reports issues.

Why?Cells are finite and restrictive so defaults are tuned to match. And to help a bit further, SwiftTUI polishes some behavior SwiftUI leaves undefined — because it doesn't have to support heterogeneous AppKit and UIKit rendering under the hood.

08

Accessibility and capture as defaults

Reduced motion changes the rendering itself: spinners go static and phase animations hold their first phase. CI=true or a non-TTY stdout selects the same stable built-in. By default, every App is also a CLI command that ships --accessible (--no-color and --reduce-motion), and --ascii configs, and an in-browser view triggered with --web and served via an in-binary http server (macOS, Linux, and iOS; on Windows the umbrella ships the terminal surface only) — all out of the box.

Why?: Many TUIs are have serious accessibility issues. SwiftTUI's declarative API allows it to provide alternative behaviors for screen readers without putting more burden on the developer.

09

What SwiftTUI adds

The differences run in both directions. Alongside the SwiftUI-shaped surface, SwiftTUI ships capabilities SwiftUI has no analog for.

Terminal-program embedding

A real child terminal program as authored content: TerminalView, pty-backed sessions, and a tabbed/split-pane workspace layer.

Presentation extensions

toast and popoverTip presentation families, with FIFO-defined prompt queueing.

FIGlet banner text

TextFigure renders FIGlet text and integrates it fully with the layout system so the banner text wraps if needed.

Border styling

Terminal borders take a cell. They're chonky. SwiftTUI makes this regrettable truth a little nice by adding an API overload for per-side border styles... And even animatable perimeter gradients with BorderBlend.

Open style protocols

SwiftTUI's style API protocols (ButtonStyle, ListStyle, PickerStyle, etc.) are public and extensible or will be soon. Even the families SwiftUI keeps closed.

One codebase, four hosts

Your SwiftTUI views run not only on the terminal, but also inside SwiftUI on macOS and iOS, in the browser via WASI, and on Android.

10

Recorded in the open

Departures from SwiftUI are listed in the framework's documentation. Some are intentional and some are known gaps to fix. Many are awaiting a good terminal usecase. If you find a surprising hole, open an issue.

Ratified: a deliberate, recorded stanceProvisional: deliberate today, held looselyGap: a recorded shortfall, not a roadmap