Getting started

Five minutes to a running TUI.

This page assumes you want a terminal-native app written in Swift. For other targets — embedding in macOS/iOS, hosting in the browser, cross-compiling for Android — see platforms after you have a hello world running.

1 — Prerequisites

  • Swift 6.3.1 via swiftly. The repo pins this exact version.
  • macOS 15+ for local Apple-platform development.
  • A terminal that supports raw mode and ANSI escapes. Any modern macOS Terminal, iTerm2, Alacritty, kitty, WezTerm, or Ghostty.

For the full toolchain story (wasm SDK, Bun, Xcode, Android NDK), see toolchains.

Add the dependency.

TerminalUI is delivered as a Swift package. The TerminalUICLI product owns the default terminal-native App.main() story and is what you want for executable apps. The root TerminalUI package is library-only.

Package.swift swift
// swift-tools-version:6.0
import PackageDescription

let package = Package(
  name: "HelloTerminal",
  platforms: [.macOS(.v15)],
  dependencies: [
    .package(
      url: "https://github.com/GoodHatsLLC/swift-terminal-ui.git",
      branch: "main"
    ),
  ],
  targets: [
    .executableTarget(
      name: "HelloTerminal",
      dependencies: [
        .product(
          name: "TerminalUICLI",
          package: "swift-terminal-ui"
        ),
      ]
    ),
  ]
)

A real, runnable, ~30-line app.

Save this as Sources/HelloTerminal/HelloApp.swift. Note the four pieces: @main App declaration, a single Scene, the WindowGroup root, and the View that draws the body.

HelloApp.swift swift
import TerminalUI
import TerminalUICLI

@main
struct HelloApp: App {
  var body: some Scene {
    WindowGroup("Hello") {
      GreetingView()
    }
  }
}

struct GreetingView: View {
  @State private var name = "world"
  @FocusState private var nameFocus: Bool

  var body: some View {
    VStack(alignment: .leading, spacing: 1) {
      Text("Hello, \(name)!").bold()
      TextField("name", text: $name)
        .focused($nameFocus)
      Text("Press Tab to focus the field. ⌘C to quit.")
        .foregroundStyle(.secondary)
    }
    .padding(.init(horizontal: 1, vertical: 0))
  }
}

From the project directory.

First run takes a minute as Swift Package Manager fetches and compiles the dependencies. Subsequent runs are incremental.

zsh bash
# From your project directory
swiftly run swift run HelloTerminal

Anatomy

The four pieces above each play a specific role:

  • @main App — the root. TerminalUICLI provides the default App.main() implementation, which owns terminal raw-mode acquisition, the alternate-screen buffer, and the run loop. You don't write a main.swift.
  • Scene — the unit of presentation. The shipped runtime renders one active scene per host. Multi-scene apps are supported, but only one is on screen at a time.
  • WindowGroup — the full-canvas container. It sizes itself to the current terminal proposal and clips drawing to terminal bounds.
  • View — body-only, identity-keyed. Modifier order matters. @State persists across rerenders by identity-path plus source location.

That's the model. The same authored App can flow into three execution modes: terminal-native (above), WASI, or host-managed embedding inside macOS / iOS / web shells.

5 — Five next things