Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 45 additions & 1 deletion crates/nemo/src/commands/screenshot.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ use anyhow::{bail, Context as _, Result};

use crate::args::ScreenshotArgs;
use crate::config::NemoConfig;
use crate::project::ActiveProject;
use crate::workspace::WorkspaceArgs;
use crate::{build_app_window, theme, BootstrapParams};

Expand Down Expand Up @@ -75,10 +76,53 @@ pub fn run(args: ScreenshotArgs) -> Result<()> {
let _ = window.update(cx, |_, w, _| w.refresh());
}

// `render_to_image` reads the window's *last drawn* frame, and
// data-source values arrive asynchronously and reach the UI only once
// their bindings are propagated into the layout. In the interactive run
// loop `App`'s data task applies those on every `data_notify` and marks
// the view dirty; the one-shot capture path must drive the same loop
// itself, or bound values stay at their placeholders in the PNG
// (issue #82). Grab the active project's runtime to pump it.
let runtime = cx
.has_global::<ActiveProject>()
.then(|| cx.global::<ActiveProject>().runtime.clone());

let out = out.clone();
let capture_err_task = capture_err_run.clone();
cx.spawn(async move |cx| {
cx.background_executor().timer(settle).await;
// Pump data → binding propagation across the settle window in small
// slices rather than sleeping it all at once. Each `timer` await
// yields to the executor, giving the async data sources time to
// deliver and letting any refresh-requested redraw actually land.
let tick = Duration::from_millis(50);
let mut remaining = settle;
loop {
let slice = remaining.min(tick);
cx.background_executor().timer(slice).await;
remaining = remaining.saturating_sub(slice);

if let Some(rt) = runtime.as_ref() {
let navigated = rt.apply_pending_navigations();
let updated = rt.apply_pending_data_updates();
if navigated || updated {
let _ = cx.update(|cx| {
let _ = window.update(cx, |_, w, _| w.refresh());
});
}
}

if remaining.is_zero() {
break;
}
}

// Ensure the last-applied state is committed to the drawn frame
// before we read it back: refresh, then yield once more so gpui
// draws the dirty window into `rendered_frame`.
let _ = cx.update(|cx| {
let _ = window.update(cx, |_, w, _| w.refresh());
});
cx.background_executor().timer(tick).await;

let result: Result<()> = cx.update(|cx| {
let img = window
Expand Down
15 changes: 11 additions & 4 deletions crates/nemo/src/runtime.rs
Original file line number Diff line number Diff line change
Expand Up @@ -763,7 +763,17 @@ impl NemoRuntime {
}
}

// Start all registered sources
// Subscribe the update loops *before* starting sources. A source's
// `start()` broadcasts its initial `full` value immediately, and a
// tokio broadcast channel drops messages sent while no receiver is
// attached — so subscribing after `start_all()` loses the first
// value, leaving the repository unseeded until some later event
// (e.g. a file-watcher change) re-delivers it. Interactive runs
// usually get such a follow-up; a one-shot `nemo screenshot` does
// not, so bound values render as placeholders (issue #82).
self.start_data_update_loop().await;

// Start all registered sources (now that receivers are attached).
let results = self.data_engine.start_all().await;
for (id, result) in &results {
match result {
Expand All @@ -772,9 +782,6 @@ impl NemoRuntime {
}
}

// Start the data update loop for each source
self.start_data_update_loop().await;

Ok::<(), anyhow::Error>(())
})?;

Expand Down
10 changes: 10 additions & 0 deletions docs/knowledgebase/concepts/data-flow.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,16 @@ created via `nemo_data::create_source()`, registered and started by the
`DataFlowEngine`, then consumed by tokio tasks the runtime spawns
(`crates/nemo/src/runtime.rs`, source setup ~520, update loop ~594).

**Startup ordering is load-bearing.** A source's `start()` broadcasts its
initial `full` value *immediately*, and a tokio `broadcast` channel drops
messages sent while no receiver is attached. So the runtime subscribes the
update loops (`start_data_update_loop()`) **before** `data_engine.start_all()`;
subscribing afterward loses each source's first value, leaving the repository
unseeded until some later event (e.g. a file-watcher change) re-delivers it.
Interactive runs usually get such a follow-up and appear to recover, but a
one-shot `nemo screenshot` does not — that was the root cause of issue #82
(bound values rendered as placeholders in captures).

# Transforms

The `Transform` trait (`crates/nemo-data/src/transform.rs:31`) and a `Pipeline`
Expand Down
1 change: 1 addition & 0 deletions docs/knowledgebase/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@ to touch the KB on every edit.
* [Roadmap](plans/roadmap.md) - current capabilities, phase-2 status, remaining roadmap items, and pointers to full planning docs.
* [Declarative children over JSON-string properties](plans/declarative-children-migration.md) - migrate collection components from JSON-string attributes to nested child elements, piloted on accordion.
* [Headless renderer and screenshots](plans/headless-screenshots.md) - `nemo screenshot` implemented on macOS via gpui's offscreen `Window::render_to_image`; Linux capture remains open.
* [Screenshot as a plugin](plans/screenshot-as-plugin.md) - move `nemo screenshot` off the feature-gated host build onto a stock release binary via an OS-native capture plugin; needs `Capability::Command` (plugin-contributed CLI subcommands) + a host `capture_app_window` bootstrap primitive. **Planned.**
* [Devtools inspector](plans/devtools-inspector.md) - what a nemo-devtools crate would take; the introspection surfaces already exist, in-process panel recommended over an external client.
* [Design tokens and active redesign](plans/design-tokens.md) - centralized spacing/radius/typography/semantic-color tokens (gpui-free `nemo-tokens` crate); full chrome migration with screenshot verification.
* [Design-system export](plans/design-system-export.md) - `cargo xtask design-export` emits tokens + themes + component structure as a pencil.dev-friendly JSON intermediate.
Expand Down
2 changes: 2 additions & 0 deletions docs/knowledgebase/log.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
# Knowledge Base Update Log

## 2026-07-31
* **Plan**: Added [Screenshot as a plugin](/docs/knowledgebase/plans/screenshot-as-plugin.md) — a design to make `nemo screenshot` deployable on a **stock release binary** by moving it out of the feature-gated host build into an **OS-native capture plugin**. Grounded the blocker in code: capture today calls gpui's `Window::render_to_image()` (`commands/screenshot.rs:85`), gated `#[cfg(feature="test-support")]` inside gpui and kept out of release by decision; a native plugin only ever receives `PluginContext` (`nemo-plugin-api/src/lib.rs:285`) — no `&mut Window`, and it can't flip a host compile-time feature — so a plugin **must capture at the OS level** (ScreenCaptureKit / `CGWindowListCreateImage`), not through gpui. Two new host primitives designed: (1) **`Capability::Command`** + `CommandSpec`/`register_command`/`PluginCommandFn` so plugins contribute CLI subcommands — requires a two-phase parse ahead of the static clap match (`main.rs:66`, `args.rs`) with **built-ins taking precedence**; (2) a host **`capture_app_window`** `PluginContext` method (default `Unsupported`, matching the `navigate` pattern at `:327`) that owns the gpui run loop + `build_app_window`/`BootstrapParams` (`main.rs:246/265`) and returns a `CapturedFrame` of raw RGBA — recommended split is host-captures/plugin-encodes, which lets the OS-capture backend ship in the **default binary** (no `test-support`) while the plugin supplies CLI surface + PNG/permission policy. Open questions logged: TCC permission UX for CI, discovery cost, native ABI versioning (no check today, `:538`), WASM parity. Registered in both plan indexes. Planning only — nothing implemented.
* **Fix**: Wired `<menu-item on-click>` inside `<dropdown-button>` ([issue #83](https://github.com/geoffjay/nemo/issues/83)). The build path in `app.rs` collected only each menu item's `label` into a `Vec<String>`, so `DropdownButton` rendered click-less `PopupMenuItem`s and every entry was a silent no-op. Now `app.rs` collects a `Vec<MenuItem { id, label, on_click }>` (new struct in `components/dropdown_button.rs`, re-exported from `components/mod.rs`), carrying each item's `handlers.get("click")`, and passes `runtime` + `entity_id` into `DropdownButton` (same as `<button>`). `DropdownButton::render` attaches `PopupMenuItem::on_click` that calls `runtime.call_handler(&handler, &item_id, "click")` + `cx.notify(entity_id)`, mirroring the button path. No registry change — `on-*` handler attributes are generic (already passed `nemo validate`). Updated the component doc-comment to show `on-click`.

## 2026-07-30
Expand Down Expand Up @@ -92,4 +93,5 @@
* **Plan**: Authored [page router](/docs/knowledgebase/plans/page-router.md) — a design for a general, chrome-free `<router>`/`<route>` primitive to replace the fragile visibility-toggle page-switching pattern (per-page `<panel visible>` + hand-maintained hide-all Rhai handlers; `examples/components` hard-codes 34 hide-calls). Chosen shape: URL-style path routes with `:param` capture + `*` fallback, declarative `<nav-link>`s, a Rhai `navigate()`/`back()`/`forward()` API, host-side per-router state (`RouterRegistry` on `NemoRuntime`, keyed by id) projected into the `DataRepository` at `data.route.<id>.*` for Rhai/binding reads, `on-enter`/`on-leave` lifecycle hooks, and nested routers. Modeled on the existing `app_shell` render pattern (`containers/app_shell.rs`, `app.rs:1099`) but decoupled from its chrome; `app-shell` left unchanged. **Key design constraint captured:** `call_handler` holds the `extension_manager` write lock (`runtime.rs:508`), so navigation must be **deferred** through a `NavIntent` queue drained by the `App` poll loop (`apply_pending_navigations()` beside `apply_pending_data_updates`) to avoid a re-entrant deadlock when `navigate()` fires hooks from inside a handler. No code written yet; registered in the plans indexes.

## 2026-07-31
* **Fix**: Issue #82 — `nemo screenshot` rendered the initial layout but bound values (`<binding>`/`bind-*`) stayed at their placeholders in the PNG even though they populate in the running app. **Root cause:** startup ordering. A data source's `start()` broadcasts its initial `full` value immediately, and a tokio `broadcast` channel drops messages sent while no receiver is attached; the runtime subscribed the update loops *after* `data_engine.start_all()`, so each source's first value was lost and the repository was never seeded. Interactive runs recover via a later event (e.g. a file-watcher change); a one-shot capture never does. **Fix (two parts):** (1) `runtime.rs` `initialize()` now calls `start_data_update_loop().await` **before** `data_engine.start_all().await`, so receivers are attached before sources broadcast — the actual root-cause fix, benefits every one-shot/early-read path, not just screenshots. (2) `commands/screenshot.rs` capture task now *pumps* the data→binding loop across the `--settle-ms` window in 50ms slices (grabbing `ActiveProject`'s runtime, calling `apply_pending_navigations()`/`apply_pending_data_updates()` and `window.refresh()` when anything changed), then does a final refresh + one executor-yield timer before `render_to_image()` — because `render_to_image` reads the window's *last drawn* frame and `refresh()` only sets a dirty flag; the interactive `App` data task drives this on every `data_notify`, so the one-shot path must drive it itself. Verified: `examples/data-binding` screenshot now shows live timer/HTTP values instead of placeholders; 233 nemo + 18 macro tests green; `cargo build -p nemo --features screenshot` clean. Updated [data flow](/docs/knowledgebase/concepts/data-flow.md) (new startup-ordering note under Data sources).
* **Feature**: Opt-in **header-bar dropdown menu**. Apps declare `<menu-item label icon on-click separator>` children under `<header-bar>`; when present, a far-left hamburger icon opens a native dropdown, each entry invoking a Rhai handler via `runtime.call_handler(handler, "header-bar", "click")`. Parser: added `process_header_bar` (`crates/nemo-config/src/xml_parser.rs`) so repeated `<menu-item>` children survive as a `menu_items` array at `app.window.header_bar.menu_items` (the generic `process_nested_block` would collapse them; note `__type__` is already snake-cased, so the special-case matches `header_bar`/`menu_item`). Render: `HeaderBar` (`crates/nemo/src/workspace/header_bar.rs`) gained `menu_items`/`runtime` fields + a `menu_items_from_config` helper, building `DropdownButton` + `PopupMenuItem` (reusing `components::icon::map_icon_name` for item icons); both construction sites (`main.rs`, `workspace/mod.rs::create_header_bar`) pass them. Declared `menu-item` in the schema surface (`crates/nemo-registry/src/schema_surface.rs`). Demoed in `examples/basic`. Verified: new parser test `test_parse_header_bar_menu_items` + `cargo build -p nemo` clean. Updated [configuration](/docs/knowledgebase/concepts/configuration.md) and the nemo-xml-reference skill.
1 change: 1 addition & 0 deletions docs/knowledgebase/plans/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ Forward-looking plans for the project.
* [Roadmap](roadmap.md) - current capabilities, phase-2 status, remaining roadmap items, and pointers to full planning docs.
* [Declarative children over JSON-string properties](declarative-children-migration.md) - migrate collection components from JSON-string attributes to nested child elements, piloted on accordion.
* [Headless renderer and screenshots](headless-screenshots.md) - implemented on macOS via gpui's offscreen `Window::render_to_image` (`nemo screenshot`); Linux capture remains open.
* [Screenshot as a plugin](screenshot-as-plugin.md) - make `nemo screenshot` distributable on a stock release binary by moving it into an OS-native capture plugin; requires two new host primitives (`Capability::Command` for plugin-contributed CLI subcommands + a host `capture_app_window` bootstrap primitive). **Planned / not implemented.**
* [Devtools inspector](devtools-inspector.md) - what a nemo-devtools crate would take; the introspection surfaces already exist, in-process panel recommended over an external client.
* [Design tokens and active redesign](design-tokens.md) - centralized spacing/radius/typography/semantic-color tokens (gpui-free `nemo-tokens` crate); full chrome migration with screenshot verification.
* [Design-system export](design-system-export.md) - `cargo xtask design-export` emits tokens + themes + component structure as a pencil.dev-friendly JSON intermediate.
Expand Down
Loading
Loading