windowhost

package
v0.0.21 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Aug 21, 2026 License: MIT Imports: 27 Imported by: 0

Documentation

Overview

Package windowhost is the M3 multi-app host. Each open app renders as a top-level c.Window (egui::Window — movable, resizable, titled), so "click app → see app on the desktop" is the natural interaction model. Apps are opened via Open(appId), invoking the registered AppCtor for a fresh AppI; closed via Close(windowKey) or the in-body "× Close" button. Each window mounts on first Frame and unmounts when removed.

Window identity: every Open call allocates a fresh uint64 key. Two windows can co-exist for the same AppId — singleton-registered apps share their AppI between windows (state is shared); factory- registered apps yield independent AppIs per window, so isolation depends entirely on the app's internal structure.

Layout: egui's built-in Memory persists each window's position, size, and collapsed state across frames, keyed by the window's widget id (derived from `ids.PrepareStr("window-<key>")`, stable for the window's lifetime). Cross-run persistence is whatever egui::Memory natively offers.

History note: the M3 design originally chose egui_dock tabs (this package was named `dockhost` through 2026-05-12). The dock model's "one active tab per leaf" semantics made "click app, see nothing change" the dominant first-time experience even after the PanelCentral fix (517fc46b) — the wrong interaction model for the multi-app runtime use case. The design was reverted to per-app egui::Window. The Manifest's Title / Icon / SurfaceHints fields fed directly into c.Window's chrome anyway.

Capabilities-as-host: the WindowHost is *the* interactive entry point. In screenshot-tour mode (IMZERO2_SCREENSHOT_DIR set) the carousel bypasses WindowHost and runs a single AppI's Frame directly via the pre-existing adaptToRenderer path, preserving tour driver behaviour.

Index

Constants

View Source
const OpenServiceAppId app.AppIdT = "runtime.windowhost"

OpenServiceAppId is the synthetic AppId the open service registers under on the bus. Apps inspecting Msg.Sender on launch replies see this string.

View Source
const OpenSubject = "windowhost.open"

OpenSubject is the audited request/reply subject apps use to open another app's window (ADR-0135 §SD1). Callers declare it in their manifest Caps (Pub direction) and send a launchrequest.LaunchRequest; the reply is a launchreply.LaunchReply. Refusals are replies with a Reason, never silent drops.

View Source
const WorkingsetCallerAppId app.AppIdT = "runtime.workingset"

WorkingsetCallerAppId is the synthetic caller a restored open is attributed to in the launch facts (ADR-0148 §SD6), so "which windows opened from restored state" is one predicate on the same column that answers "which app asked for this window". Nothing registers under it — the restore has no requesting app; the host is acting on the user's plain open. Mirrors OpenServiceAppId's shape.

View Source
const WorkingsetDefaultName = "default"

WorkingsetDefaultName is the single workingset name v1 wires (ADR-0148 §SD3). The store, the row, and the host paths carry a name from day one so a named-set UX needs no migration; nothing mints another value yet.

Variables

View Source
var DebugRender = env.NewString(env.Spec{
	Name:        "WINDOWHOST_DEBUG_RENDER",
	Description: "non-empty enables per-window-body render logging in the windowhost",
	Category:    env.CategoryDev,
})

DebugRender, when set to a non-empty value, logs every window-body invocation so we can confirm which windows the user actually saw painted. Off by default; enable via WINDOWHOST_DEBUG_RENDER=1 for an investigative session.

View Source
var PackageProps = packageprops.Props{
	WASMWASI:         packageprops.WASMBlocked,
	WASMJS:           packageprops.WASMBlocked,
	WASMFreestanding: packageprops.WASMBlocked,
}

PackageProps records this package's curated properties (ADR-0080). Seeded by `boxer code analysis golang wasmsurvey props generate`; curate by hand. The same group's `props verify` reconciles it.

Functions

func RequestOpen added in v0.0.15

func RequestOpen(bus app.BusI, targetAppId app.AppIdT, configKind string, config []byte) (windowKey uint64, err error)

RequestOpen asks the window host to open targetAppId's window over the audited OpenSubject (ADR-0135 §SD1), optionally delivering a launch config: configKind is the vocabulary kind the config bytes claim and config is its facts-CBOR (encode with the config's generated codec, e.g. buscodec.Encode(launchcfg.PlayLaunch{…})); pass "" and nil for a plain open. It is the client half of OpenService — the mirror of adhocdata.PublishRequest — so an app drives windowhost.open without re-deriving the request/reply codec dance.

The caller's bus client needs Pub on OpenSubject; the caller identity is attributed by the bus (Msg.Sender), not the payload. RequestOpen blocks on the bus round-trip, so call it off the frame loop. A refusal reply (unknown app, kind mismatch, oversize, malformed envelope) returns an error carrying the host's reason; on success it returns the opened window's key.

Types

type Inst

type Inst struct {
	// contains filtered or unexported fields
}

Inst is the window host: the registry plus the list of open windows. The zero value is unusable; construct via NewInst.

Goroutine safety: split. Open / OpenWithConfig / Close / CloseAll and the metadata snapshots guard every touch of host state with inst.mu and may be called off the render thread — the open service (ADR-0135) invokes OpenWithConfig from bus-handler goroutines, and a window opened that way is picked up by the next Frame. App factory ctors run on the calling goroutine and must not touch render state (the lifecycle contract already reserves rendering for Frame). The render surfaces themselves (Frame, RenderAppsMenu, the picker/search state) remain single-threaded: render-loop only.

func NewInst

func NewInst(registry *app.Registry, logger zerolog.Logger) (inst *Inst)

NewInst constructs a WindowHost backed by registry. logger is used for per-window mount/frame errors; per-app loggers (with app_id pre- tagged) are derived from it at Open time.

Audit-trail wiring (optional but recommended for production use): call SetAudit(runId, facts) after construction; once set, Open and reapClosed emit app-lifecycle rows so the persistence layer carries a per-window open/close trail correlated with the runtime-start row that runId points at.

func (*Inst) Close

func (inst *Inst) Close(windowKey WindowKeyT, reason string)

Close requests removal of the window with the given key, attaching an optional reason that lands in the "stopped" app-lifecycle row. The actual Unmount + slice removal happens at the end of the current frame (Frame()) so we don't pull state out from under an in-flight body. Closing an unknown key is a no-op.

func (*Inst) CloseAll

func (inst *Inst) CloseAll(reason string)

CloseAll marks every open window for reaping with the supplied StopReason. Used by the carousel on shutdown to leave a clean audit trail — without it, windows still mounted at process exit would have "started" rows in the facts table but no matching "stopped" rows. Call Frame once after CloseAll to drive reap; or call ReapAll for out-of-render-loop teardown.

func (*Inst) Frame

func (inst *Inst) Frame(ids *c.WidgetIdStack) (err error)

Frame renders every open window as a top-level c.Window (egui::Window — floating, movable, resizable). Each window's body is a small × Close header followed by the app's Frame call. Mount runs lazily on the first pass per window; sticky mountErr displays an error label and skips Frame so the host stays responsive.

ids must be a stable WidgetIdStack supplied by the caller (usually the carousel renderer's bodyIds). Per-window egui Memory (position, size, collapsed flag) is keyed by the window's widget id, which is derived from `ids.PrepareStr("window-<key>")` — stable for the window's lifetime because window keys are monotonic and never reused.

When zero windows are open, an empty-state pane is rendered instead. The empty-state pane lists every registered app with an "open" button per app and runs inside a c.PanelCentral so the user can at least see something on the desktop after launch.

func (*Inst) Len

func (inst *Inst) Len() (n int)

Len returns the number of open windows.

func (*Inst) Open

func (inst *Inst) Open(appId app.AppIdT) (key WindowKeyT, err error)

Open allocates a new window for the given AppId. Returns the fresh key on success; an error if the registry doesn't know the Id or the ctor fails. The window is mounted lazily on first Frame; if Mount fails the window stays open with an error label so the user can Close it.

For an app that declares Manifest.Workingset this is also the restore path: OpenWithConfig looks up the app's stored record and, when one is usable, opens the window carrying it (ADR-0148 §SD5).

func (*Inst) OpenOrRaise added in v0.0.20

func (inst *Inst) OpenOrRaise(appId app.AppIdT) (key WindowKeyT, opened bool, err error)

OpenOrRaise opens appId — unless a window over that app is already open, in which case that window is queued to be raised instead of a second one stacking. This is the affordance a recurring global shortcut wants (F1 → help): the first press opens, every further press brings the same window back to the front.

The raise runs on the next Frame, and egui's stacking then reports the window topmost — so it also becomes the shell's active window (app.WindowFocusI), exactly as a fresh open would be: both halves end with the window on top and focused. The oldest window wins when several show the app. opened reports which half ran.

func (*Inst) OpenWindows

func (inst *Inst) OpenWindows() (keys []WindowKeyT)

OpenWindows returns the keys of currently open windows in declaration order (== the order in which they were Open()'d). Primarily a test helper.

func (*Inst) OpenWithConfig added in v0.0.15

func (inst *Inst) OpenWithConfig(appId app.AppIdT, kind string, cfg []byte) (key WindowKeyT, err error)

OpenWithConfig allocates a new window for appId carrying a launch config (ADR-0135): kind names the config's vocabulary kind and cfg is the config DTO's facts-CBOR bytes, delivered untouched to the app via MountContextI.LaunchConfig at Mount (§SD4). Empty kind + nil cfg is a plain open, exactly Open's behaviour.

Boundary validation, in order: target manifest exists → the manifest's LaunchKind accepts kind (an argument-carrying open of an app with an empty LaunchKind is refused, §SD3) → size cap → the bytes decode as the claimed kind (kindcheck). Refusals are returned as named errors — the bus-facing open service turns them into LaunchReply refusals, never silent drops (§SD1).

A plain open of a workingset participant (ADR-0148 §SD5) is where the host supplies a config of its own: the stored record for the app is looked up and, when it survives the same boundary rules, the open proceeds as a config-carrying one with LaunchReasonRestore. Restore therefore has no second delivery channel — the app decodes a launch config in Mount either way, and reads MountContextI.LaunchReason to tell the two tiers apart.

func (*Inst) ReapAll

func (inst *Inst) ReapAll(reason string)

ReapAll runs Unmount and writes "stopped" lifecycle rows for every currently-open window, then empties the slice. Unlike reapClosed (which fires after the render pass), this is the shutdown path — call it from a defer in the carousel main after the render loop has exited so closing-window audit rows still get written.

func (*Inst) RenderAppsMenu

func (inst *Inst) RenderAppsMenu(ids *c.WidgetIdStack)

RenderAppsMenu draws an "Apps ▾" menu listing every registered app, grouped into per-topic submenus in app.AllTopics order (ADR-0158 §SD3; an app carrying two topics appears under both). Clicking an entry calls Open(id) for that app; the new window appears on the next frame. Entries within a topic sort by Display.

ids is the caller's stack; the menu uses derived ids for the per-entry buttons. Place inside a MenuBar (typically the carousel's top PanelTop), alongside File / Layout menus.

The menu deliberately has no in-bar search field. egui's menu_button closes on any click outside a menu Button (TextEdit focus clicks included), and lifting the field into the menu bar added chrome clutter for a rarely-used affordance. Search lives in the empty-state pane instead (see renderEmptyState), backed by the same inst.searchText buffer so future surfaces hook into the same filter state.

The kind toggles are a different matter and do appear here, as a "Show" submenu of plain Buttons rather than the pane's checkboxes — same constraint, different resolution. The menu is the *only* launcher surface once a window is open, so a filter reachable solely from the pane could not be undone without closing everything; and a Button is exactly the widget the menu tolerates. The cost is that the menu closes on the click, so changing two toggles means opening it twice. That is acceptable for a mode switch, and it is why the label reports state ("✔ Demos") rather than relying on the user remembering it.

func (*Inst) SetAudit

func (inst *Inst) SetAudit(runId string, facts factsstore.FactsStoreI)

SetAudit attaches a runId + FactsStoreI to the window host. Once set, every Open emits an "app-lifecycle started" row and every reapClosed emits a "stopped" row carrying the supplied StopReason. Both writes are best-effort; a failure to persist is logged at warn level but never bubbles up to the caller.

Calling SetAudit after windows have been opened is supported but won't retroactively emit started rows for windows that are already open — audit is forward-only from the point of attachment.

func (*Inst) SetBus

func (inst *Inst) SetBus(provider app.BusProvider)

SetBus attaches a bus provider to the window host. Once set, each Open mints a per-app BusI client (gated on the app's Manifest.Caps) and threads it through MountCtx.Bus() so apps can publish/subscribe/request (ADR-0026 §SD3/§SD5). The provider chooses the transport: inprocbus.Inst co-located, natsbus.Provider in a NATS deployment (§SD4) — apps never see it. Passing nil clears the wiring (subsequent Opens hand out NoopBus). Calling SetBus after windows have been opened only affects subsequent Opens — already- mounted windows keep the bus they were given.

func (*Inst) WindowInfos added in v0.0.9

func (inst *Inst) WindowInfos() (out []WindowInfo)

WindowInfos returns a metadata snapshot of the currently open windows in declaration order.

Every field it reads is either immutable for the window's lifetime (the manifest, the key, the mount context's delivered config and reason — both set at Open before the window is published) or mutated only under inst.mu (stop reason, the shared-instance refcount), so the snapshot is safe to take off the render thread — which the introspection provider does, serving a query from an HTTP handler. Deliberately absent for that reason: the lazy Mount flags, which the render thread writes without the lock.

type OpenService added in v0.0.15

type OpenService struct {
	// contains filtered or unexported fields
}

OpenService subscribes OpenSubject and forwards decoded requests to the host's OpenWithConfig, replying with the window key or the named refusal. On each accepted open it persists the request as a factsstore.LaunchRow beside the app-lifecycle "started" row the Open itself emitted (§SD6) — best-effort, like every audit write.

Handlers run synchronously on the requester's goroutine (inprocbus dispatch), so the service leans on OpenWithConfig being safe off the render thread; the opened window is picked up by the next Frame.

func NewOpenService added in v0.0.15

func NewOpenService(bus *inprocbus.Inst, host *Inst, log zerolog.Logger) (svc *OpenService, err error)

NewOpenService constructs the service bound to bus and host and immediately subscribes OpenSubject. Callers keep it alive for the bus's lifetime and invoke Close to release the subscription.

func (*OpenService) Close added in v0.0.15

func (inst *OpenService) Close()

Close unsubscribes the service from the open subject. Safe to call once; subsequent calls are no-ops.

type WindowInfo added in v0.0.9

type WindowInfo struct {
	Key     WindowKeyT
	AppId   app.AppIdT
	Display string
	Title   string
	Surface app.SurfaceE
	// Topics is the app's subject classification (ADR-0158 §SD2), copied
	// from the manifest so a window row is readable without joining the
	// app table. Multi-valued: a window has no single category.
	Topics []app.TopicT
	// Kind is the app's provenance (ADR-0158 §SD5) — a filter dimension,
	// not a section.
	Kind       app.KindE
	StopReason string

	// LaunchReason says where this window's content came from: nobody
	// delivered a config, a caller did, or the host restored the app's
	// stored workingset (ADR-0148 §SD5). The distinction is invisible in
	// the window itself, and it is what "which of my windows came back
	// from stored state" asks about.
	LaunchReason app.LaunchReasonE
	// ConfigKind is the vocabulary kind of the delivered launch config,
	// empty for a plain open. It is the manifest's LaunchKind — the host
	// refuses any other at the boundary — repeated here so a row is
	// readable without joining the app table.
	ConfigKind string
	// ConfigBytes is the delivered config's size, 0 for a plain open. The
	// bytes themselves stay inside the window: they are the app's own DTO,
	// may carry a user's query text, and the audit trail already records
	// them where that is intended (ADR-0135 §SD6).
	ConfigBytes int
	// SharesInstance reports that another open window points at the same
	// AppI instance — only possible for a singleton-registered app shown
	// more than once. Load-bearing rather than trivia: such a window can
	// neither be handed a config nor have its workingset saved, because
	// the state is not this window's alone.
	SharesInstance bool
}

WindowInfo is a public, read-only snapshot of one open window's metadata, returned by WindowInfos for runtime introspection (ADR-0094 §SD8) without exposing the private window state.

type WindowKeyT

type WindowKeyT uint64

WindowKeyT identifies one open window. Stable for the lifetime of the window; never reused. Encoded as a uint64 because egui's per-widget Memory state keys are u64-hashes — keeping the key itself a uint64 means the widget id derived for the window scope is stable across frames, so position/size/collapsed state persists for as long as the window stays open.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL