geoffjay/nemo · Archived

nemo-component-patterns

Reference for contributing new built-in components to the Nemo source tree (Rust) — the 4-file workflow, NemoComponent derive, RenderOnce vs Render, stateful vs stateless patterns, sizing gotchas, and color resolution.

First seen Jul 15, 2026

Installation

$ npx skills add geoffjay/nemo --skill nemo-component-patterns

Summary

  • Reference for contributing new built-in components to the Nemo source tree (Rust) — the 4-file workflow, NemoComponent derive, RenderOnce vs Render, stateful vs stateless patterns, sizing gotchas, and color resolution.
  • NOT for authoring app.xml configs — use nemo-xml-reference for that.

Stronger alternatives

This repository is archived — consider an actively maintained alternative.

Similar popular skills

Related neighbors and high-traction skills in the same topics — useful to compare before installing.

Also in this package

Other skills from geoffjay/nemo.

npx skills add geoffjay/nemo

Browse all from geoffjay/nemo

More details

Agent compatibility

Declared targets from SKILL.md / docs. Unmarked agents are not listed — the skill may still install via the CLI.

Claude Code Not declared
Cursor Not declared
Codex Not declared
GitHub Copilot Not declared
Windsurf Not declared
Gemini CLI Not declared
Cline Not declared
OpenCode Not declared

Repository health

Stars 1
License LICENSE-APACHE
Default branch main
Open issues 59
Status Archived

Package contents

Files included with this skill beyond the listing page.

  • skill md SKILL.md 5,410 B
  • docs SUMMARY.md 322 B

History

  1. First seen on skills.sh
  2. First recorded snapshot · 5 installs

SKILL.md

Nemo Component Patterns

Use this skill when writing, modifying, or debugging Nemo UI components. It covers the conventions, patterns, and common pitfalls for the component system.

The 4-File Pattern

Every component touches exactly 4 files:

  1. crates/nemo/src/components/<name>.rs — struct + RenderOnce impl
  2. crates/nemo/src/components/mod.rs — mod + pub use
  3. crates/nemo-registry/src/builtins.rs — schema registration
  4. crates/nemo/src/app.rs — render dispatch match arm

Component Struct Convention

use gpui::*;
use nemo_macros::NemoComponent;

#[derive(IntoElement, NemoComponent)]
pub struct MyComponent {
    // Properties extracted from BuiltComponent
    #[property(default = "default_value")]
    my_prop: String,
    #[property]
    optional_prop: Option<i64>,
    #[children]              // Only if accepts children
    children: Vec<AnyElement>,
    // Non-macro fields
    runtime: Option<Arc<NemoRuntime>>,  // Only if has event handlers
    entity_id: Option<EntityId>,         // Only if has event handlers
    #[source]                // MUST be last field
    source: nemo_layout::BuiltComponent,
}

Key Rules

RenderOnce, Not Render

  • All Nemo components implement RenderOnce (stateless, consumed on render)
  • Render is for GPUI views backed by Entity<T> — NOT for Nemo components
  • RenderOnce signature: fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement

#[source] Must Be Last

The macro generates code that consumes the component parameter. Property extractions borrow it first, so #[source] assignment must come after all #[property] fields.

Supported Property Types

  • String, i64, f64, bool
  • Option<String>, Option<i64>, Option<f64>, Option<bool>
  • Use Option<T> for optional properties, bare T with #[property(default = ...)] for required

Builder Methods for Runtime/EntityId

Components with event handlers need builder methods (the macro doesn't generate these):

impl MyComponent {
    pub fn runtime(mut self, runtime: Arc<NemoRuntime>) -> Self {
        self.runtime = Some(runtime);
        self
    }
    pub fn entity_id(mut self, entity_id: EntityId) -> Self {
        self.entity_id = Some(entity_id);
        self
    }
}

Event Handler Pattern

// In RenderOnce::render():
let click_handler = self.source.handlers.get("click").cloned();
let component_id = self.source.id.clone();
if let Some(handler) = click_handler {
    if let (Some(runtime), Some(entity_id)) = (self.runtime, self.entity_id) {
        element = element.on_click(move |_event, _window, cx| {
            runtime.call_handler(&handler, &component_id, "click");
            cx.notify(entity_id);
        });
    }
}

Color Resolution

Use resolve_color() from components/mod.rs:

  • Theme colors: "theme.border", "theme.accent", "theme.danger", etc.
  • Hex colors: "#4c566a", "FF0000", "#FF000080" (with alpha)

Shadow and Rounded Presets

  • apply_shadow(div, Some("md")) — sm, md, lg, xl, 2xl
  • apply_rounded(div, Some("lg")) — sm, md, lg, xl, full

Stateful Component Pattern

For widgets that need state persistence across re-renders (Table, Tree, Input, Slider):

  1. Add a variant to ComponentState enum in components/state.rs
  2. Add a getorcreate*state() method in app.rs
  3. Store Entity<T> in ComponentStates
  4. Compare data to detect changes and update state

Height Gotcha for List Widgets

Table and Tree use uniformlist — they collapse to 0px height without a parent that has definite height. Wrap in div().wfull().h(px(height)) (default 300px).

Registry Schema Pattern

reg(
    registry,
    "my_component",                    // snake_case name (XML uses kebab-case)
    ComponentCategory::Display,        // Category
    "My Component",                    // Display name
    "Description of the component",    // Description
    ConfigSchema::new("my_component")
        .property("label", PropertySchema::string())
        .property("size", PropertySchema::string().with_default("md"))
        .property("count", PropertySchema::integer())
        .property("enabled", PropertySchema::boolean().with_default(true))
        .require("label"),             // Required properties
);

Render Dispatch Patterns

In app.rs render_component():

// Simple (no state, no children, no handlers)
"my_component" => MyComponent::new(component.clone()).into_any_element(),

// With children
"container" => {
    let children = self.render_children(component, components, entity_id, window, cx);
    Container::new(component.clone()).children(children).into_any_element()
}

// With event handlers
"button" => Button::new(component.clone())
    .runtime(Arc::clone(&self.runtime))
    .entity_id(entity_id)
    .into_any_element(),

// Stateful
"table" => {
    let state = self.get_or_create_table_state(component, window, cx);
    Table::new(component.clone()).table_state(state).into_any_element()
}