Problem
AppRunner executes a synchronous MainActor polling loop with usleep in Sources/TUIkit/App/App.swift:104-191. MainActor tasks that update @State cannot resume cooperatively, idle apps still wake repeatedly, and pulse/cursor timers create background mutations that only partly hide missed invalidation.
Sources/TUIkit/App/SignalManager.swift:17-51 also uses unprotected Swift global Bool flags from POSIX signal handlers. The claim that missed signals are acceptable is not a valid Swift 6 memory-safety contract. Terminal writes can stop silently on EINTR or an error.
Proposed solution
- Drive one async per-app event loop from state invalidations, input readiness, resize/signal events, animation deadlines, and shutdown.
- Use an async-signal-safe bridge such as a self-pipe or platform-appropriate dispatch source; do not mutate Swift globals in a signal handler.
- Use
ContinuousClock plus an injectable fake clock for deadlines.
- Serialize input, scene-phase changes, state commits, renders, and shutdown through the runtime.
- Retry or surface interrupted/failed terminal writes instead of silently truncating output.
Acceptance criteria
- A
.task that returns to MainActor and mutates @State completes without polling or manual yielding.
- An idle app has no periodic render or timer wakeup.
- SIGWINCH and termination signals are delivered without data races or unsafe handler work.
- Shutdown cancels all sources and tasks and restores the terminal deterministically.
- Input and terminal-write interruption/error cases have integration tests.
- Swift 6.0 build, tests, lint, and DocC are warning-free on macOS and Linux.
Dependencies
Depends on the per-app runtime interface from P0-03.
Parallelization
After the runtime interface is fixed, this can run in parallel with structural identity and terminal-surface work if ownership of App.swift, RenderLoop.swift, and SignalManager.swift stays here.
Commit structure
- Split this issue into small, thematic, independently revertible commits wherever the work can remain coherent.
- Every commit must build and keep its applicable tests/gates green. Do not commit an intentionally failing regression test; use local/known-issue characterization or land the test with the smallest fix.
- Keep characterization/fixtures, mechanical renames or moves, semantic changes, and documentation/migration updates separate when each step remains green.
- The issue boundary is not a commit boundary; multiple commits are expected for independently reversible changes.
Problem
AppRunnerexecutes a synchronous MainActor polling loop withusleepinSources/TUIkit/App/App.swift:104-191. MainActor tasks that update@Statecannot resume cooperatively, idle apps still wake repeatedly, and pulse/cursor timers create background mutations that only partly hide missed invalidation.Sources/TUIkit/App/SignalManager.swift:17-51also uses unprotected Swift global Bool flags from POSIX signal handlers. The claim that missed signals are acceptable is not a valid Swift 6 memory-safety contract. Terminal writes can stop silently on EINTR or an error.Proposed solution
ContinuousClockplus an injectable fake clock for deadlines.Acceptance criteria
.taskthat returns to MainActor and mutates@Statecompletes without polling or manual yielding.Dependencies
Depends on the per-app runtime interface from P0-03.
Parallelization
After the runtime interface is fixed, this can run in parallel with structural identity and terminal-surface work if ownership of
App.swift,RenderLoop.swift, andSignalManager.swiftstays here.Commit structure