| title | Building with Rust |
|---|---|
| description | Write a Native Shell app entirely in Rust, with no QuickJS on any platform, or in Rust with TSX screens on top: when to choose which, and how the two share components and modules. |
| order | 5.5 |
| group | start |
Native Shell apps are written in Rust, in TSX, or in both. All three run on the same engine: the reactive graph, the renderer, Taffy layout and the native modules (navigation, dialogs, the keyboard, the status bar) are Rust in every case, and every platform renders with its own UI. What differs is the language of your own code, and with it whether the app carries a JavaScript VM.
| Rust | TSX | Rust with TSX | |
|---|---|---|---|
| Your UI, state and handlers | Rust | TSX on QuickJS | Rust components and modules, TSX screens that use them |
| The app runs | the engine and your crate only, on every platform and in the browser | QuickJS and the engine (in the browser, its JavaScript engine) | QuickJS and the engine, with your crate (in the browser, its JavaScript engine) |
| Suits | Medium and large apps with demanding business logic | Apps and modules delivered at run time, quick iteration | A Rust core with parts that change often |
| Edits | Rebuild and restart | Hot updates | Hot updates for TSX, rebuild and restart for Rust |
| Start | nsh init my-app --template rust |
nsh init my-app --template ts |
nsh init my-app --template mixed |
Rust suits apps whose business logic is large and demanding: trading, editors, data-heavy tools. You get Rust's
type system and compile-time checks across the whole app, the crates.io ecosystem, native performance for your own
code (not only for NSH's), and headless tests of the real UI with cargo test. The app is Rust and native code from
start to end: no JavaScript VM ships with it or runs in it, on iOS, Android, macOS, Windows, Linux, GPUI and the web.
Components are functions that run once and bind signals to what they show, as in TSX (Reactivity),
written with typed styles instead of class strings.
TSX suits UI that arrives after the app ships: modules a native app loads at run time, generated by a model or written by another team, delivered as signed packages and run in a sandbox the host configures, with hot updates while you develop.
Rust with TSX keeps the Rust core and adds TSX where it pays off: screens, settings, onboarding, experiments, anything you iterate on with hot updates or deliver at run time as a signed package. TSX imports your Rust components and modules like any NSH module, so the two halves meet at typed interfaces. The TSX runs on QuickJS, which the app then carries.
nsh init my-app --template rust
cd my-app
nsh run # this machine's backend; --target ios, android, web, macos, linux or windows
nsh package --target web # or ios, ios-simulator, android, macos, linux, windowssrc/lib.rs holds the app: components, state, handlers, tasks.
use nsh_base::{button, theme};
use nsh_core::prelude::*;
/// The components available to TSX through `app/native`.
#[native_module("app/native")]
fn native(registry: &mut Registry) {
registry.register::<App>();
}
#[component]
pub fn App() -> impl IntoElement {
let count = signal(0_u32);
let quote = signal(SharedString::from("…"));
// Async work runs in tasks; requests go through NSH's network stack.
spawn(async move {
if let Ok(response) = fetch("https://api.example.com/quote").await {
quote.set(response.text().into());
}
});
view()
.flex_1()
.items_center()
.justify_center()
.gap_4()
.child(text(move || format!("Count: {}", count.get())))
.child(text(quote))
.child(
// nsh_base's unstyled button, drawn with the theme's colors.
button()
.px(px(20.0))
.py(px(12.0))
.rounded(move || theme().radius)
.bg(move || theme().primary)
.active(|s| s.bg(move || theme().primary.opacity(0.9)))
.on_press(move || count.update(|c| *c += 1))
.child(text("Tap").text_color(move || theme().primary_foreground)),
)
}Elements and components are made the same way: a function makes one (view(), text(..), nsh_base's
button(), your #[component] fn Chart's chart()), and its methods set its props, one setter per prop (a value,
a signal or a closure), and its listeners, one method per event. Listeners are closures of their event
(move |text: &str| ..), and move || .. for an event without data; .child(..) takes an element, or an
Option, a Vec, an array or an iterator of elements (.child(rows.iter().map(|r| row(r)))).
nsh.json names the root, and nsh mounts it:
{
"name": "My App",
"rust": ".",
"root": "App",
"theme": "theme.json",
"appearance": "system"
}"root" is the component's path in the crate (App, shell::Gallery; in the first one with
several crates); "theme" is the theme its components draw with, and "appearance" applies to it as it does to a TSX app. With it,
nsh run and nsh package of the app's directory build the app's own runner for the target, with the root mounted
and no QuickJS linked in:
| Target | What runs |
|---|---|
| macOS, Windows, Linux | The release runner with your crate, in a window; nsh run runs a debug build and shows the app's logs in the terminal |
| iOS, the iOS simulator, Mac Catalyst | The iOS runner with your crate, in an app |
| Android | The app's libnsh.so with your crate, started by the runtime's activity |
| Web | nsh.wasm with the engine and your crate (3.3 MB for the whole Rust gallery, 0.75 MB with brotli); the page mounts the root |
nsh builds what your crate needs and nothing more: a small program of your crate and NSH's core that describes its
components and modules to the CLI, which then does the rest itself, and the runner of each target you run or
package. The first build of a new Rust app compiles your crate and NSH's core, about 60 crates. nsh test runs
the app's tests headless in a runner of your crate built the same way.
On Linux, your crate selects optional platform features. Enable webview on your
nsh-platform-linux dependency to use web views; this links WebKitGTK 6.0, which must be installed to build and run
the app. The generated runner inherits that choice through Cargo feature unification.
During nsh run, a change to the crate's Rust code builds the runner again and restarts the app; in the browser,
the open page loads the new runtime by itself. cargo run starts the same root directly on the desktop backend
(src/main.rs), and native apps that embed NSH mount a Rust root with AppSource::Rust on the AppView of macOS,
Windows, Linux, GPUI and iOS.
Code for every platform, the web included, uses NSH's services: fetch for HTTP
(Networking), timer, set_timeout and set_interval for waiting,
nsh_core::time::Instant and nsh_core::time::unix_ms() for the clock. background and blocking run work on the
native targets' threads.
Rust apps draw their own title bars with nsh_core::element::desktop. Call
set_title_bar(TitleBarStyle::Hidden, Some(height)), mark the bar's background
with .window_drag(true), and reserve the native window controls' space with
title_bar(). Its top is the bar's height, and left and right are the widths
occupied by the system controls. Reading it subscribes to changes, including full
screen. The platform handles window dragging and the user's title-bar double-click
behavior; .window_drag(false) keeps a child out of its parent's drag region.
use nsh_core::{
desktop::TitleBarStyle,
element::{desktop::{close_window, set_title_bar, title_bar}}, prelude::*,
};
fn window_bar() -> impl IntoElement {
set_title_bar(TitleBarStyle::Hidden, Some(px(42.0)));
view()
.h(px(42.0))
.flex_row()
.items_center()
.window_drag(true)
.pl(move || title_bar().left + px(12.0))
.pr(move || title_bar().right + px(12.0))
.child(text("My app").flex_1())
.child(text("Close").window_drag(false).on_press(close_window))
}set_title_bar(TitleBarStyle::Default, None) restores the system title bar.
close_window() follows the embedding host's normal window-close behavior.
The module has the window's other functions (set_title, minimize,
toggle_full_screen, ...) and the menus (set_application_menu,
popup_menu) too: nsh/desktop.
On the desktops, Rust code shows the system's file dialogs and hands local paths to
the system, each answer awaited in a task: pick_paths with an OpenDialog
(OpenDialog::files(), ::folders() or ::files_and_folders()), pick_save_path
with a SaveDialog, open_path (the file's default app), reveal_path (the item
selected in Finder, File Explorer or the file manager) and move_to_trash (where the
item went, when macOS says). The dialogs answer Ok(None) when the user cancels and
an error when they cannot show (on Linux without a file chooser portal, and on iOS,
Android and the web, where open_path, reveal_path and move_to_trash answer
an error too). Every request that fails answers its error, never false.
use nsh_core::{
file_dialog::{FileFilter, OpenDialog},
prelude::*,
};
fn open_notes() {
let picked = pick_paths(
OpenDialog::files()
.multiple(true)
.filter(FileFilter::new("Markdown", ["md", "markdown"]))
.button_label("Open"),
);
spawn(async move {
match picked.await {
Ok(Some(paths)) => {
for path in paths {
let _ = reveal_path(path).await;
}
}
Ok(None) => {} // Cancelled.
Err(error) => log::warn!("no file dialog: {error}"),
}
});
}The crate's #[native_module("app/native")] function names what TSX may import: components
(registry.register::<Chart>()), modules of functions (registry.module::<Portfolio>(), #[module] on a unit struct and its impl),
classes (registry.class::<Ledger>(), #[class] on a struct and its impl) and assets; the attribute also makes
pub fn registry() at the crate's root, the same set for your tests and other Rust code (my_app_native::registry(),
next to nsh_base::registry()). In a #[module] or
#[class] impl, the pub fns are exported (pub(crate) too); functions without a visibility are helpers that
stay in Rust. Their TypeScript
declarations, doc comments included, are generated from the Rust types into .nsh/types, so TSX is checked against
them by nsh check and your editor:
/// A price chart.
#[derive(Clone, PartialEq, Props)]
pub struct ChartProps {
/// The symbol to chart.
pub symbol: Prop<SharedString>,
/// The range shown.
#[prop(default = SharedString::from("1D"))]
pub range: Prop<SharedString>,
}
#[derive(Event)]
pub enum ChartEvent {
/// A price was selected.
#[args(price)]
Select(f64),
}
/// A price chart; a press selects the last price.
#[component]
pub fn Chart(props: ChartProps, cx: Scope<ChartEvent>) -> impl IntoElement {
view()
.h(px(160.0))
.bg(move || theme().muted)
.on_press(move || cx.emit(ChartEvent::Select(101.5)))
.child(text(move || format!("{} · {}", props.symbol.get(), props.range.get())))
}
/// A ledger kept in Rust.
#[class]
pub struct Ledger {
entries: Vec<f64>,
}
#[class]
impl Ledger {
/// An empty ledger.
pub fn new() -> Self {
Ledger { entries: Vec::new() }
}
/// Adds `amount`; the new balance.
pub fn add(&mut self, amount: f64) -> f64 {
self.entries.push(amount);
self.balance()
}
/// The balance.
#[getter]
pub fn balance(&self) -> f64 {
self.entries.iter().sum()
}
/// Uploads the entries; resolves when the server has them.
pub async fn upload(this: Instance<Ledger>, url: String) -> Result<(), String> {
let body = this.with(|l| format!("{:?}", l.entries));
fetch(HttpRequest::post(url, body)).await.map(|_| ())
}
}
/// The app's holdings.
#[module]
pub struct Portfolio;
#[module]
impl Portfolio {
/// The value of the holdings.
pub fn total() -> f64 {
1250.0
}
}import { mount, signal, View, Text } from "nsh";
import { Chart, Ledger, Portfolio } from "app/native"; // Rust
const ledger = new Ledger(); // its state lives in Rust
function Overview() {
const balance = signal(ledger.add(Portfolio.total())); // Rust, called at once
return (
<View class="gap-2 p-4">
<Text>Balance: {balance()}</Text>
<Chart symbol="NSH" range="1M" onSelect={(e) => balance.set(ledger.add(e.price))} />
<View onPress={() => ledger.upload("https://example.com/ledger")} role="button" class="rounded-lg bg-blue-600 px-4 py-2 active:bg-blue-700">
<Text class="text-white">Upload</Text>
</View>
</View>
);
}
mount(() => <Overview />);Each export is documented in TypeScript by the doc comment of the Rust item that names it: a component by its
#[component] fn (Chart), a module and a class by their struct (Portfolio, Ledger), and props, events,
functions and members by their own (a setter needs none beside its getter). The Props struct's and the event
enum's own doc comments, and those of impl blocks, stay in Rust; a doc comment on a #[module] or #[class]
impl block is a compile error, since TypeScript would not show it. Each command that writes .nsh/types warns
about an export TSX sees without a doc comment, naming it (Ledger.add, Chart.range).
TypeScript shows a doc comment up to its first heading. Notes for Rust callers, such as what &self borrows or
why a function takes cx, go under a heading, # Rust (or rustdoc's # Panics, # Examples), and stay in the
Rust docs:
/// Puts `text` on the clipboard; resolves with the number of characters copied.
///
/// # Rust
///
/// Taking `cx` makes it an op, run in order with the UI changes TS made before it.
pub fn copy(text: String, cx: &mut ModuleCx) -> u32 {
let count = text.chars().count() as u32;
cx.set_clipboard(text);
count
}Props are the fields of the component's Props struct and events the variants of its Event enum (onSelect),
both in camel case. Rust code places the same component with its constructor, a setter per prop and a listener
method per event: chart().symbol("NSH").range("1M").on_select(move |price: &f64| last.set(*price)). A Rust component's own content, events and state stay in Rust; TSX passes props in and hears
events. A prop of the wrong type (level={1.5} for a u32 prop) is a warning, as on a built-in element: the
prop keeps its value and the rest of the component renders.
A function or a member runs the way its Rust signature says. One that is not async runs inside the TSX call and
returns its value (Portfolio.total() is a number, ledger.balance a property): it computes and keeps state. An
async fn returns a Promise and runs as a task with NSH's services (fetch, timer, dialogs). A module function
taking cx: &mut ModuleCx reaches the platform in order with the UI (a toast, the status bar), and so does a
component's ref method; their return type decides what TSX gets: one returning () gives void, with no reply
(TSX calls it and moves on); one returning Result<(), E> gives Promise<void>, which rejects with the Err's
message (the call runs with the UI's changes, after the TSX call returned, so the Promise carries its error; one
nobody awaits or catches is logged as an unhandled rejection); one
returning a value gives Promise<T>. Arguments and results are typed from the signature: numbers, strings, Option, Vec, your
#[derive(JsObject)] structs and #[derive(PropEnum)] enums, instances of your classes, and TSX functions
(Callback<A, R>, which Rust calls at once inside a call, or later with post and call_async; TS sees
(value: number) => void for a Callback<f64>, and #[args(balance)] listener: Callback<f64> names the argument,
(balance: number) => void). A function that starts listening is named on(event, listener) and returns Stop
(Stop::new(|| …)), which TSX gets as a function that stops it (() => void); it runs once, when TSX calls it or
when what called on goes (a component, or the module's run under hot reload). An event's handler gets one object: a variant's value is its field value, which
#[args(price)] on the variant names (onSelect({ price })), and #[args(open, reason)] names the fields of a
variant of several. An argument of
the wrong type throws a TypeError in the call, or rejects the Promise of one that returns a Promise; an Err
throws (or rejects) with its message. A class's object owns its Rust value: it is freed when the object is collected, or at
once with ledger.dispose() (also using ledger = new Ledger()). Name the TSX entry to run it, nsh run App.tsx or nsh package App.tsx --target …: TSX edits are hot
updates, Rust edits rebuild and restart the app. The CLI bundles and compiles the TSX itself, knowing your crate's
exports from its description; the development hosts and nsh test's headless runner are built with your crate and
QuickJS.
examples/rust-exports in the repository is the reference for exporting: a small TSX app whose crate exports one
component (props required, defaulted and optional, an event, children), one module (a function of each kind) and
one class (a constructor, methods, a getter and a setter, a static, an async member, another instance as an
argument, a callback Rust posts to, dispose() and using), each with a short doc comment, and tests, in Rust and
in TypeScript (App.test.tsx), that press through the app and check what each one shows.
examples/gallery-rs in the repository is a complete Rust app: every demo is Rust, nsh run examples/gallery-rs --target web (or ios, android, …) runs its root, nsh test examples/gallery-rs runs its tests headless, and
cargo run -p gallery-native starts it on the desktop backend.
A component draws content of the app's for values it has (a row of its data, a code block of a document) with a
render function: a prop Option<Renderer<T>>, in TypeScript (value: T) => JSX.Element. The component calls
render(key, value) where the content goes and gets a view it styles and places as any other (inside a text(..), an
inline view). The content is kept per key while the value stays equal, and rendered again when it changes. A Rust
parent passes a closure to the prop's setter (quote().render_badge(|symbol| ..)), which makes the
Renderer of it.
Styles in a component are typed; the app's Tailwind classes reach one of its parts through a prop of classes and
.app_class(..) on that part's element. They apply after the element's own styles, with the app's theme, dark: and
the states, as class on an element does.
/// A quote: its symbol, then content of the app's.
#[derive(Clone, Copy, PartialEq, Props)]
pub struct QuoteProps {
/// The symbol.
pub symbol: Prop<SharedString>,
/// Tailwind classes of the symbol's text.
pub symbol_class: Prop<Option<SharedString>>,
/// Content of the app's after the symbol (a change, a badge).
pub render_badge: Prop<Option<Renderer<SharedString>>>,
}
/// A quote of a symbol.
#[component]
pub fn Quote(props: QuoteProps) -> impl IntoElement {
view()
.flex_row()
.items_center()
.gap_2()
.child(text(props.symbol).app_class(props.symbol_class))
.child(dynamic(move || {
props.render_badge.get().map(|r| r.render("badge", props.symbol.get()).flex_1())
}))
}<Quote
symbol="NSH"
symbolClass="text-lg font-semibold"
renderBadge={(symbol) => <Text class="text-green-600">{`${symbol} ▲ 2.4%`}</Text>}
/>TextView in nsh/base is built this way: its renderCodeBlock, renderMath, ... are render functions, and its
classes are the app's classes by part (TextView).
A Rust app's tests run it headless, in cargo test or in nsh test, with the same driver: on the real engine,
without a window, with display frames at 60 Hz, so timers, tasks and animations run as on screen. The driver starts
the app, keeps a model of the views it shows and takes steps (press, type_text, submit, scroll, wait_for)
on views named by selectors (a text, an accessibility label or a test id; text:, label:, id:); a step that
cannot be taken fails with the texts on screen.
In cargo test, nsh_testing::Headless (nsh-testing's default features, without QuickJS) runs the root
(app.screen(): the texts on screen, the commands it sent); EngineConfig::new is a phone-sized engine with nothing
else set. nsh test runs TypeScript tests (Testing) in a headless runner built with your
crates, where open(".") starts a Rust app's root:
[dev-dependencies]
nsh-engine = "0.1"
nsh-testing = "0.1"// tests/app.rs: cargo test
use nsh_engine::{EngineConfig, Root};
use nsh_testing::Headless;
#[test]
fn a_tap_counts() {
let mut app = Headless::start(EngineConfig::new(Root::of::<my_app::App>())).unwrap();
app.wait_for("Count: 0").unwrap();
app.press("Tap").unwrap();
app.wait_for("Count: 1").unwrap();
}// App.test.tsx, next to nsh.json: nsh test
import { expect, open, test } from "nsh/test";
test("a tap counts", async () => {
const app = await open(".");
await app.press("Tap");
expect(app.texts()).toContain("Count: 1");
});A TSX entry is built with nsh-testing's feature bundle as nsh run builds it, against the UI sets its hosts
serve: NSH's, and each crate's registry(), which #[native_module] makes:
[dev-dependencies]
nsh-testing = { version = "0.1", features = ["bundle"] }
nsh-base = "0.1"#[test]
fn the_module_formats_money() {
let sets = vec![nsh_base::registry(), my_app_native::registry()];
let mut app = nsh_testing::tsx("../App.tsx", sets).unwrap();
app.wait_for("Formatted at once: $1,234.50").unwrap();
}nsh_testing::compile_tsx builds the entry once for several tests, and nsh_testing::start hands the
batches to a test that keeps its own model of the screen. examples/rust-exports tests its TSX entry this way;
examples/gallery-rs tests its Rust root in TypeScript, a test file for about each demo.
The look of an app is the app's own: NSH provides the primitives and the unstyled base components of nsh_base, and
your crate is the styled layer on top, the app's own component library (Three layers). Rust components take their colors, corner radius and fonts from a theme, theme() from
nsh_base, so one file restyles every component of the app. Each color has a light and a dark value;
when the system switches appearance, a binding such as .bg(move || theme().primary) changes color natively, with no
render.
The theme has the design tokens of shadcn/ui, and its default is shadcn's
neutral theme, a black-and-white palette with a 10 pt corner radius. To use your own, set "theme" in nsh.json
to the path of a theme file, relative to nsh.json, or to the theme's content inline:
{
"name": "My App",
"rust": ".",
"theme": "brand-theme.json"
}A theme file is shadcn's cssVars, the format of shadcn's themes and registry items, so a theme exported from
shadcn's themes or tweakcn works as it is. light sets the
values of the light appearance, dark those of the dark one, and theme those of both:
{
"cssVars": {
"theme": {
"radius": "0.5rem",
"font-sans": "Inter, sans-serif"
},
"light": {
"primary": "oklch(0.541 0.281 293.009)",
"primary-foreground": "oklch(0.969 0.016 293.756)",
"ring": "var(--primary)",
"up": "#16a34a"
},
"dark": {
"primary": "oklch(0.702 0.183 293.541)",
"primary-foreground": "oklch(0.283 0.141 291.089)",
"up": "#4ade80"
}
}
}- Your theme is applied over the default theme, so you list only what you change. A value
lightsets anddarkleaves out is used in both appearances. - Values are written as in CSS. Colors are any CSS color (
#7c3aed,rgb(…),hsl(…),oklch(…),white),color-mix(in oklab, var(--primary) 80%, white), or another token of the same appearance,var(--primary)(with a fallback:var(--brand, #7c3aed)). Lengths arerem(16 points) orpx(points). Fonts are font family lists: the first family that is not a generic one is used, and a generic first family (system-ui,sans-serif) is the system font. - Your own tokens. Any other name is a token of the app, as a variable in Tailwind's
@themeis:"up"above istheme().color("up")in Rust,bg-up/text-up/70in classes andtoken("up")in TSX (Theme tokens). A number such as"radius-card": "14px"istheme().number("radius-card")androunded-(--radius-card). - The bundler checks the theme when it builds: a missing file, invalid JSON or a theme other than
cssVarsfails the build with annsh.json: theme: …error.
The tokens, with their default light and dark values (shadcn's neutral):
| Token | theme() |
Light | Dark | Role |
|---|---|---|---|---|
background / foreground |
background / foreground |
oklch(1 0 0) / oklch(0.145 0 0) |
oklch(0.145 0 0) / oklch(0.985 0 0) |
The app's background, and the text on it |
card / card-foreground |
card / card_foreground |
oklch(1 0 0) / oklch(0.145 0 0) |
oklch(0.205 0 0) / oklch(0.985 0 0) |
Cards and grouped surfaces, and the text on them |
popover / popover-foreground |
popover / popover_foreground |
oklch(1 0 0) / oklch(0.145 0 0) |
oklch(0.205 0 0) / oklch(0.985 0 0) |
Popovers, menus and tooltips, and the text on them |
primary / primary-foreground |
primary / primary_foreground |
oklch(0.205 0 0) / oklch(0.985 0 0) |
oklch(0.922 0 0) / oklch(0.205 0 0) |
The main action (a primary button), and the text on it |
secondary / secondary-foreground |
secondary / secondary_foreground |
oklch(0.97 0 0) / oklch(0.205 0 0) |
oklch(0.269 0 0) / oklch(0.985 0 0) |
Secondary actions, and the text on them |
muted / muted-foreground |
muted / muted_foreground |
oklch(0.97 0 0) / oklch(0.556 0 0) |
oklch(0.269 0 0) / oklch(0.708 0 0) |
Quiet surfaces; secondary text: captions, labels, placeholders |
accent / accent-foreground |
accent / accent_foreground |
oklch(0.97 0 0) / oklch(0.205 0 0) |
oklch(0.269 0 0) / oklch(0.985 0 0) |
A hovered or highlighted item, such as a menu item or list row, and its text |
destructive |
destructive |
oklch(0.577 0.245 27.325) |
oklch(0.704 0.191 22.216) |
Destructive actions and errors |
border |
border |
oklch(0.922 0 0) |
oklch(1 0 0 / 10%) |
Dividers and borders |
input |
input |
oklch(0.922 0 0) |
oklch(1 0 0 / 15%) |
The border of inputs |
ring |
ring |
oklch(0.708 0 0) |
oklch(0.556 0 0) |
Focus rings |
chart-1 … chart-5 |
chart_1 … chart_5 |
oklch(0.87 0 0) … oklch(0.269 0 0) |
the same | The series of a chart |
sidebar, sidebar-foreground, sidebar-primary, sidebar-primary-foreground, sidebar-accent, sidebar-accent-foreground, sidebar-border, sidebar-ring |
sidebar, sidebar_foreground, ... |
sidebar oklch(0.985 0 0), sidebar-primary oklch(0.205 0 0) |
sidebar oklch(0.205 0 0), sidebar-primary oklch(0.488 0.243 264.376) |
A sidebar's surface, its main action, its highlighted item, borders and focus ring |
radius |
radius |
0.625rem (10 pt) |
The corner radius | |
font-sans |
font_sans |
the system font | The app's font: every text that sets no family of its own | |
font-mono |
font_mono |
the system monospace font | The monospaced font: font-mono, code in TextView |
An app's Rust code can be split into crates, each imported by its own specifier: "rust" takes a list of crate
paths, and each crate in it has its own #[native_module] function.
{
"rust": ["native/charts", "native/storage"],
"root": "App"
}import { Chart } from "app/charts"; // native/charts: #[native_module("app/charts")]
import { Store } from "app/storage"; // native/storage: #[native_module("app/storage")]Every runner and host links all of them, and .nsh/types declares each specifier. Crates share code through Cargo
path dependencies (shared = { path = "../shared" }): a crate of company components can be used by several crates
and listed by several apps, and only the crates listed in "rust" export to TSX. Each listed crate has a package
name and a specifier of its own; nsh reports a repeated one with the two paths that share it. A Rust app's
"root" is a component of the first crate listed. During nsh run and nsh dev, a change to any of them, or to a
crate they depend on by path, rebuilds the app and restarts it.
On the web, a crate can call the browser's APIs (a WebSocket, IndexedDB, …) through wasm-bindgen and web-sys;
nsh builds the glue they need with the runtime. See Web.
- Project structure: every key of
nsh.json. - Web: Rust apps in the browser.
- Networking:
fetchfrom Rust. - CLI:
nsh init --template rustand--template mixed.