Skip to content

[P0-04] Replace polling and unsafe signal flags with an async event source #9

Description

@phranck

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workingpriority: criticalBlocks runtime correctness or the release-quality gate

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions