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
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions crates/nemo-config/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -16,3 +16,4 @@ regex = "1"

[dev-dependencies]
proptest = "1"
tempfile = "3"
62 changes: 61 additions & 1 deletion crates/nemo-config/src/xml_parser.rs
Original file line number Diff line number Diff line change
Expand Up @@ -460,7 +460,11 @@ impl XmlParser {
obj: &IndexMap<String, Value>,
result: &mut IndexMap<String, Value>,
) -> Result<(), ParseError> {
let src = match obj.get("src").and_then(|v| v.as_str()) {
let src = match obj
.get("src")
.or_else(|| obj.get("href"))
.and_then(|v| v.as_str())
{
Some(s) => s,
None => return Ok(()),
};
Expand Down Expand Up @@ -1517,4 +1521,60 @@ mod tests {
assert!(template.get("nav_item").is_some());
assert!(template.get("content_page").is_some());
}

#[test]
fn test_include_href_attribute() {
let dir = tempfile::tempdir().unwrap();

// Create an included file with templates
let templates_dir = dir.path().join("templates");
std::fs::create_dir_all(&templates_dir).unwrap();
std::fs::write(
templates_dir.join("buttons.xml"),
r#"<nemo>
<templates>
<template name="primary_btn">
<button variant="primary" size="md" />
</template>
</templates>
</nemo>"#,
)
.unwrap();

// Main file uses href instead of src
let main_xml = r#"<nemo>
<include href="templates/buttons.xml" />

<layout type="stack">
<button id="my_btn" template="primary_btn" label="Click" />
</layout>
</nemo>"#;

let parser = XmlParser::new()
.with_source_name("test".to_string())
.with_base_dir(dir.path());
let value = parser.parse(main_xml).unwrap();

// Verify the included templates were merged
let templates = value.get("templates").unwrap();
let template = templates.get("template").unwrap();
assert!(template.get("primary_btn").is_some());
}

#[test]
fn test_parse_example_complete() {
let value = parse_example("complete");
assert!(value.get("app").is_some());
assert!(value.get("templates").is_some());
assert!(value.get("scripts").is_some());
assert!(value.get("layout").is_some());
assert!(value.get("data").is_some());

// Verify templates from included files were merged
let templates = value.get("templates").unwrap();
let template = templates.get("template").unwrap();
assert!(template.get("nav_item").is_some());
assert!(template.get("status_card").is_some());
assert!(template.get("metric_display").is_some());
}
}
4 changes: 4 additions & 0 deletions crates/nemo/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -212,7 +212,11 @@ fn main() -> Result<()> {

// Navigate to the initial route after window creation
let route = ws.read(cx).current_route.clone();
let needs_refresh = route != "/";
use_navigate(cx)(route.into());
if needs_refresh {
window.refresh();
}

*workspace_entity.borrow_mut() = Some(ws.clone());
cx.new(|_cx| Root::new(ws, window, _cx))
Expand Down
2 changes: 1 addition & 1 deletion crates/nemo/src/runtime.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1467,7 +1467,7 @@ fn parse_layout_node_as_root(layout: &Value, layout_type: &LayoutType) -> Option
LayoutType::Tiles => "tiles",
};

let mut root = LayoutNode::new(root_type).with_id("root");
let mut root = LayoutNode::new(root_type).with_id("__layout_root__");

// Parse component children from the layout object
if let Some(layout_obj) = layout.as_object() {
Expand Down
146 changes: 146 additions & 0 deletions examples/complete/app.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- Complete Example -->
<!-- Demonstrates multi-file configuration with includes, templates, -->
<!-- data binding, RHAI scripts, and the demo-settings plugin. -->
<nemo>
<variable name="app_title" type="string" default="Nemo Complete Example" />
<variable name="refresh_interval" type="int" default="5" />

<app title="${var.app_title}" plugins='["demo-settings"]'>
<window title="${var.app_title}" width="1000" height="700">
<header-bar github-url="https://github.com/geoffjay/nemo/tree/main/examples/complete"
theme-toggle="true" />
<footer-bar enabled="false" />
</window>
<theme name="kanagawa" mode="dark" />
</app>

<script src="./scripts" />

<!-- Multi-file includes: templates and data sources from separate files -->
<include href="templates/nav.xml" />
<include href="templates/cards.xml" />
<include href="templates/data.xml" />

<layout type="stack">
<stack id="main" direction="horizontal" spacing="0">

<!-- Sidebar navigation -->
<panel id="sidebar">
<stack id="sidebar_inner" direction="vertical" spacing="8" padding="16"
scroll="true" border-right="1" border-color="theme.border">
<label id="sidebar_title" text="${var.app_title}" size="lg" />
<label id="cat_main" text="Pages" size="sm" />
<button id="nav_dashboard" template="nav_item" label="Dashboard" />
<button id="nav_monitoring" template="nav_item" label="Monitoring" />
<button id="nav_settings" template="nav_item" label="Settings" />
</stack>
</panel>

<!-- Content area -->
<stack id="content" direction="vertical" spacing="0" padding="16" scroll="true">

<!-- Dashboard page (visible by default) -->
<panel id="page_dashboard" template="content_page" visible="true">
<label id="dash_title" text="Dashboard" size="lg" />
<text id="dash_desc" content="Overview of application state with live data from timer sources." />

<!-- Status cards using included template -->
<stack id="dash_cards" direction="horizontal" spacing="16">
<panel id="card_uptime" template="status_card">
<label id="uptime_title" text="Uptime" size="sm" />
<label id="uptime_value" text="0s" size="xl">
<binding source="data.ticker" target="text" transform="format_uptime" />
</label>
</panel>

<panel id="card_counter" template="status_card">
<label id="counter_title" text="Event Counter" size="sm" />
<label id="counter_value" text="Count: 0" size="xl">
<binding source="data.ticker" target="text" transform="format_counter" />
</label>
<button id="btn_reset" label="Reset" variant="danger" size="sm"
on-click="on_reset_counter" />
</panel>

<panel id="card_refresh" template="status_card">
<label id="refresh_title" text="Stats Refresh" size="sm" />
<label id="refresh_value" text="Last update: --" size="md">
<binding source="data.stats" target="text" transform="format_timestamp" />
</label>
</panel>
</stack>

<!-- Metrics section using included template -->
<label id="dash_metrics_title" text="Metrics" size="md" />
<panel id="metrics_panel" padding="16" border="1" border-color="theme.border" rounded="md">
<stack id="metric_1" template="metric_display">
<label id="metric_1_name" text="Ticker Interval" size="sm" />
<label id="metric_1_val" text="1s" size="sm" />
</stack>
<stack id="metric_2" template="metric_display">
<label id="metric_2_name" text="Stats Interval" size="sm" />
<label id="metric_2_val" text="${var.refresh_interval}s" size="sm" />
</stack>
<stack id="metric_3" template="metric_display">
<label id="metric_3_name" text="Plugin" size="sm" />
<label id="metric_3_val" text="demo-settings" size="sm" />
</stack>
</panel>
</panel>

<!-- Monitoring page -->
<panel id="page_monitoring" template="content_page">
<label id="mon_title" text="Monitoring" size="lg" />
<text id="mon_desc" content="Live data monitoring with real-time updates from data sources." />

<stack id="mon_status_row" direction="horizontal" spacing="8">
<tag id="mon_tag_active" label="Active" variant="success" />
<tag id="mon_tag_sources" label="2 Sources" variant="info" />
</stack>

<label id="mon_ticker_title" text="Ticker Source" size="md" />
<panel id="mon_ticker_panel" padding="12" border="1" border-color="theme.border" rounded="md">
<label id="mon_ticker_val" text="Waiting...">
<binding source="data.ticker" target="text" transform="format_uptime" />
</label>
<progress id="mon_ticker_progress" value="0">
<binding source="data.ticker" target="value" />
</progress>
</panel>

<label id="mon_stats_title" text="Stats Source" size="md" />
<panel id="mon_stats_panel" padding="12" border="1" border-color="theme.border" rounded="md">
<label id="mon_stats_val" text="Waiting...">
<binding source="data.stats" target="text" transform="format_timestamp" />
</label>
</panel>
</panel>

<!-- Settings page -->
<panel id="page_settings" template="content_page">
<label id="settings_title" text="Settings" size="lg" />
<text id="settings_desc" content="Application settings. The demo-settings plugin provides additional configuration options." />

<label id="settings_app_title" text="Application" size="md" />
<panel id="settings_app_panel" padding="16" border="1" border-color="theme.border" rounded="md">
<switch id="settings_auto_refresh" label="Auto-refresh data" checked="true"
on-click="on_toggle_refresh" />
<stack id="settings_interval_row" direction="horizontal" spacing="8">
<label id="settings_interval_label" text="Refresh interval" size="sm" />
<slider id="settings_interval_slider" min="1" max="30" step="1"
value="${var.refresh_interval}" />
</stack>
</panel>

<label id="settings_theme_title" text="Theme" size="md" />
<panel id="settings_theme_panel" padding="16" border="1" border-color="theme.border" rounded="md">
<radio id="settings_theme_mode" options='["Dark","Light"]' value="Dark"
direction="horizontal" />
</panel>
</panel>

</stack>
</stack>
</layout>
</nemo>
31 changes: 31 additions & 0 deletions examples/complete/scripts/handlers.rhai
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
// Complete Example - UI Event Handlers

// Page panel IDs for navigation
let ALL_PAGES = [
"page_dashboard",
"page_monitoring",
"page_settings"
];

// Navigation handler: hides all pages, shows the one matching the clicked button
fn on_nav(component_id, event_data) {
set_component_property("page_dashboard", "visible", false);
set_component_property("page_monitoring", "visible", false);
set_component_property("page_settings", "visible", false);

// Derive page ID from nav button ID: "nav_dashboard" -> "page_dashboard"
let page_id = replace(component_id, "nav_", "page_");
set_component_property(page_id, "visible", true);
}

// Reset counter button handler
fn on_reset_counter(component_id, event_data) {
set_data("app.counter", 0);
}

// Toggle auto-refresh
fn on_toggle_refresh(component_id, event_data) {
let current = get_data("app.auto_refresh");
let new_val = if current == true { false } else { true };
set_data("app.auto_refresh", new_val);
}
37 changes: 37 additions & 0 deletions examples/complete/scripts/transforms.rhai
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
// Complete Example - Data Transform Functions

// Format the ticker value as an uptime string
fn format_uptime(value) {
let seconds = value;
if seconds == () { seconds = 0; }

let hours = seconds / 3600;
let minutes = (seconds % 3600) / 60;
let secs = seconds % 60;

let result = "";
if hours > 0 {
result += "" + hours + "h ";
}
if minutes > 0 || hours > 0 {
result += "" + minutes + "m ";
}
result += "" + secs + "s";
result
}

// Format a counter value with label
fn format_counter(value) {
if value == () {
return "Count: 0";
}
"Count: " + value
}

// Format stats refresh timestamp
fn format_timestamp(value) {
if value == () {
return "Last update: --";
}
"Last update: " + value + "s ago"
}
27 changes: 27 additions & 0 deletions examples/complete/templates/cards.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- Card and display templates for the complete example -->
<nemo>
<templates>
<template name="status_card">
<panel padding="16" border="1" border-color="theme.border" shadow="md" rounded="lg">
<stack direction="vertical" spacing="8">
<slot />
</stack>
</panel>
</template>

<template name="metric_display">
<stack direction="horizontal" spacing="12" padding="8" border-bottom="1" border-color="theme.border">
<slot />
</stack>
</template>

<template name="content_page">
<panel visible="false">
<stack id="inner" direction="vertical" spacing="12" padding="32">
<slot />
</stack>
</panel>
</template>
</templates>
</nemo>
8 changes: 8 additions & 0 deletions examples/complete/templates/data.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- Data source definitions for the complete example -->
<nemo>
<data>
<source name="ticker" type="timer" interval="1" />
<source name="stats" type="timer" interval="5" />
</data>
</nemo>
17 changes: 17 additions & 0 deletions examples/complete/templates/nav.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- Navigation templates for the complete example -->
<nemo>
<templates>
<template name="nav_item">
<button
variant="ghost"
size="sm"
text-color="theme.muted_foreground"
full-width="true"
min-height="24"
align="left"
padding-left="2"
on-click="on_nav" />
</template>
</templates>
</nemo>