Rendering and UI
The console model, the device rendering probe, input consumption and resize reflow have passed their gates (see the roadmap). The integrated terminal, the tab surface and the general composited UI described here are planned; Managed UI and console preview defines their gates.
Scope
Pwsh needs a terminal console and a general UI. Neither replaces the other.
- Console: a native terminal in the shape of Windows Terminal, with a tab strip and one terminal per tab. Its text area is a monospace cell grid, fully VT/ANSI compatible, that reflows on resize.
- UI: for applications that are not terminals, laid out in dp rather than
cells. It is drawn and composited by Pwsh on the owned window surface;
setContentViewoverNativeActivity's content view is not used.
The device is touch-first. Graphical applications and Android services use the host without constructing a console.
Component ownership
| Component | Owns | Dependency boundary |
|---|---|---|
| Managed runtime host | Component lifetime, event routing, runspace admission, startup guard and recovery state | Starts without SMA, terminal state or graphical UI |
| Android bindings | NDK exports, JNI dispatch, window/input/looper operations and fixed Java forwarders | Platform mechanisms; no terminal or application policy |
| Managed retained UI | Layout, focus, hit regions, scene state, damage and tab controls | Client of the host; ordinary coordinates, not cells |
| Managed console | VT/ANSI state, transcript, editor, selection, reflow and terminal draw commands | Pane content; optional to the retained UI and host |
| PowerShell session adapter | Pipeline invocation, host interaction, streams and completion | SMA references stay behind this boundary |
| Application scripts | Application behavior and composition | Use admitted platform capabilities |
Composition: cells only where VT needs them
Only the terminal's text area is a grid of monospace cells, because VT/ANSI semantics (cursor addressing, erase, scroll regions) are defined over one. The tab strip, overlays and everything else are separate layers, sized in dp and composited over or beside the grid.
The preview renders through Android hardware Canvas, reached through owned
JNI bindings, for both retained controls and console panes. Software
rasterization is not a production fallback. A hardware Canvas buffer is not
preserved between frames, so every submission covers the whole surface; damage
decides when a frame is requested, not how much of it is drawn.
The renderer executes a display list. Retained controls record rectangles,
text, icons and drawing-state operations in UI coordinates. Terminal panes
contribute background runs and text runs derived from cells, with one
drawText per merged same-format span. PowerShell decides state changes;
lowered code builds and executes the repeated layout and drawing work.
Event bus
One queue, drained on the main thread. Producers (local input, shell
runspaces, timers, remote input) add an event and signal an eventfd
registered with the main looper. The looper callback drains every queued
event, applies each to state, and produces one frame if anything is damaged.
Bursts coalesce into one frame. Nothing polls and nothing waits on a timeout;
the looper is the wait, because blocking the main thread would stall input
and lifecycle. Animation requests one display-refresh callback at a time while
something moves and stops when it settles.
Frame contract
The engine publishes frames, not pixels. Each display renders at its own size and density.
- Three slots. The engine writes the free slot and commits it by incrementing a sequence number; it never waits for a reader. A consumer takes the newest committed slot when it wakes, so a slow consumer skips frames without stalling the engine or other consumers.
- Header. Each slot starts with a 64-byte header: sequence, columns, rows, cell format version, cursor position and flags. The body is the whole frame: the full cell grid, or later a recorded display list.
- Damage per consumer. Each consumer diffs the slot against the last frame it drew, so a consumer that skipped frames still sees every change. A changed-rows bitmap in the header is a hint, never required for correctness.
- Wake. Commit signals each consumer's wake source: an
eventfdon its looper, or a socket for a remote consumer. For another process the ring lives in shared memory (memfd), passed over a Unix socket and mapped read-only. - Cell format. Fixed and versioned before any consumer depends on it: full Unicode scalar values, a width for double-width cells, 24-bit foreground and background, and attribute bits (bold, italic, underline, inverse, strike).
| Consumer | Sends or draws | Rendered | Client needs |
|---|---|---|---|
| Local display | Canvas calls on the window surface |
on the device | nothing |
| Contract stream | changed rows of the frame | on the client, at its density | a Pwsh client |
| RDP | graphics-pipeline region updates (MS-RDPEGFX) from an encoder surface | on the device | the Windows App client |
| Video | H.264 from the same encoder surface | on the device | any video player |
RDP and a foreground service need manifest declarations fixed at the release
freeze (INTERNET, the service and its types, POST_NOTIFICATIONS). TLS
needs the crypto initialization from the emitted NativeActivity subclass.
Text input
Traced in AOSP frameworks/base at android-14.0.0_r1 (commit 299fe6f5):
NativeActivity's content view is a bareNativeContentView extends View(NativeActivity.java:113-123).ViewreturnsfalsefromonCheckIsTextEditorandnullfromonCreateInputConnection(View.java:16460,:16483). The native side has onlyANativeActivity_showSoftInputandhideSoftInput.- Soft-keyboard text (committed and composing text, deletions) therefore needs
a view that returns an
InputConnection, which means emitted DEX. The emittedNativeActivitysubclass hosts that view, added withaddContentView;NativeContentViewstays the content view. NativeActivitysetsSOFT_INPUT_ADJUST_RESIZE(:136-138), so the IME arrives as a content-rect change, and sets the window format to RGB_565 (:135); the host selects a 32-bit format before drawing color.
Intents
Scripts reach the Android intent system through owned bridges: constructing
any Intent, starting activities (including for a result) and sending
broadcasts. Receiving uses a declared, disabled, frozen manifest:
- The emitted manifest declares, once, nearly every component and
<intent-filter>the product can use, each withandroid:enabled="false"and naming the script stub that handles it. A script enables the components it handles at runtime withPackageManager.setComponentEnabledSetting. Adding a capability means enabling a declared component, not re-emitting the manifest or re-signing the APK. - Activity intents go to
<activity-alias>entries whosetargetActivityis theNativeActivity, so they need no Java class. Receivers and services point at one emitted DEX forwarder per Android base class. - An app may change the enabled state of its own components without
CHANGE_COMPONENT_ENABLED_STATE(PackageManagerService.java:3869-3890); every call passesDONT_KILL_APP. NativeActivitydoes not overrideonNewIntent, andActivity.onNewIntentis empty (Activity.java:2275), so receiving an intent while running needs the emitted subclass to forward it.
Interaction rules
- Draw on damage only: output arrived, input, resize, tab switch. Command completion arrives as an event.
- Android reserves the left, right and bottom edges for system gestures. No action depends on an edge swipe; tabs switch by tap; every action has a single-pointer alternative (WCAG 2.2 2.5.1, 2.5.7). Tap targets are at least 24x24 dp (2.5.8), aiming for 44x44 (2.5.5).
- The tab strip sits at the top, below the status-bar and cutout insets; the bottom belongs to the home gesture and the IME.
- Bridges use NDK C APIs first and JNI only where no C API exists, with contracts read from AOSP source at a pinned tag.
- The VT parser and screen model are built from specifications and checked against independent implementations: ECMA-48, the DEC VT references, xterm's control-sequence documentation, and the Paul Williams parser state machine, with Windows Terminal and conhost as cross-checks.
Applications
A graphical application is a module that declares, with no host setup and no loop:
| Function | Purpose |
|---|---|
Get-AppInfo |
title, icon, default size |
New-AppState |
its data |
Show-App $Context $State $Bounds |
records drawing and hit regions using theme tokens |
Invoke-AppAction $State $Action $Arguments |
updates state and returns what changed |
Themes are .psd1 data (palette, layout constants, fonts, radii). Each
application may run in its own runspace behind the dispatcher, so an
application's exception stays in its window.
Prior art
setup.ps1's interactive interface has the event shape used here: a blocking event bus, input produced in its own runspace, every queued event drained before one render, and output emitted only where the format changes.- The console reference implementation has the console model: a transcript of entries tagged by PowerShell stream, a line editor with selection and IME composition, reflow by relayout at the current width, packed cells, and a renderer that repaints only changed cells. See the console reference specification.
- CellCanvas's timing line (FPS, cell build, upload, submit, total, dropped frames) becomes a diagnostic overlay that is off by default, and its animated fill an explicit benchmark mode.
- An earlier terminal script was reviewed and not used as a base: every binding was a Mono.Android type, it redrew every frame, polled for command completion, and had no VT parser. Its tab model, SMA-tokenizer syntax coloring and line reflow are re-derived here.