Skip to content

Architecture

Terminality is built on a pure-Rust MVVM core (libszt) with replaceable UI clients. The desktop uses Tauri 2 + React; mobile uses React Native with UniFFI-generated Swift/Kotlin bindings.

Process Model

┌─ One Desktop Process ───────────────────────────────┐
│  libszt Context (shared)                              │
│  ├─ WorkspaceModel                                   │
│  ├─ HostCatalogModel                                 │
│  ├─ SessionRegistry                                  │
│  ├─ Vault (EncryptedVaultRepository)                 │
│  ├─ SettingsModel                                    │
│  ├─ QuickCommandModel                                │
│  └─ AsyncExecutor (Tokio)                            │
│                                                      │
│  ┌─ Window A ───────┐  ┌─ Window B ───────┐         │
│  │ WebviewWindow    │  │ WebviewWindow    │         │
│  │ Tab 1, Tab 2...  │  │ Tab 1, Tab 2...  │         │
│  │ Split trees       │  │ Split trees       │        │
│  └──────────────────┘  └──────────────────┘         │
│                                                      │
│  ┌─ Dialog Windows ────────────────────────────┐     │
│  │ Settings | Connection Library | New Host    │     │
│  │ tmux Sessions | Open Source Licenses        │     │
│  └─────────────────────────────────────────────┘     │
└──────────────────────────────────────────────────────┘

One process, one libszt Context. Many native windows. Dialog Windows share state but own no main-window tabs or panes.

Layered Architecture

┌─────────────────────────────────┐
│  React / React Native           │  Presentation
│  xterm.js, canvas, touch        │
├─────────────────────────────────┤
│  @szt/libszt                      │  TypeScript facade
│  ViewModel hooks, adapters      │  Handwritten bindings
├─────────────────────────────────┤
│  Tauri Commands / UniFFI        │  IPC boundary
│  Binary Channels (terminal)     │
├─────────────────────────────────┤
│  libszt (Pure Rust)              │  Behavioral core
│  ┌─────────────────────────┐    │
│  │ ViewModels              │    │  UI state, commands, events
│  │ Models                  │    │  Domain state, validation
│  │ Services                │    │  I/O, PTY, SSH, networking
│  │ Binding Interface       │    │  Typed foreign-language contract
│  │ Vault / Crypto          │    │  Encryption, persistence
│  └─────────────────────────┘    │
├─────────────────────────────────┤
│  Platform Adapters              │
│  PTY, Keychain, trash, fonts   │
└─────────────────────────────────┘

MVVM Component Model

All application behavior lives in typed components registered in a shared Context:

LayerRoleExample
ModelDomain state, validation, serializable command argsHostCatalogModel, WorkspaceModel
ServiceI/O, orchestration, dependency registrationTmux Module, FileService, OminiService
ViewModelUI-facing state, commands, events. Owns keyed childrenWorkspaceViewModel, SettingsViewModel

Component lookup is checked by TypeId at Rust level. Foreign clients use stable versioned contracts. ViewModel facades are handwritten — no reflection scan or syntax parser.

State, Command, Event Primitives

rust
State<T>        // Observable, ordered delivery, panic-isolated
ReadonlyState<T> // Non-mutating subscription
Command<T>      // Cancellable intent, typed argument
Task<T>         // Async execution with awaitable cancellation
Event<T>        // Bidirectional or readonly, explicit subscription ownership

Async Runtime

Production async Commands run on a Context-owned Tokio reactor through a generic AsyncExecutor boundary:

  • Awaitable cancellation: tasks cooperatively yield on cancel.
  • Linearized commit: CancellationToken::try_commit — cancellation wins before the side effect, or returns false. A Task cannot publish Cancelled and then mutate state in the background.
  • Safe shutdown: even when the final owner is released by a worker task.

Data Flow: Terminal Bytes

PTY reader
  → bounded queue (64 × 8 KiB)
  → 16 ms or 32 KiB batch
  → raw Tauri Channel (binary)
  → xterm TerminalSurface (WebGL)

Terminal bytes use a specialized raw channel — not JSON, not the binding protocol. Control state and intent use opaque handles plus versioned JSON slots.

Data Flow: Binding Protocol

React Component
  → useViewModel("workspace")         // TypeScript hook
  → WorkspaceViewModel (handwritten)  // Typed facade
  → Tauri invoke / UniFFI call        // IPC
  → BindingRuntime                     // Rust dispatch
  → ViewModel.commands.handle(...)    // Typed command execution
  → State update → UI re-render       // Observable notification

useViewModel("workspace") infers WorkspaceViewModel through a handwritten ViewModelMap. The complete native contract is validated before the facade is exposed.

Repository Map

text
apps/desktop/          Tauri 2 + React + xterm.js desktop application
apps/mobile/           React Native bare iOS/Android + TurboModule spec
crates/szt-core/        Pure-Rust libszt: base, models, viewmodels, services, i18n, binding
crates/szt-mobile-ffi/  UniFFI boundary for generated Swift/Kotlin bindings
packages/contracts/    Generated TypeScript DTOs (mechanical export, never edit by hand)
packages/libszt/        Handwritten TypeScript facade, React hooks, adapter protocol
assets/fonts/           Audited, pinned MesloLGS NF font assets

Runtime vs Durable State

Runtime (ephemeral)Durable (encrypted in SQLite)
Windows, tabs, splits, panesHost profiles
Session registry, PTY childrenSettings
ScrollbackQuick Commands
Discovered machine snapshotsQCS recent history
Command Composer draftCredential records (future)

Closing the app drops the Rust session registry (kills PTY children). The next launch creates a fresh runtime layout and empty discovery snapshot. Only the Vault survives.

Supported Targets

macOS 13+ (Intel & Apple Silicon) · Windows 11 x64 · Ubuntu 22.04/24.04 x64 (X11 & Wayland) · iOS 16+ · Android 10+ ARM64

CI compilation alone does not certify this matrix; release gates require physical/virtual device acceptance runs.

Terminality — Connect Everycorner