# Native Shell > Native Shell is a Rust-first native UI framework: build one app for iOS, Android, macOS, Windows, Linux and the web, with each platform’s own UI and interaction. Apps are written in Rust (compiled to native code) or TSX (run by QuickJS, and by the browser's JavaScript engine on the web), or both, on one native Rust runtime; the toolchain is the single `nsh` binary (`nsh init my-app --template rust|ts`, `nsh run my-app`, `nsh package my-app --target `). Every page below is also served as Markdown at its URL plus `.md`. --- # Introduction Native Shell (NSH for short) runs native UI written in **Rust or TSX**, on one engine written in Rust, and shows it with each platform's own UI. Write a whole app in Rust, compiled to native code with no JavaScript VM; write TSX modules that a native app loads at run time, in a sandbox it controls; or combine both, with TSX screens on top of your Rust components. NSH is designed for **AI-generated, dynamically delivered native UI**: a model writes a small TSX module, and the app shows it natively. The same mechanism makes native apps extensible in general — plugins, features, whole screens delivered after release. NSH runs on phones and desktops alike: iOS (UIKit), Android (Android views), macOS (AppKit), Windows (Win32 windows drawn with Direct2D) and Linux (GTK 4), in GPUI apps and on the web. Each platform has a native backend of its own, and the same app runs on all of them ([Platforms](https://nsh.dev/versions/v0.2.0/docs/platforms.md)). > [!IMPORTANT] > NSH is the layer inside an app for UI that is generated, changed or added after it ships. On a phone or a desktop, the app stays a native app, built and released the way it always was. ## Why it exists **First: UI generated by AI.** An assistant inside your app can answer with an interface rather than with text. A model writes a small TSX module (a form, a chart, a booking card), your server checks, builds and signs it, and the app downloads it and shows it in an `NshAppView`: native UI, sandboxed, limited to what you granted, and with no app update. NSH is built around this use, so its design is AI friendly throughout: - **A format models write well.** A module is one TSX file using signals, a handful of host elements and Tailwind classes — the vocabulary models already know from the web, with no build configuration and no NPM. - **A precise, generated contract.** Every element, prop, style key and module has TypeScript declarations generated from the Rust sources (`.nsh/types`), so generated code can be type-checked, and a model can be given the exact API instead of guessing. These docs are also served as plain text for models ([`llms.txt`](/llms.txt), [`llms-full.txt`](/llms-full.txt)). - **Checkable before it ships.** `nsh test` runs a module headless, without a device, takes steps on it and reports errors and the views it produced; build errors come with a code frame. A generation loop can compile, run, read the result and retry. The compiler also runs in-process as a library (`nsh_bundler::compile_module`) and in the browser ([Playground](/playground)). - **Safe to run when it is not trusted.** Generated code is untrusted code. Each module runs in its own QuickJS VM with memory, call-time and node limits, reaches only the capabilities the host grants, and arrives as a signed package the app verifies first. - **Native without native code.** The output is the platform's own UI — UIKit, Android views, AppKit, GTK 4 or Win32 with Direct2D — with the system's text, input and accessibility, yet the model never writes Swift, Kotlin, Java or Rust, and the app never needs a new release. ![From a prompt to native UI: a model writes a TSX module; your server builds, checks and signs it; across the trust boundary, the app verifies the package, runs it in a sandbox with the limits and capabilities it set, and shows it with the platform's own UI.](https://nsh.dev/versions/v0.2.0/diagrams/ai-flow-light.svg) **Second: extending native apps, and delivering UI dynamically.** The same mechanism serves any module, not only generated ones. The app embeds an `NshAppView` (iOS), `NshView` (Android) or `AppView` (macOS, Windows, Linux) wherever a module should appear, and NSH runs the module inside it: - **Delivered after release.** A module is a signed package (`.nshpkg`) the app downloads at run time. The app checks its signature against the keys it trusts and against a revocation list before running it ([Signed packages](https://nsh.dev/versions/v0.2.0/docs/signed-packages.md)). A change needs no new binary and no store release. - **The host stays in control.** Each module runs in its own sandboxed VM, with memory, time and node limits, and reaches only what the host grants: network hosts, web pages, the status bar ([Sandbox and capabilities](https://nsh.dev/versions/v0.2.0/docs/sandbox-and-capabilities.md)). Several modules can run side by side; there are no global singletons. - **It looks like the rest of the app.** Modules render with the platform's own UI, so text, input, accessibility and scrolling are the system's. The host can register its own native views — a map, a player, a design-system component — for modules to use ([Native components](https://nsh.dev/versions/v0.2.0/docs/native-components.md)). **Third: whole apps, in Rust or TSX.** `nsh package` turns an app into an installable iOS, Android, macOS, Windows, Linux or web app. An app written in Rust gets Rust's type system, the crates.io ecosystem and native performance for its own code, with no JavaScript VM on any platform ([Building with Rust](https://nsh.dev/versions/v0.2.0/docs/rust.md)); a TSX app runs on the prebuilt runtime. The [getting started](https://nsh.dev/versions/v0.2.0/docs/getting-started.md) guide creates one of each. ## Why Native Shell, next to GPUI Shell [Longbridge](https://longbridge.com) is building NSH as the plugin layer of its mobile client, and its desktop client needs the same layer. [GPUI Shell](https://gpui-kit.com/shell) makes [GPUI](https://gpui-kit.com) desktop apps extensible in JavaScript, and the first idea was to use it on phones too. It does not fit there, because its modules are drawn by GPUI itself, on the GPU, not with the platform's views: - **System behavior would have to be rebuilt.** A self-drawn surface inside a native screen has none of the platform's text selection, nor the selection menu that comes with it (Copy, Share, Translate, Look Up, Select All), nor the system's input methods, accessibility and new visual materials each iOS release brings. Recreating them in a GPU renderer is a large, never-finished job, and the copies drift from the real thing. - **Mobile teams would have to adopt a new UI stack.** Running a self-drawn layer at scale in an existing mobile client means its iOS and Android teams take on an unfamiliar renderer, its compatibility issues across OS versions and devices, and its bugs. NSH therefore renders modules with each platform's own UI: UIKit on [iOS](https://nsh.dev/versions/v0.2.0/docs/ios.md), Android views on [Android](https://nsh.dev/versions/v0.2.0/docs/android.md), AppKit on [macOS](https://nsh.dev/versions/v0.2.0/docs/macos.md), Win32 windows drawn with Direct2D on [Windows](https://nsh.dev/versions/v0.2.0/docs/windows.md) and GTK 4 on [Linux](https://nsh.dev/versions/v0.2.0/docs/linux.md). One module format and one runtime run in all these hosts. The two are positioned differently. GPUI Shell is for extending GPUI desktop applications; Native Shell is for native apps on every platform, phones and desktops alike. ## An app The same counter, in Rust and in TSX: ```rust use nsh_core::ui::prelude::*; #[component] pub fn Counter() -> impl IntoElement { let count = signal(0); view() .flex_1() .items_center() .justify_center() .gap_3() .child(text(move || count.get().to_string()).text_5xl().font_weight(FontWeight::BOLD)) .child( view() .rounded_lg() .bg(rgb(0x2563eb)) .px_5() .py_3() .active(|s| s.opacity(0.6)) .on_press(move || count.update(|n| *n += 1)) .child(text("Tap").text_lg().text_color(white())), ) } ``` ```tsx import { mount, signal, Text, View } from "nsh"; function Counter() { const count = signal(0); return ( {count()} count.update((n) => n + 1)} activeStyle={{ opacity: 0.6 }} class="rounded-lg bg-blue-600 px-5 py-3" > Tap ); } mount(Counter); ``` A view (`view()`, ``) becomes a `UIView` on iOS, a `ViewGroup` on Android, an `NSView` on macOS and a GTK widget on Linux, a text a native text view, and the button's press feedback (`.active(..)`, `activeStyle`) runs on the main thread. When you tap, `count.update` changes one signal, and only the text that reads it is updated: the component function does not run again. The Rust app is native code from start to end; in the TSX app, only the module's own code is JavaScript. A TSX module is also what a native app loads at run time. The host app shows it with a few lines of native code ([iOS](https://nsh.dev/versions/v0.2.0/docs/ios.md), [Android](https://nsh.dev/versions/v0.2.0/docs/android.md), [macOS](https://nsh.dev/versions/v0.2.0/docs/macos.md#embedding-in-a-mac-app), [Windows](https://nsh.dev/versions/v0.2.0/docs/windows.md#embedding-in-a-win32-app), [Linux](https://nsh.dev/versions/v0.2.0/docs/linux.md#embedding-in-a-gtk-app)). On iOS: ```swift let app = NshAppView() // a UIView: place it anywhere try app.grantCapabilities(#"{ "network": { "hosts": ["api.example.com"] } }"#) let info = try await app.loadPackage(at: downloadedFile, trust: trust) // verified, then run ``` Everything the module calls into — the reactive graph, the renderer, layout, text and every module it imports — is native, written in Rust. ## How it runs The toolchain is a single `nsh` binary. An app written in Rust is a crate: `nsh` builds the runner of each target with your crate linked in, and its root component runs on the engine directly. A TSX app (a module) is a set of `.tsx` / `.ts` files with an entry, usually `App.tsx`: 1. **Build.** `nsh` bundles the module's files (with rolldown and Oxc) and compiles the JSX with its own compiler. Imports of `nsh` and `nsh/*` stay imports: the runtime serves them natively, so the bundle holds only your code. The bundle is compiled to QuickJS bytecode. 2. **Run.** On the device, the engine thread runs the bytecode in [QuickJS](https://github.com/quickjs-ng/quickjs). Compiled JSX calls the native renderer directly; changes are collected during one JavaScript turn, laid out with [Taffy](https://github.com/DioxusLabs/taffy) (flexbox and CSS grid), and handed to the main thread as one batch of view operations per frame. 3. **Render.** The main thread applies the batch to native views. Text is measured and laid out once on the engine thread (CoreText on Apple platforms, `StaticLayout` on Android, DirectWrite on Windows, Pango on Linux) and only drawn on the main thread. A Rust app takes the same path from step 2 on, with its components running natively in place of QuickJS. In a TSX app, JavaScript only describes the UI and handles events, so the work done in the interpreter is small. Parsing styles, resolving Tailwind classes, diffing, layout and text all happen in Rust. > [!NOTE] > The main thread never waits for JavaScript. Questions the system asks synchronously (may this input accept another character, may the user swipe back, may this sheet close) are answered natively from props you set beforehand, such as `maxLength`, `popOnBack` or `closeOnSwipe`. Events reach your code afterwards. ## What you get - **Rust or TSX.** Apps written in Rust, with typed styles and compile-time checks and no JavaScript VM; TSX for screens and modules delivered at run time; or both, with TSX importing your Rust components, modules and classes ([Building with Rust](https://nsh.dev/versions/v0.2.0/docs/rust.md)). - **Embedding.** `NshAppView` (Swift), `NshView` (Java/Kotlin) and the desktops' `AppView` (AppKit, GTK, Win32) put a module in a native app, as many as you like ([iOS](https://nsh.dev/versions/v0.2.0/docs/ios.md), [Android](https://nsh.dev/versions/v0.2.0/docs/android.md), [macOS](https://nsh.dev/versions/v0.2.0/docs/macos.md#embedding-in-a-mac-app), [Linux](https://nsh.dev/versions/v0.2.0/docs/linux.md#embedding-in-a-gtk-app), [Windows](https://nsh.dev/versions/v0.2.0/docs/windows.md#embedding-in-a-win32-app)). - **Signed, revocable delivery.** Ed25519-signed packages, release keys certified by an offline root key, revocation lists ([Signed packages](https://nsh.dev/versions/v0.2.0/docs/signed-packages.md)). - **A sandbox the host configures.** VM limits and capabilities granted per view, in the iOS and Android SDKs ([Sandbox and capabilities](https://nsh.dev/versions/v0.2.0/docs/sandbox-and-capabilities.md)). - **The host's own views in modules.** Register native components from Swift, Java or Rust ([Native components](https://nsh.dev/versions/v0.2.0/docs/native-components.md)). - **Native views.** Embedding a native map, web view or video player has no layering problems, accessibility uses the platform's own tree, and text input is the system's. - **Fine-grained reactivity.** [Signals](https://nsh.dev/versions/v0.2.0/docs/reactivity.md) like Solid's, implemented natively. Components run once; only the expressions that read a changed signal run again. - **Styles and Tailwind**, resolved natively: flexbox, CSS grid, shadows, gradients, filters, transitions ([Styling](https://nsh.dev/versions/v0.2.0/docs/styling.md), [Tailwind](https://nsh.dev/versions/v0.2.0/docs/tailwind.md)). - **Primitives and base components; the look is the app's.** Apps draw their UI from the primitives (`View`, `Text`, `Input`, `List`, ...) with styles and Tailwind classes, and build on [`nsh/base`](https://nsh.dev/versions/v0.2.0/modules/nsh-base.md), unstyled base components that own behavior, state and accessibility: navigation stacks and modals, safe areas, system alerts, the keyboard and the status bar. The styled component library on top is the app's own or the community's ([Three layers](https://nsh.dev/versions/v0.2.0/docs/architecture.md#three-layers)). Desktop layouts are built from the primitives with `desktop={{…}}` props and `platform.select` ([Platform-specific code](https://nsh.dev/versions/v0.2.0/docs/platform-specific.md)). - **One binary toolchain.** A single `nsh` executable builds modules, writes their type declarations, serves them with hot module replacement, packages and signs them. ## Where it runs | Platform | Status | Backend | Notes | |---|---|---|---| | iOS | Supported (iOS 15.0+) | UIKit | Devices and the simulator; embedded with `NativeShell.xcframework` ([iOS](https://nsh.dev/versions/v0.2.0/docs/ios.md)) | | Android | Supported (API 28, Android 9+) | Android views | `arm64-v8a` and `x86_64`; embedded with `nsh.aar` ([Android](https://nsh.dev/versions/v0.2.0/docs/android.md)) | | macOS | Supported (macOS 14+) | AppKit | Standalone apps; embedded with `AppView` ([macOS](https://nsh.dev/versions/v0.2.0/docs/macos.md)) | | Windows | Supported (Windows 10 1809+) | Win32, Direct2D | Standalone apps; embedded with `AppView` ([Windows](https://nsh.dev/versions/v0.2.0/docs/windows.md)) | | Linux | Supported (GTK 4.14+) | GTK 4 | Standalone apps; embedded with `AppView` ([Linux](https://nsh.dev/versions/v0.2.0/docs/linux.md)) | | GPUI (macOS, Windows, Linux) | Supported | GPUI | Embedded with `AppView` in GPUI and GPUI Kit apps ([GPUI](https://nsh.dev/versions/v0.2.0/docs/gpui.md)) | | Web | Supported (modern browsers) | HTML | Static sites; embedded in a page with `createPlayer` or an iframe ([Web](https://nsh.dev/versions/v0.2.0/docs/web.md)) | | Mac Catalyst | For development | UIKit | The iPhone-sized window `nsh run` opens on a Mac, to see a module as on a phone | ![One module, many hosts: the same bundle and Rust runtime on iOS, Android, Linux, macOS with AppKit and Windows with Win32 / Direct2D, GPUI on all three desktops, and web pages drawn with HTML.](https://nsh.dev/versions/v0.2.0/diagrams/platforms-light.svg) ## How it compares The usual ways to put UI into an app after release are web views, server-driven UI described in JSON, and mini-program containers; for whole apps, cross-platform frameworks such as React Native, Flutter, Electron and Tauri. An NSH module renders native UI where a web view renders a page, carries its own state and logic where a server-driven schema can only recombine built-in components, and runs in a sandbox the host configures in code. [Comparing approaches](https://nsh.dev/versions/v0.2.0/docs/comparison.md) goes through each. ## Next steps - [Installation](https://nsh.dev/versions/v0.2.0/docs/installation.md) and [Getting started](https://nsh.dev/versions/v0.2.0/docs/getting-started.md): get `nsh`, write a first app in Rust or TSX and run it on each platform. - [Building with Rust](https://nsh.dev/versions/v0.2.0/docs/rust.md): apps written in Rust, and TSX on top of them. - [Platforms](https://nsh.dev/versions/v0.2.0/docs/platforms.md): what runs where, and how to run, package and embed on iOS, Android, macOS, Windows and Linux. - [Signed packages](https://nsh.dev/versions/v0.2.0/docs/signed-packages.md) and [Sandbox and capabilities](https://nsh.dev/versions/v0.2.0/docs/sandbox-and-capabilities.md): deliver modules and decide what they may do. - [JSX and components](https://nsh.dev/versions/v0.2.0/docs/jsx-and-components.md) and [Reactivity](https://nsh.dev/versions/v0.2.0/docs/reactivity.md): the two ideas everything else builds on, in Rust and TSX. Source: https://nsh.dev/versions/v0.2.0/docs --- # Comparing approaches An app that wants UI it did not ship with — answers generated by a model, features from partners or other teams, screens that change without a release — has a handful of established options, on phones and desktops alike. They differ in what draws the UI, what the delivered code may do, and how much of the app has to change to host them. This page compares them with NSH, and then compares NSH with the cross-platform frameworks used to write whole apps. ## The options | | What draws it | What it can express | Confinement | Fit for generated UI | Cost to the host app | |---|---|---|---|---|---| | **Web view (H5)** | A browser engine | Anything HTML, CSS and JS can | Same-origin rules; native access through a hand-written bridge | Models write HTML well; the result looks and behaves like a web page | A browser engine per view; a bridge to maintain | | **Server-driven UI** | The app's own native views | Only the components and actions the app built in, described in JSON | Total: the schema is the limit | Easy to generate, little to express: no local state or logic beyond what the schema provides | A renderer for every component and its versions, in each app | | **Mini-program containers** | Usually a web engine with a proprietary layer | A platform-specific framework and API | The container's permission model | Proprietary dialect, less known to models | A large container SDK | | **Over-the-air updates of a cross-platform app** (React Native with a code-push service, for example) | That framework's renderer | The whole framework | None: updates are trusted app code | Code generated per request is not what these updates are for | The app has to be written in that framework | | **NSH** | The platform's own UI: UIKit, Android views, AppKit, GTK 4, Win32 with Direct2D | Components with state, logic, animation and layout; host-registered native views | Signed packages, a VM per view with host-set limits, capabilities granted per view | Designed for it: one TSX module, typed API, headless checks | One view and a library in an existing native app | ## How NSH differs - **The platform's own UI.** A module is made of the platform's views, so text selection and its menu, input methods, accessibility and scrolling are the system's own — the same reason NSH exists next to [GPUI Shell](https://nsh.dev/versions/v0.2.0/docs.md#why-native-shell-next-to-gpui-shell). - **More than a schema.** Server-driven UI only recombines what the app already ships. An NSH module has its own state, logic, animations and layout, so a model can produce an interface the app's developers never anticipated. - **Confined by the host.** Delivered code is untrusted. NSH checks signatures and revocations before running anything, gives each view its own VM with memory, call-time and node limits, and lets the host decide which network hosts, web pages and system settings a module may reach ([Sandbox and capabilities](https://nsh.dev/versions/v0.2.0/docs/sandbox-and-capabilities.md)). - **Embedded, not adopted.** The app stays native and keeps its toolchain; NSH is a view placed where a module should appear ([Platforms](https://nsh.dev/versions/v0.2.0/docs/platforms.md#embed-in-a-native-app)). - **One module, every platform.** The same bundle runs on iOS, Android, macOS, Windows and Linux, each time with that platform's own UI ([Platforms](https://nsh.dev/versions/v0.2.0/docs/platforms.md)). ## Cross-platform frameworks NSH is designed for modules inside native apps, and whole apps, written in Rust or TSX, package for every platform, so it is natural to compare it with the frameworks people write cross-platform apps in. The comparison is about how each draws and where it runs, not about maturity: these frameworks have large ecosystems that NSH does not have. | | What draws it | Platforms | App code | Code delivered after release | |---|---|---|---|---| | **React Native** | The platform's views on iOS and Android; desktops through separate projects | iOS, Android; Windows and macOS out of tree | JavaScript or TypeScript, with React | Over-the-air updates of trusted app code | | **Flutter** | Its own engine draws every pixel | iOS, Android, macOS, Windows, Linux, web | Dart | Not part of the framework | | **Electron** | Chromium, bundled with the app | macOS, Windows, Linux | HTML, CSS, JavaScript | Web content the app loads | | **Tauri** | The system's web view | macOS, Windows, Linux, iOS, Android | HTML, CSS, JavaScript; Rust for the native side | Web content the app loads | | **NSH** | The platform's own UI on each platform | iOS, Android, macOS, Windows, Linux, web | Rust apps with no JavaScript VM, or TSX modules; the runtime is Rust | Signed TSX packages, verified and sandboxed per view | What sets NSH apart: - **Native UI on every platform, not one renderer everywhere.** Flutter draws everything itself, and Electron and Tauri show web pages; NSH builds each platform's own views, so text selection, input methods, accessibility and the system's look come from the platform. - **Small modules inside native apps.** The frameworks above own the whole app. NSH is designed to sit inside an app written with the platform's own tools, as one view among others, and to run modules the app did not ship with, in a sandbox. - **Rust apps, TSX modules.** A whole app can be Rust from start to end, with Rust's type system and crates and no JavaScript VM, while the parts that change after release stay TSX modules on the same engine. - **A light runtime.** No browser engine per app or per view; in a TSX module, JavaScript only runs the module's own code, and everything it calls is native Rust. ## When another option fits better - The UI is a document or an existing web page: a web view shows it as it is. - The set of screens is fixed and small, and only content and order change: server-driven UI needs no runtime at all. - The whole app is written in a cross-platform framework already: its own update mechanism ships its code. - You need a large ecosystem of third-party packages, or a UI kit built from each desktop's own controls: NSH's components are drawn by NSH and look the same on every platform. ## See also - [Introduction](https://nsh.dev/versions/v0.2.0/docs.md): why NSH exists and what it is designed for. - [Architecture](https://nsh.dev/versions/v0.2.0/docs/architecture.md): what runs where inside NSH. Source: https://nsh.dev/versions/v0.2.0/docs/comparison --- # Installation Everything NSH does goes through one command, `nsh`. It builds apps written in Rust or TSX, runs them on every platform, serves TSX to development hosts with hot updates, and packages installable apps. You can use it from a prebuilt distribution or build it from a checkout of the repository. Apps written in Rust also need a Rust toolchain ([below](#for-apps-written-in-rust)); TSX apps need `nsh` alone. On top of that, each platform needs its own tools: | Platform | Develop and package on | Needs | |---|---|---| | iOS | macOS | Xcode and an iOS simulator runtime | | Android | macOS, Windows or Linux | The Android SDK and a JDK; an emulator or a device | | macOS | macOS | Xcode's command line tools; apps run on macOS 14+ | | Windows | Windows | Windows 10 1809+; the WebView2 Runtime for web views | | Linux | Linux | GTK 4.14+ (and its development packages to build the runtime) | | [GPUI](https://nsh.dev/versions/v0.2.0/docs/gpui.md) (embedding in GPUI apps) | macOS, Windows or Linux | A GPU (Metal, DirectX or Vulkan); on Linux, Wayland or X11 | The AppKit host is the default on macOS. Mac Catalyst (`--target catalyst`) needs Xcode like iOS. Details are under [Platform tools](#platform-tools). ## Installing a release Each [GitHub release](https://github.com/longbridge/nsh/releases) carries the prebuilt distribution (below) for macOS (Apple silicon), Linux x86_64 and Windows x86_64. Every way of installing it puts the same `nsh` command on your `PATH`. **macOS and Linux**, with the install script: ```sh curl -fsSL https://nsh.dev/install.sh | sh ``` It installs the distribution to `~/.nsh` and links `~/.local/bin/nsh` to it; running it again updates it. `NSH_VERSION=v0.1.0` pins a release, `NSH_INSTALL_DIR` picks another directory. **Windows**, in PowerShell: ```powershell irm https://nsh.dev/install.ps1 | iex ``` It installs to `%LOCALAPPDATA%\nsh` and adds its `bin` to your user `PATH` (open a new terminal afterwards). `$env:NSH_VERSION` and `$env:NSH_INSTALL_DIR` work as above. **Homebrew** (macOS and Linux): ```sh brew install longbridge/tap/nsh ``` **Scoop** (Windows): ```powershell scoop install https://github.com/longbridge/nsh/releases/latest/download/nsh.json ``` **mise** (all three): ```sh mise use -g github:longbridge/nsh ``` The archives (`nsh-macos-arm64.tar.gz`, `nsh-linux-x86_64.tar.gz`, `nsh-windows-x86_64.zip`, each with a `.sha256`) carry no version in their names, so `https://github.com/longbridge/nsh/releases/latest/download/` always fetches the latest. The embedding SDKs are also released alone, for native apps that need no CLI: `nsh-embed-ios.zip` and `nsh-embed-android.zip`. > [!NOTE] > The binaries are not signed yet. macOS quarantines files downloaded by a browser; the install script, Homebrew and mise download with `curl` and are not affected, otherwise run `xattr -dr com.apple.quarantine `. The Linux runtime needs glibc 2.39 and GTK 4.14 or newer (Ubuntu 24.04 and later). There is no Intel Mac or arm64 Linux / Windows build: build those from a checkout. ## For apps written in Rust Install Rust through [rustup](https://rustup.rs). `nsh` builds a Rust app's runner for each target with Cargo, from your crate and NSH's crates on crates.io at the CLI's version, so add the Rust targets of the platforms you run on: ```sh rustup target add aarch64-apple-ios-sim aarch64-apple-ios aarch64-apple-ios-macabi # iOS simulator, devices, Mac Catalyst rustup target add aarch64-linux-android x86_64-linux-android # Android (with the SDK's NDK) rustup target add wasm32-unknown-unknown # the web ``` The desktops build for the machine you are on. `cargo run` in a Rust app's directory starts it on the desktop backend directly, which builds with the backend's development libraries ([Platform tools](#platform-tools)). Android builds also use an **NDK** from the SDK, as [building the Android runtime](#building-the-android-runtime) does. ## The prebuilt distribution The distribution is a directory, `dist/nsh`, with the CLI and the prebuilt native runtimes it packages apps with: ```sh dist/nsh/bin/nsh # the CLI dist/nsh/runtime/ios/iphoneos/nsh-ios-app # runs packaged apps on iOS devices dist/nsh/runtime/ios/iphonesimulator/{nsh-ios-app,nsh-ios-host} dist/nsh/runtime/ios/maccatalyst/nsh-ios-host # the Mac Catalyst development host dist/nsh/runtime/android/{release,dev}/ # classes.dex, lib//libnsh.so dist/nsh/runtime/macos/{nsh-macos-host,nsh-macos-app} # the macOS (AppKit) development host and release runner dist/nsh/runtime/linux/{nsh-linux-host,nsh-linux-app} # the Linux development host and release runner dist/nsh/runtime/windows/{nsh-windows-host,nsh-windows-app}.exe # the Windows development host and release runner dist/nsh/runtime/gpui/nsh-gpui-host[.exe] # the GPUI development host, for this OS dist/nsh/runtime/web/{nsh.wasm,nsh-dev.wasm,nsh.js} # the web runtimes (nsh package / run --target web) dist/nsh/embed/ios/{NativeShell,NativeShellDev}.xcframework # embedding in iOS apps dist/nsh/embed/android/{nsh,nsh-dev}.aar # embedding in Android apps ``` Put `dist/nsh/bin` on your `PATH` (or call `dist/nsh/bin/nsh` directly) and keep the directory together: `nsh` finds the runtimes next to itself, in `bin/../runtime` (through a symlink to it, as the installers make, too). ```sh export PATH="$PWD/dist/nsh/bin:$PATH" nsh --version ``` The distribution is made with `cargo xtask dist` in a checkout (below). The `nsh` binary it contains is built for that machine. On a Mac with Xcode it includes the iOS and macOS runtimes (Apple Silicon); on Linux it includes the Linux runtime instead, built for that machine; on Windows the Windows runtime, with `bin\nsh.exe`. Each also includes the [GPUI](https://nsh.dev/versions/v0.2.0/docs/gpui.md) development host, built for that machine. All include the Android runtime (on Windows only when the Android SDK is found). ## Installing the latest source On macOS and Linux, install the latest `main` with Git, Rust and the platform build dependencies below: ```sh curl -fsSL https://nsh.dev/install.sh | sh -s -- --source nsh -V # nsh 0.1.1 (abcdef0) ``` The installer keeps the checkout in `~/.nsh/source`, builds the release CLI and links it into `~/.local/bin`. Re-run the command to update; local source changes must be saved first. The retained checkout lets NSH build native runtimes on demand. Cargo uses its existing configuration and target directory. `NSH_INSTALL_DIR` changes the installation prefix; `NSH_VERSION` selects prebuilt releases only. Both `-V` and `--version` show the version and seven-character source revision. ## Building from a checkout You need Rust through [rustup](https://rustup.rs). The repository's `rust-toolchain.toml` selects the stable toolchain and installs the iOS and Android targets on first use. ```sh git clone https://github.com/longbridge/nsh.git cd nsh cargo build -p nsh-cli # ./target/debug/nsh ``` Then build the development host of each target you want to run on, once, and again after you update the checkout: ```sh cargo build -p nsh-ios-host --target aarch64-apple-ios-macabi # nsh run (Mac Catalyst window) cargo build -p nsh-ios-host --target aarch64-apple-ios-sim # nsh run --target ios cargo xtask android --dev # nsh run --target android cargo build -p nsh-macos-host # nsh run --target macos (built for you in a checkout) cargo build -p nsh-linux-host # nsh run on Linux (built for you in a checkout) cargo build -p nsh-windows-host # nsh run on Windows (built for you in a checkout) cargo build --manifest-path crates/gpui/Cargo.toml --target-dir target -p nsh-gpui-host # nsh run --target gpui (built for you in a checkout) cargo build --profile web -p nsh-platform-web --target wasm32-unknown-unknown # nsh run --target web (built for you in a checkout) ``` For `nsh package`, build the release runners as well: ```sh cargo build -p nsh-ios-app --target aarch64-apple-ios-sim # nsh package --target ios-simulator cargo build -p nsh-ios-app --target aarch64-apple-ios # nsh package --target ios cargo xtask android # nsh package --target android cargo build -p nsh-macos-app # nsh package --target macos cargo build -p nsh-linux-app # nsh package --target linux cargo build -p nsh-windows-app # nsh package --target windows # nsh package --target web uses the same web runtime as nsh run --target web ``` A CLI built from a checkout looks for the runtimes in `target//` (the macOS, Linux, Windows and GPUI ones, built for this machine, in `target/`) and `target/android/[-dev]`, using its own profile: `./target/debug/nsh` uses debug runtimes, `./target/release/nsh` release ones. Build both with the same profile (add `--release` to all of them for faster apps). > [!TIP] > `NSH_RUNTIME=/path/to/runtime` makes `nsh` use a runtime directory laid out like the distribution's `runtime/`, wherever the CLI itself lives. ## Platform tools ### iOS and Mac Catalyst - **Xcode** (with its command line tools). `nsh run` uses `xcrun simctl` to boot simulators and install the development host; `nsh package` uses `actool` for app icons and `codesign` for signing. - An iOS simulator runtime. If none is installed, install one with `xcodebuild -downloadPlatform iOS`. - Apps target iOS 15.0 and later. ### Android - **The Android SDK**, found through `ANDROID_HOME`, `ANDROID_SDK_ROOT` or Android Studio's default place (`~/Library/Android/sdk`, `%LOCALAPPDATA%\Android\Sdk` on Windows), with: - `platforms;android-35` and a recent `build-tools` (NSH uses `aapt2`, `zipalign` and `apksigner` from it, no Gradle); - `platform-tools` (`adb`). - **A JDK** (`keytool` creates the debug keystore in `~/.android/debug.keystore` the first time you package, `apksigner` needs Java). - A running emulator or a device connected over `adb`. Apps need Android 9 (API 28) or later. ```sh sdkmanager "platforms;android-35" "build-tools;35.0.1" "platform-tools" ``` ### macOS - **Xcode's command line tools** (`xcode-select --install`): the SDK and clang to build the runtime, `codesign` and `sips` to package apps. - Apps need macOS 14 or later. See [macOS](https://nsh.dev/versions/v0.2.0/docs/macos.md). ### Linux - **GTK 4.14** or later, with their development packages and clang to build the runtime: `libgtk-4-dev clang` (Debian, Ubuntu 24.04 or later), `gtk4 clang` (Arch), `gtk4-devel clang` (Fedora). - **WebKitGTK 6.0** (2.40 or later) for web views: `libwebkitgtk-6.0-dev` (Debian, Ubuntu), `webkitgtk-6.0` (Arch), `webkitgtk6.0-devel` (Fedora) to build the runtime; apps then need it where they run. Without it, `cargo xtask dist` builds the runtime with placeholders for web views. - **GStreamer** plugins for the video formats apps play. See [Linux](https://nsh.dev/versions/v0.2.0/docs/linux.md). ### Windows - **Windows 10 1809** or later, or Windows 11. The WebView2 Runtime (part of Windows 11, installed with Edge on Windows 10) for web views. - To build the runtime: **Visual Studio Build Tools** with the "Desktop development with C++" workload and the Windows SDK, and **LLVM** (libclang, for QuickJS' bindings; found in `C:\Program Files\LLVM`, else set `LIBCLANG_PATH`). See [Windows](https://nsh.dev/versions/v0.2.0/docs/windows.md). ### Building the Android runtime Building the Android runtime from a checkout (`cargo xtask android`) additionally needs an **NDK** from the SDK (`ndk;`; set `ANDROID_NDK_HOME` if the newest one installed is incomplete) and `javac`. ## What works where | Command | macOS | Linux | Windows | |---|---|---|---| | `nsh init`, `nsh build`, `nsh test`, `nsh dev`, `nsh check` | Yes | Yes | Yes | | `nsh run --target catalyst` (Mac Catalyst) | Yes | No | No | | `nsh run --target macos`, `nsh package --target macos` | Yes | No | No | | `nsh run --target linux` (the default on Linux), `nsh package --target linux` | No | Yes | No | | `nsh run --target windows` (the default on Windows), `nsh package --target windows` | No | No | Yes | | `nsh run --target gpui` (the backend GPUI apps embed) | Yes | Yes | Yes | | `nsh run --target ios`, `nsh package --target ios` / `ios-simulator` | Yes, with Xcode | No | No | | `nsh run --target android`, `nsh package --target android` | Yes | Yes, from a checkout | Yes, with the Android runtime | | `nsh keygen`, `certify`, `publish`, `verify`, `revoke` | Yes | Yes | Yes | On Linux, build the CLI and the Android runtime from a checkout (`cargo build -p nsh-cli` and `cargo xtask android --dev`); the NDK's Linux toolchain is used automatically. On Windows the NDK's Windows toolchain is used the same way, and `cargo xtask dist` includes the Android runtime when it finds the Android SDK. ## Editor setup A Rust app is a Cargo project: rust-analyzer (VS Code, Zed, Neovim, RustRover) checks it and completes NSH's API, with its doc comments, as for any crate. For TSX, NSH needs no NPM. `nsh init` writes a `tsconfig.json` and the type declarations of everything NSH provides into `.nsh/types`, and every command that builds the app keeps them up to date. Any editor that uses the TypeScript language server (VS Code, Zed, Neovim, WebStorm) type-checks the app with them. See [Project structure](https://nsh.dev/versions/v0.2.0/docs/project-structure.md#tsconfigjson-and-nshtypes). ## Next steps Continue with [Getting started](https://nsh.dev/versions/v0.2.0/docs/getting-started.md) to create an app and run it. Source: https://nsh.dev/versions/v0.2.0/docs/installation --- # Getting started This guide creates an NSH app, runs it on the platforms you target, and ships it. NSH apps are written in Rust or TSX, on the same engine: Rust compiles your app into native code with no JavaScript VM, and TSX runs on QuickJS with hot updates, as modules a native app can load at run time. The guide follows a Rust app first, then the same app in TSX. It assumes `nsh` is installed and on your `PATH` ([Installation](https://nsh.dev/versions/v0.2.0/docs/installation.md)); in a checkout, use `./target/debug/nsh` instead. > [!TIP] > No app project at hand? The first two sections need none: `nsh run` shows the app in a window on your computer, in > the iOS simulator, on an Android device or in the browser. You can stop there, or [package it as a standalone > app](#or-ship-it-as-a-standalone-app). ## Create an app On a terminal, `nsh init` asks what the app is written in: `rust`, `ts` or `mixed` (TSX with a Rust crate of its own). `--template` chooses without asking. ### In Rust ```sh nsh init my-app --template rust ``` `nsh init` creates the directory and writes a Cargo project: | File | What it is | |---|---| | `src/lib.rs` | The app: its root component, state and handlers | | `src/main.rs` | Mounts the same root on macOS, Windows or Linux for `cargo run` | | `Cargo.toml` | The crate, depending on `nsh-core` and `nsh-base` at the CLI's version | | `nsh.json` | The app's name, id, version and build number, its crate (`"rust": "."`) and root component (`"root": "App"`) ([Project structure](https://nsh.dev/versions/v0.2.0/docs/project-structure.md#nshjson)) | | `App.test.tsx` | The root's test, in TypeScript (`nsh test`), with `tsconfig.json` and `.nsh/types` for your editor | | `README.md` | How to run, test and package the app, and how to add a TSX entry later | | `.gitignore` | Ignores `.nsh/` and `target/` | Rust development needs a Rust toolchain ([rustup](https://rustup.rs)); the `nsh` commands build the app's runners from NSH's crates on crates.io, at the CLI's version. ### In TSX ```sh nsh init my-app --template ts ``` A TSX app is one or more `.tsx` files with an entry: what `nsh run` runs, and what a host app loads as a module. | File | What it is | |---|---| | `App.tsx` | The entry module: renders the app with `mount` | | `App.test.tsx` | Its test (`nsh test`) | | `nsh.json` | The app's name, id, version and build number ([Project structure](https://nsh.dev/versions/v0.2.0/docs/project-structure.md#nshjson)) | | `tsconfig.json` | Extends `.nsh/tsconfig.json`, NSH's TypeScript settings, which resolve `nsh` to the declarations in `.nsh/types` | | `.gitignore` | Ignores `.nsh/` and `target/` | | `.nsh/types/` | Generated declarations of every NSH module, for your editor | Files that already exist are kept, so you can run it in a directory that has some of them; a Rust app is created in a new directory. For both, the display name defaults to the directory's name; pass `--name "My App"` to choose another. The id becomes `com.example.`: change it in `nsh.json` before you ship. `nsh init my-app --template mixed` creates a TSX app that is also a Rust crate (`Cargo.toml` and `src/lib.rs` beside `App.tsx`), whose component, module and class `App.tsx` imports ([Building with Rust](https://nsh.dev/versions/v0.2.0/docs/rust.md)). ### The root component The two templates make the same app, a counter: ```rust // src/lib.rs use nsh_base::{safe_area_view, theme}; use nsh_core::ui::prelude::*; /// The crate's UI set: what a TSX entry may import from `my-app/native`. #[native_module("my-app/native")] fn native(registry: &mut Registry) { registry.register::(); } /// The application's root component. #[component] pub fn App() -> impl IntoElement { let count = signal(0_u32); view() .flex_1() .bg(move || theme().background) .text_color(move || theme().foreground) .child( safe_area_view() .flex_1() .child( view() .flex_1() .items_center() .justify_center() .gap_4() .child(text(move || format!("Count: {}", count.get())).text_size(px(28.0))) .child( view() .px_5() .py_3() .rounded(px(10.0)) .bg(move || theme().primary) .active(|s| s.opacity(0.6)) .on_press(move || count.update(|value| *value += 1)) .child( text("Tap") .text_size(px(17.0)) .text_color(move || theme().primary_foreground), ), ), ), ) } ``` ```tsx // App.tsx import { mount, signal, Text, View } from "nsh"; import { SafeAreaView } from "nsh/base"; function App() { const count = signal(0); return ( {count()} count.update((c) => c + 1)} role="button" class="rounded-(--radius) bg-primary px-5 py-3 active:bg-primary/90" > Tap ); } mount(App); ``` In Rust: - **`use nsh_core::ui::prelude::*`** brings the primitives (`view()`, `text()`, `scroll()`, `input()`, `image()`, ...), [signals](https://nsh.dev/versions/v0.2.0/docs/reactivity.md) and the typed style methods; **`nsh_base`** has the unstyled base components ([`safe_area_view()`](https://nsh.dev/versions/v0.2.0/modules/nsh-base.md#safeareaview), `button()`, the keyboard, dialogs) and `theme()`, the colors and radii the app's components draw with ([Theme](https://nsh.dev/versions/v0.2.0/docs/rust.md#theme)). - **`#[component] pub fn App()`** is a component: a function that runs **once** and returns its elements. `nsh.json`'s `"root": "App"` names it, and `nsh run` mounts it. `signal(0_u32)` makes a value that changes over time. - **`text(move || format!(…, count.get()))`** binds the text to the signal: when `count` changes, NSH runs this closure again and updates this one text, nothing else. Styles take closures the same way: `.bg(move || theme().primary)` follows the theme into dark mode. - **`.on_press(..)`** makes the view answer presses; `.active(|s| s.opacity(0.6))` is its style while pressed, applied natively. The button is the app's own: NSH provides the primitives and the capabilities, and the app draws its UI from them. - **`.px_5()`, `.rounded(px(10.0))`, `.gap_4()`** are typed style methods named as Tailwind's utilities are, in points (`px`). Layout is flexbox, and children stack in a column by default ([Layout](https://nsh.dev/versions/v0.2.0/docs/layout.md)). - **`#[native_module("my-app/native")]`** names what a TSX entry may import from the crate, for when you add TSX screens on top ([Building with Rust](https://nsh.dev/versions/v0.2.0/docs/rust.md#extending-a-rust-app-with-tsx)). In TSX: - **`import { mount, signal, Text, View } from "nsh"`**: `nsh` is the core module: signals, `mount`, the other reactive primitives, and the host elements. It is served by the native runtime, not bundled. **`nsh/base`** has the base components, as `nsh_base` does in Rust. - **`function App()`** is a component that runs **once**. **`{count()}`** reads the signal inside JSX: the compiler sees a call and makes this text follow the signal. - **``** is a native view that reacts to presses; `active:` classes are its style while pressed. `View`, `Text`, `Image`, `Scroll`, `Input` and the other [host elements](https://nsh.dev/versions/v0.2.0/elements.md) are imported from `nsh`; only `If` and `Each` need no import. - **`class`** takes [Tailwind](https://nsh.dev/versions/v0.2.0/docs/tailwind.md) classes; `style={{ … }}` takes an object of camelCase style properties, with numbers in points. `bg-background`, `bg-primary`, `text-primary-foreground` and `rounded-(--radius)` name the tokens of the app's theme ([Theme tokens](https://nsh.dev/versions/v0.2.0/docs/tailwind.md#theme-tokens)): they follow dark mode by themselves, and `"theme"` in `nsh.json` restyles them. - **`mount(App)`** renders the component into the app's surface. > [!IMPORTANT] > Text shows in a text element: `text(..)` in Rust, `` in TSX. In TSX, text written straight in a `` > (`Hello`) is a build error that points at it, and a string an expression puts there > (`{name}`) is reported in the log. ## Develop it ### Run it `nsh run` builds the app, launches it and rebuilds while you edit; the app's logs appear in the terminal. Give it the app's directory: it runs a Rust app's root, or a TSX app's `App.tsx` (`nsh run my-app/App.tsx` names another entry). For a Rust app, `nsh run` builds the app's own runner for the target, with your crate and the root mounted and no QuickJS, and starts it; a change to the Rust code builds it again and restarts the app. `cargo run` in the app's directory starts the same root straight on the desktop backend. For a TSX app, `nsh run` starts a dev server and a development host that loads the app from it, and patches the running app on every save. Without `--target`, `nsh run` uses the machine you are on: an AppKit window on macOS, a Win32 window on Windows and a GTK window on Linux. Pick another platform with `--target`: | Target | Command | Runs from | |---|---|---| | iOS simulator | `nsh run my-app --target ios` | macOS with Xcode | | Android | `nsh run my-app --target android` | macOS, Windows, Linux | | macOS (AppKit) | `nsh run my-app` | macOS (the default) | | Windows | `nsh run my-app` | Windows (the default) | | Linux (GTK 4) | `nsh run my-app` | Linux (the default) | | Web | `nsh run my-app --target web` | macOS, Windows, Linux | | Mac Catalyst | `nsh run my-app --target catalyst` | macOS | #### iOS simulator ```sh nsh run my-app --target ios nsh run my-app --target ios --device "iPhone 16 Pro" ``` `nsh` uses a booted simulator, or boots the newest iPhone if none is running, and installs the app (a Rust app's runner, or the development host) into it. `--device` picks a simulator by name or UDID. #### Android ```sh nsh run my-app --target android ``` Start an emulator or connect a device first (`adb devices` lists them; NSH uses the first one). `nsh` installs the app on it: a Rust app's own `libnsh.so`, or a development host APK (rebuilt only when the runtime changes) whose dev server port is forwarded to the device with `adb reverse`, so it works over USB without a shared network. #### macOS (AppKit) ```sh nsh run my-app --target macos ``` The app opens as a desktop Mac app (AppKit, in a resizable window), with mouse, trackpad and keyboard input instead of touches. It needs macOS 14 or later; see [macOS](https://nsh.dev/versions/v0.2.0/docs/macos.md). #### Windows (default on Windows) ```sh nsh run my-app ``` The app opens in a desktop window (1200×800 points), with mouse, touchpad, touch and keyboard input. It needs Windows 10 1809 or later; see [Windows](https://nsh.dev/versions/v0.2.0/docs/windows.md). #### Linux (default on Linux) ```sh nsh run my-app ``` The app opens in a GTK window (390×844 points). It needs GTK 4.14 or later; see [Linux](https://nsh.dev/versions/v0.2.0/docs/linux.md). #### Web ```sh nsh run my-app --target web ``` `nsh` prints the page's address; open it in a browser. A Rust app runs as WebAssembly with your crate, and the open page loads the new build by itself after a change; see [Web](https://nsh.dev/versions/v0.2.0/docs/web.md). #### Mac Catalyst ```sh nsh run my-app --target catalyst ``` The app opens in an iPhone-sized window (390×844 points) on your Mac. This is the quickest way to see an app as a phone shows it: no simulator to boot. It uses UIKit like iOS, so what you see is what an iPhone shows, apart from Mac-specific behavior such as scrolling with a mouse wheel. For the Mac app itself, use `--target macos`. ### Edit it Leave `nsh run` running and change the button's label: ```rust text("Add one") .text_size(px(17.0)) .text_color(move || theme().primary_foreground) ``` ```tsx Add one ``` Save. A Rust app is built again and restarts with the new label; the count starts over. A TSX app gets a hot update: the CLI rebuilds and sends a patch, and the host runs the changed module again and renders again only the components whose code changed. Here that is `App`, which owns the `count` signal, so the count starts over too. Pieces of UI move into components of their own, or plain functions that return elements: ```rust /// The app's button: a view that answers presses. fn tap_button(on_press: impl Fn() + 'static) -> impl IntoElement { view() .p_3() .rounded(px(10.0)) .bg(move || theme().primary) .active(|s| s.opacity(0.6)) .on_press(on_press) .child(text("Tap").text_size(px(17.0)).text_color(move || theme().primary_foreground)) } // In App: .child(tap_button(move || count.update(|value| *value += 1))) ``` ```tsx function TapButton(props: { onPress: () => void }) { return ( Tap ); } // In App: count.update((c) => c + 1)} /> ``` In TSX, state survives when you edit a component that does not own it: tap a few times, then change `TapButton`'s color, and the button updates while the count stays. Hot replacement works per top-level component: a function with a capitalized name whose body contains JSX. A component renders again when its own code changes, or when the module's code outside components changes (a constant it uses, a helper). If an edit cannot be applied in place (for example, it imports an NSH module the running app did not import yet, or changes `nsh.json`'s capabilities), the host reloads the whole app. #### Errors A Rust compile error is printed in the terminal with cargo's output, and `nsh run` builds again with the next save. A TSX build error is printed in the terminal and shown on the device as a red overlay with the file, line and column; the app keeps running the last good build. The overlay disappears with the next successful build (tap it to dismiss it earlier). An exception thrown at run time is shown the same way, with its stack mapped back to your `.tsx` source. ## Host it in your app A native app shows NSH in a view of its own: `NshAppView` (a `UIView`) on iOS, `NshView` (an Android `View`) on Android, `AppView` on macOS, Windows, Linux and in GPUI apps. Place it like any other view, on any screen, as many times as you need. A Rust root is part of the app's build: the app links your crate and mounts the root with `AppSource::Rust` on the `AppView` of macOS, Windows, Linux, GPUI and iOS ([Building with Rust](https://nsh.dev/versions/v0.2.0/docs/rust.md#an-app-written-in-rust)). A TSX app is a module the view loads: during development from the dev server, in a release from a built bundle or a downloaded, signed package. The sections below load TSX modules. ### iOS Link `NativeShellDev.xcframework` in Debug and `NativeShell.xcframework` in Release ([Embedding in iOS apps](https://nsh.dev/versions/v0.2.0/docs/ios.md)), then: ```swift import NativeShell let app = NshAppView() app.onError = { message in print("nsh error: \(message)") } view.addSubview(app) // and constrain it like any view #if DEBUG try app.connectDevServer(URL(string: "ws://127.0.0.1:8790/ws")!) // `nsh dev my-app` on your Mac #else try app.loadBundle(at: Bundle.main.url(forResource: "bundle", withExtension: "nshc", subdirectory: "nsh")!) #endif ``` `nsh dev my-app` serves the module to the app and pushes every save, as `nsh run` does with its own host. For the release build, `nsh build my-app --release --out MyApp/nsh` writes the bundle into a folder you add to the app's resources. ### Android Add `nsh-dev.aar` for debug builds and `nsh.aar` for release builds ([Embedding in Android apps](https://nsh.dev/versions/v0.2.0/docs/android.md)), then: ```kotlin val nsh = NshView(this) nsh.setErrorListener { message -> Log.e("nsh", message) } setContentView(nsh) // or add it to any layout if (BuildConfig.DEBUG) { nsh.connectDevServer("ws://127.0.0.1:8790/ws") // with `adb reverse tcp:8790 tcp:8790` } else { nsh.loadAssetBundle("nsh") // assets/nsh, from `nsh build my-app --release --out app/src/main/assets/nsh` } ``` ### macOS, Windows and Linux Desktop apps written in Rust embed NSH with `AppView`, from the `nsh-platform-macos`, `nsh-platform-windows`, `nsh-platform-linux` and `nsh-platform-gpui` crates: an `NSView`, a child window, a GTK widget or a GPUI view that mounts a Rust root or loads a built bundle. See [macOS](https://nsh.dev/versions/v0.2.0/docs/macos.md#embedding-in-a-mac-app), [Windows](https://nsh.dev/versions/v0.2.0/docs/windows.md#embedding-in-a-win32-app), [Linux](https://nsh.dev/versions/v0.2.0/docs/linux.md#embedding-in-a-gtk-app) and [GPUI](https://nsh.dev/versions/v0.2.0/docs/gpui.md), and [Platforms](https://nsh.dev/versions/v0.2.0/docs/platforms.md#embed-in-a-native-app) for every host side by side. ### What the module may do On iOS and Android, the host decides before loading what a module may do: `grantCapabilities` sets the network hosts and other capabilities it may reach, and limits cap its memory, call time and node count ([Sandbox and capabilities](https://nsh.dev/versions/v0.2.0/docs/sandbox-and-capabilities.md)). To give modules your own native views (a map, a chart, a design-system component), register them as [native components](https://nsh.dev/versions/v0.2.0/docs/native-components.md). ## Ship updates as signed packages A TSX module the app downloads after release comes as a signed package. Create a root key once and keep it offline, certify a release key with it, and publish: ```sh nsh keygen keys/root && nsh keygen keys/release nsh certify --root keys/root.secret --key keys/release.pub --apps 'com.example.*' -o keys/release.cert nsh publish my-app --key keys/release.secret --cert keys/release.cert --build 2 ``` The app trusts the root's public key and loads what it downloads; NSH verifies the signature, the certificate and the revocation list before running anything: ```swift let support = FileManager.default.urls(for: .applicationSupportDirectory, in: .userDomainMask)[0] let trust = try NshTrust(stateDirectory: support.appendingPathComponent("nsh")) try trust.addRoot(rootPublicKey) // the contents of keys/root.pub let info = try await app.loadPackage(at: downloadedFile, trust: trust) ``` Android has the same flow with `NshTrust` and `NshPackage`. Hosting, delivery and revocation in detail: [Signed packages](https://nsh.dev/versions/v0.2.0/docs/signed-packages.md). ## Or ship it as a standalone app `nsh package` builds an installable app, Rust or TSX, with no Xcode project or Gradle build of your own: ```sh nsh package my-app --target ios-simulator --run nsh package my-app --target android nsh package my-app --target macos nsh package my-app --target linux nsh package my-app --target windows nsh package my-app --target web ``` A Rust app is packaged with its own runner, built from your crate and NSH's crates, its root mounted; a TSX app with the prebuilt runtime. Options, signing and icons: [Packaging](https://nsh.dev/versions/v0.2.0/docs/packaging.md). ## Try it in the terminal `nsh test` runs an app headless, with no device or window. Given a Rust app's `nsh.json` or a TSX app's entry, it opens the app, lets it run and prints what it mounts; with `--trace`, also the native view operations and the time of every commit ([Testing](https://nsh.dev/versions/v0.2.0/docs/testing-and-trace.md)): ```sh nsh test my-app/nsh.json --trace # a Rust app's root nsh test my-app/App.tsx --trace # a TSX app's entry ``` Given a directory, it runs the app's tests: each template's `App.test.tsx` opens the app, presses its button and checks what it shows (`nsh test my-app`). A Rust app's opens its root (`open(".")`); it can also be tested in `cargo test` with `nsh_testing::Headless` ([Building with Rust](https://nsh.dev/versions/v0.2.0/docs/rust.md#testing)). ## Next steps - [Building with Rust](https://nsh.dev/versions/v0.2.0/docs/rust.md): Rust apps, their theme, and TSX on top of a Rust crate. - [JSX and components](https://nsh.dev/versions/v0.2.0/docs/jsx-and-components.md) and [Reactivity](https://nsh.dev/versions/v0.2.0/docs/reactivity.md): components and signals, in Rust and TSX. - [Platforms](https://nsh.dev/versions/v0.2.0/docs/platforms.md): what each platform supports, and its own page ([iOS](https://nsh.dev/versions/v0.2.0/docs/ios.md), [Android](https://nsh.dev/versions/v0.2.0/docs/android.md), [macOS](https://nsh.dev/versions/v0.2.0/docs/macos.md), [Windows](https://nsh.dev/versions/v0.2.0/docs/windows.md), [Linux](https://nsh.dev/versions/v0.2.0/docs/linux.md)). - [Signed packages](https://nsh.dev/versions/v0.2.0/docs/signed-packages.md) and [Sandbox and capabilities](https://nsh.dev/versions/v0.2.0/docs/sandbox-and-capabilities.md): delivery and what modules may do. - [Project structure](https://nsh.dev/versions/v0.2.0/docs/project-structure.md): what goes in `nsh.json`, assets, files. Source: https://nsh.dev/versions/v0.2.0/docs/getting-started --- # Project structure An NSH app is a directory with an `nsh.json` and its code: a Rust crate whose root component `nsh.json` names, or a TSX entry module, or both. Everything else (more modules, images, fonts, JSON files) is found from there: a crate's modules and its `assets/` directory, or the files the TSX entry imports, so there is no file list to maintain. An app written in Rust is a Cargo project: ```text my-app/ ├── nsh.json name, id, version, icon, the crate ("rust") and its root component ("root"), theme ├── Cargo.toml the crate: nsh-core, nsh-base and your dependencies ├── src/ │ ├── lib.rs the root component and the crate's UI set (#[native_module]) │ ├── main.rs cargo run: the root on this machine's desktop backend │ └── screens/ your own modules (any layout you like) ├── assets/ files the crate ships: asset!("logo@2x.png"), fonts ├── .gitignore .nsh/ and target/ ├── .nsh/ generated: the runners' sources, test entries, build output (.nsh/out) └── target/ Cargo's build directory ``` A TSX app is an entry and the modules it imports: ```text my-app/ ├── App.tsx the entry module: calls mount() ├── App.test.tsx its tests (nsh test) ├── nsh.json name, id, version, icon, Tailwind theme, capabilities ├── tsconfig.json TypeScript settings for editors ├── .gitignore .nsh/ and target/ ├── components/ your own modules (any layout you like) │ └── Header.tsx ├── assets/ │ ├── logo@2x.png imported images │ └── Inter.ttf imported fonts └── .nsh/ generated: type declarations, development host builds, test entries, build output (.nsh/out) ``` An app with both lists its crates in `"rust"` and has a TSX entry that imports them ([Building with Rust](https://nsh.dev/versions/v0.2.0/docs/rust.md#extending-a-rust-app-with-tsx)). Every command takes the app's directory or its entry as its argument (`nsh run my-app`, `nsh run my-app/App.tsx`). The directory of `nsh.json` and the entry is the app's root: `nsh.json` is read from there, asset paths are relative to it, and `.nsh/` is written there. In a Rust app (`nsh.json` `"root"`), the directory runs the root component; naming a TSX entry runs that instead. ## The root component A Rust app's root is a component of its crate, which `nsh.json`'s `"root"` names by its path in the crate (`App`, `shell::Gallery`). A TSX entry is a normal module that ends by rendering the root component: ```rust // src/lib.rs; nsh.json: { "rust": ".", "root": "App" } mod screens; use nsh_core::ui::prelude::*; /// What a TSX entry may import from the crate. #[native_module("my-app/native")] fn native(registry: &mut Registry) { registry.register::().assets(assets!()); } #[component] pub fn App() -> impl IntoElement { screens::home() } ``` ```tsx // App.tsx import { mount } from "nsh"; import { Home } from "./components/Home"; function App() { return ; } mount(App); ``` `nsh run` and `nsh package` mount a Rust root on every platform, with no QuickJS. In TSX, `mount(component, props?)` renders the component into the app's surface and returns a function that removes it again; an app calls it once. Code at the top level of the entry runs when the bundle loads, before the first frame. ## nsh.json `nsh.json` sits in the app's root. `nsh init` writes `name`, `id`, `version` and `build`, and for a Rust app the crate and its root component: ```json { "name": "Gallery", "id": "com.example.gallery", "version": "1.0.0", "build": 1, "rust": ".", "root": "App", "theme": "theme.json" } ``` Every other key is optional; a TSX app may also leave out the four. A TSX app's `nsh.json` might read: ```json { "name": "Gallery", "id": "com.example.gallery", "version": "1.0.0", "build": 1, "icon": "icon.png", "tailwind": { "colors": { "brand": { "500": "#7c3aed", "600": "#6d28d9" } }, "radius": { "card": 14 } }, "capabilities": { "network": { "hosts": ["api.example.com", "*.cdn.example.com"] } } } ``` | Key | Type | Used by | Description | |---|---|---|---| | `name` | string | `package` | The display name under the app icon. Default: the entry directory's name. | | `id` | string | `package`, `publish`, `nsh/storage` | Bundle identifier / application id in reverse-DNS form, e.g. `com.yourcompany.app`; required by `package` and `publish`. `nsh run` uses `com.example.` without one. | | `version` | string | `package`, `publish` | The user-visible version, e.g. `1.2.0` (`CFBundleShortVersionString`, Android `versionName`). | | `build` | number | `package`, `publish` | The build number (`CFBundleVersion`, Android `versionCode`). Signed packages require it to grow with every release. | | `minRuntime` | string | `publish` | The lowest NSH runtime that runs the signed package, e.g. `0.2.0`. Default: the CLI's version ([Signed packages](https://nsh.dev/versions/v0.2.0/docs/signed-packages.md#which-runtimes-run-a-package)). | | `icon` | string | `package` | A square PNG, ideally 1024×1024, relative to `nsh.json`. | | `tailwind` | object | every build | Extends the Tailwind theme your `class` strings resolve against ([Tailwind](https://nsh.dev/versions/v0.2.0/docs/tailwind.md#theme)). | | `appearance` | string | every build, `package` | `"light"`, `"dark"` or `"system"` (the default): an app that is always light or dark says so here. `colorScheme()`, `dark:` classes, the components' theme and the platform's controls follow it from the start ([Device and environment](https://nsh.dev/versions/v0.2.0/docs/device-and-environment.md#dark-mode)). | | `theme` | string or object | every build | The app's design tokens (shadcn/ui's `cssVars`, over the default `neutral` theme), which token classes, `token()` and Rust components draw with: a theme file's path relative to `nsh.json`, or the theme inline ([Building with Rust](https://nsh.dev/versions/v0.2.0/docs/rust.md#theme), [Theme tokens](https://nsh.dev/versions/v0.2.0/docs/tailwind.md#theme-tokens)). | | `statusBar` | object | every build, `package` | The status bar's default under every page and dialog (iOS and Android): `{ "foreground": "auto" \| "light" \| "dark", "hidden": boolean }`, both optional. Packaged iOS apps show it from the launch screen on ([Status bar](https://nsh.dev/versions/v0.2.0/modules/nsh-base.md#status-bar)). | | `capabilities` | object | every build | What the app may reach outside its VM: network hosts, web view hosts, the status bar ([Sandbox and capabilities](https://nsh.dev/versions/v0.2.0/docs/sandbox-and-capabilities.md)). | | `resolve` | object | every build | How bare imports resolve: the export conditions of npm packages and aliases ([npm packages](#npm-packages)). | | `rust` | string or string[] | every command | The app's Rust crate, or a list of them, relative to `nsh.json`: their components and modules, which TSX imports by each crate's `#[native_module]` specifier ([Building with Rust](https://nsh.dev/versions/v0.2.0/docs/rust.md#several-crates)). | | `root` | string | `run`, `package` | A Rust app's root component, its path in the `rust` crate, the first one of a list (`App`, `shell::Gallery`): `nsh run` and `nsh package` of the app's directory mount it with no QuickJS, on every platform; `nsh run App.tsx` still runs the TSX entry ([Building with Rust](https://nsh.dev/versions/v0.2.0/docs/rust.md)). | The flags of `nsh package` and `nsh publish` (`--name`, `--id`, `--app-version`, `--build`, `--icon`, `--min-runtime`) override the file's values ([CLI](https://nsh.dev/versions/v0.2.0/docs/cli.md)). `tailwind`, `theme`, `appearance`, `statusBar`, `capabilities` and `resolve` are checked when the app is built. A typo is a build error rather than a setting that silently does nothing: ```text error building App.tsx failed nsh.json: tailwind: unknown theme key `colours` ``` ### capabilities Without a `capabilities` key, an app may use the network, web views and the status bar freely: a standalone app is its developer's own. With the key, everything not listed is denied, both in development and in packaged apps: ```json "capabilities": { "network": { "hosts": ["api.example.com", "*.cdn.example.com"], "http": [{ "host": "quotes.example.com", "methods": ["GET"], "pathPrefix": "/v1/" }] }, "webview": { "hosts": ["*.example.com"] }, "statusBar": true } ``` `network` covers `fetch`, remote images and video sources; local assets are always allowed. An app that embeds NSH may grant less than an app declares. The details are in [Sandbox and capabilities](https://nsh.dev/versions/v0.2.0/docs/sandbox-and-capabilities.md). ## tsconfig.json and .nsh/types NSH does not need `node_modules`. Instead, every command that builds the app (`init`, `build`, `test`, `dev`, `run`, `package`, `publish`, `check`) writes the TypeScript declarations of everything NSH provides into `.nsh/types`, and NSH's TypeScript settings into `.nsh/tsconfig.json`. The app's `tsconfig.json` extends them: ```json { "extends": "./.nsh/tsconfig.json", "include": ["**/*.ts", "**/*.tsx"] } ``` `.nsh/tsconfig.json` points the `nsh` imports and your app's crates to `.nsh/types`, includes the globals, asset and token declarations, and sets the compiler options NSH apps use (`lib`, `jsx`, `strict`, ...). NSH keeps it current: when a new version adds a library or a path, your app has it with the next build, and your `tsconfig.json` stays as you wrote it. Options you add to `tsconfig.json` apply over NSH's. | File in `.nsh/types/nsh/` | Declares | |---|---| | `index.d.ts` | `nsh`: `signal`, `computed`, `effect`, `mount`, `View`, `Text`, ... and re-exports the types below | | `types.d.ts` | Styles (`ViewStyle`, `TextStyle`, ...), every element's props and events, `YNode` (what a `ref` receives) | | `jsx-runtime.d.ts` | The JSX runtime TypeScript resolves through `jsxImportSource` (the `JSX` namespace) | | `globals.d.ts` | `If` and `Each`; `console`, timers, `requestAnimationFrame`, `fetch` and the other [global APIs](https://nsh.dev/versions/v0.2.0/modules/globals.md) | | `assets.d.ts` | What importing an image or font gives | | `tokens.d.ts` | `Tokens`: the app's design tokens, from its `"theme"`, which type [`token()`](https://nsh.dev/versions/v0.2.0/modules/nsh.md#token) | | `animated.d.ts`, `device.d.ts`, ... | One file per module (`nsh/animated`, `nsh/device`, ...) | | `base.d.ts` | `nsh/base` | `lib` is `ES2023` without `DOM`: there is no `document` or `window`. The files in `.nsh` are regenerated (only when their contents change), so do not edit them and do not commit them; `.gitignore` already excludes `.nsh/`. If your editor shows `nsh` as missing after cloning a project, run any build command once, e.g. `nsh check`. `.nsh/tsconfig.json` adds `"ESNext.Disposable"`, which declares `using` declarations and `Symbol.dispose`, which the runtime has: `using account = new Account()` frees a [Rust class](https://nsh.dev/versions/v0.2.0/docs/rust.md)'s value at the end of the block. Its `jsxImportSource: "nsh"` and `jsx: "preserve"` only matter to the editor: NSH compiles JSX with its own compiler, whatever the `tsconfig.json` says. TypeScript is checked by your editor or by `nsh check`; the other commands strip types without checking them. An app made by an earlier `nsh init` gets the new `tsconfig.json` with its next build when its own is NSH's template, unchanged; NSH prints a line saying so. A `tsconfig.json` you changed is yours: `nsh check` points out when it does not extend `./.nsh/tsconfig.json`, and you add the `extends` line and remove the options it sets. ## Modules A Rust app's code is its crate's modules (`mod screens;`), and its dependencies come from crates.io like any crate's; NSH's own crates are `nsh-core` (the primitives, signals and typed styles, `use nsh_core::ui::prelude::*`) and `nsh-base` (the base components and the theme). In TSX, split the app into as many `.ts` and `.tsx` modules as you like and import them with relative paths; the extension may be left out: ```tsx import { Header } from "./components/Header"; import { formatPrice } from "./lib/format"; import strings from "./strings.json"; ``` All of them are bundled into one script. JSON files import as their parsed value. ### Loading code on demand `import()` of one of the app's modules returns a promise of its exports, as in a browser: ```tsx const open = async () => { const { Settings } = await import("./screens/Settings"); screen.set(() => Settings); }; ``` Apps on devices ship one bundle, so the module is already there and the promise resolves at once. A web app published split (`nsh build --split`) gets the module, and what only it uses, as a separate file that is downloaded the first time `import()` asks for it ([Web](https://nsh.dev/versions/v0.2.0/docs/web.md#split-bundles)). The specifier must be a literal relative path: the bundler has to see which module is meant. ### NSH's modules Imports of `nsh` and `nsh/...` are not bundled: the runtime provides them natively. | Module | What it has | |---|---| | `nsh` | Reactivity (`signal`, `computed`, `effect`, ...), `mount`, `split`, `context`; the primitives `View`, `Text`, `Scroll`, `Input`, `Image`, `List`, `Canvas`, `WebView`, `Video` ([reference](https://nsh.dev/versions/v0.2.0/modules/nsh.md)) | | `nsh/base` | The unstyled foundation of an app's UI: navigation stacks and their pages (`NavStack`, `NavPage`, `createNavStack`), dialogs (`Dialog`), a press target (`Button`), safe areas (`SafeAreaView`, `safeArea`, `safeAreaInsets`), the system's alert dialog (`AlertDialog`), the software keyboard (`Keyboard`, `keyboardFrame`, `KeyboardAvoidingView`); pages and dialogs set the status bar (`statusBar`) ([reference](https://nsh.dev/versions/v0.2.0/modules/nsh-base.md)) | | `nsh/animated` | Native animations | | `nsh/gesture` | `PanResponder` | | `nsh/device` | `platform`, `windowSize`, `colorScheme`, `fontScale`, `pixelRatio`, `locale` | | `nsh/accessibility` | Screen reader and reduce motion state, announcements | | `nsh/native` | Components and functions the embedding app registers | | `nsh/ios`, `nsh/android` | Platform-only APIs | The [modules reference](https://nsh.dev/versions/v0.2.0/modules.md) documents each one. Importing a module NSH does not have (`nsh/foo`) is a build error. So is a name your code uses that the module does not export (`import { Buton } from "nsh/base"`): the error points at the import and names the closest export (``did you mean `Button`?``). Names used only as types are checked by `nsh check`. ### npm packages NSH needs nothing from npm, but the bundler resolves bare imports from a `node_modules` directory if you install packages with a package manager. Packages resolve their browser builds, because the runtime provides web APIs (`fetch`, streams, `URL`, `TextEncoder`, ...) and not Node's (`fs`, `Buffer`, `process`). Packages that touch the DOM do not work: there is none. A package whose browser build uses the DOM often has another build for workers or React Native. `resolve.conditions` picks the export conditions packages resolve with, in place of the default `["browser"]`: `import`, `require`, `module` and `default` always apply, and without `browser` packages resolve through their `module` and `main` fields. `resolve.alias` replaces an import: by a path relative to `nsh.json` (`./` or `../`), another package, or `false` for an empty module. TypeScript (`nsh check`, editors) types the import as written: a name that is not an installed package gets its types from a `.d.ts` file of the app. ```json "resolve": { "conditions": ["worker"], "alias": { "dom-only-helper": "./shims/helper.ts", "node-polyfill": false } } ``` ```ts // shims/aliases.d.ts declare module "node-polyfill"; ``` `solid-js` cannot be imported: the bundler refuses it and points to `nsh`'s own `signal`, `computed` and `effect`, which replace it. ## Assets Images and fonts ship with the app. A Rust crate reads each file of its `assets/` directory at compile time with `asset!`, and ships them all with `registry.assets(assets!())` in its `#[native_module]` function; TSX imports them like modules, and the bundler records each imported file (and an image's pixel-density variants). `nsh build` copies them to `.nsh/out/assets/`, `nsh package` puts them in the app, and during development they are served to the app. ```rust use nsh_core::ui::{FontAsset, ImageAsset, asset}; const LOGO: ImageAsset = asset!("logo@2x.png"); // its url, and its size: 64×64 points const INTER: FontAsset = asset!("Inter.ttf"); // its uri, and its family: "Inter" image(LOGO) text("Hello").font_family(INTER.family) ``` ```tsx import { Image, Text } from "nsh"; import logo from "./assets/logo@2x.png"; // { url: "asset:/assets/logo@2x.png", size: { width: 64, height: 64 } } import Inter from "./assets/Inter.ttf"; // "Inter" Hello ``` | Extension | Import gives | |---|---| | `.png`, `.jpg`, `.jpeg`, `.gif`, `.webp`, `.heic` | An `ImageAsset`: `{ url, size, scales? }`, sized in points. An `@2x` / `@3x` suffix divides the pixel size by 2 / 3. A file without a suffix also ships its `@1.5x`–`@4x` siblings, listed in `scales`, and the screen's variant is loaded ([pixel densities](https://nsh.dev/versions/v0.2.0/docs/images-and-assets.md#pixel-densities)). | | `.svg` | An `ImageAsset` sized in points by the SVG's `width` and `height` (or `viewBox`). | | `.ttf`, `.otf` | The font's family name, a string. The font is registered before the first frame. | | `.json` | The parsed JSON | See [Images and assets](https://nsh.dev/versions/v0.2.0/docs/images-and-assets.md) and [Fonts](https://nsh.dev/versions/v0.2.0/docs/fonts.md). ## Generated directories - **`.nsh/`**: everything NSH generates: the type declarations (`.nsh/types`), the sources of a Rust app's runners (`.nsh/rust`), what `nsh run` installs on simulators and devices (the Catalyst host app, the Android development APK), and `.nsh/out`, the default output of `nsh build` (`bundle.js`, `bundle.js.map`, `bundle.nshc`, `capabilities.json`, `assets/`; for a Rust app, `app.json` and `assets/`) and `nsh package` (`.app`, `.ipa`, `.apk`, a site); `-o` writes elsewhere. Safe to delete; it is recreated. - **`target/`**: Cargo's build directory for an app's crates and runners (or `CARGO_TARGET_DIR`). Source: https://nsh.dev/versions/v0.2.0/docs/project-structure --- # Building with Rust 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 | QuickJS and the engine, with your crate | | 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` | ## Choosing **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](https://nsh.dev/versions/v0.2.0/docs/reactivity.md)), 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](https://nsh.dev/versions/v0.2.0/docs/signed-packages.md) 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](https://nsh.dev/versions/v0.2.0/docs/signed-packages.md). 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. ## An app written in Rust ```sh 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, windows ``` `src/lib.rs` holds the app: components, state, handlers, tasks. ```rust use nsh_base::{button, theme}; use nsh_core::ui::prelude::*; /// The components available to TSX through `app/native`. #[native_module("app/native")] fn native(registry: &mut Registry) { registry.register::(); } #[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: ```json { "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](#several-crates)); `"theme"` is the [theme](#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. 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](https://nsh.dev/versions/v0.2.0/docs/networking.md#from-rust-components)), `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. ## Desktop title bars Rust apps draw their own title bars with `nsh_core::ui::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. ```rust use nsh_core::{ desktop::TitleBarStyle, ui::{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. ## Extending a Rust app with TSX The crate's `#[native_module("app/native")]` function names what TSX may import: components (`registry.register::()`), modules of functions (`registry.module::()`, `#[module]` on a unit struct and its `impl`), classes (`registry.class::()`, `#[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 fn`s 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: ```rust /// A price chart. #[derive(Clone, PartialEq, Props)] pub struct ChartProps { /// The symbol to chart. pub symbol: Prop, /// The range shown. #[prop(default = SharedString::from("1D"))] pub range: Prop, } #[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) -> 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, } #[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, 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 } } ``` ```tsx 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 ( Balance: {balance()} balance.set(ledger.add(price))} /> ledger.upload("https://example.com/ledger")} role="button" class="rounded-lg bg-blue-600 px-4 py-2 active:bg-blue-700"> Upload ); } mount(() => ); ``` 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: ```rust /// 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`, 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](https://nsh.dev/versions/v0.2.0/docs/dev-server-and-debugging.md#logs)); one returning a value gives `Promise`. 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`, which Rust calls at once inside a call, or later with `post` and `call_async`; TS sees `(value: number) => void` for a `Callback`, and `#[args(balance)] listener: Callback` names the argument, `(balance: number) => void`). An event's handler argument is `value` too, and `#[args(index)]` on the event's variant names it. 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. ### Render functions and the app's classes 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>`, 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. ```rust /// A quote: its symbol, then content of the app's. #[derive(Clone, Copy, PartialEq, Props)] pub struct QuoteProps { /// The symbol. pub symbol: Prop, /// Tailwind classes of the symbol's text. pub symbol_class: Prop>, /// Content of the app's after the symbol (a change, a badge). pub render_badge: Prop>>, } /// 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()) })) } ``` ```tsx {`${symbol} ▲ 2.4%`}} /> ``` `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](https://nsh.dev/versions/v0.2.0/elements/text-view.md)). ## Testing 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](https://nsh.dev/versions/v0.2.0/docs/testing-and-trace.md)) in a headless runner built with your crates, where `open(".")` starts a Rust app's root: ```toml [dev-dependencies] nsh-engine = "0.1" nsh-testing = "0.1" ``` ```rust // 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::())).unwrap(); app.wait_for("Count: 0").unwrap(); app.press("Tap").unwrap(); app.wait_for("Count: 1").unwrap(); } ``` ```tsx // 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: ```toml [dev-dependencies] nsh-testing = { version = "0.1", features = ["bundle"] } nsh-base = "0.1" ``` ```rust #[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. ## Theme 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](https://nsh.dev/versions/v0.2.0/docs/architecture.md#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](https://ui.shadcn.com/docs/theming), 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: ```json { "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](https://ui.shadcn.com/themes) or [tweakcn](https://tweakcn.com) works as it is. `light` sets the values of the light appearance, `dark` those of the dark one, and `theme` those of both: ```json { "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 `light` sets and `dark` leaves 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 are `rem` (16 points) or `px` (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 `@theme` is: `"up"` above is `theme().color("up")` in Rust, `bg-up` / `text-up/70` in classes and `token("up")` in TSX ([Theme tokens](https://nsh.dev/versions/v0.2.0/docs/tailwind.md#theme-tokens)). A number such as `"radius-card": "14px"` is `theme().number("radius-card")` and `rounded-(--radius-card)`. - The bundler checks the theme when it builds: a missing file, invalid JSON or a theme other than `cssVars` fails the build with an `nsh.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` | ## Several crates 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. ```json { "rust": ["native/charts", "native/storage"], "root": "App" } ``` ```tsx 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. ## Browser APIs on the web 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](https://nsh.dev/versions/v0.2.0/docs/web.md#browser-apis-from-rust). ## See also - [Project structure](https://nsh.dev/versions/v0.2.0/docs/project-structure.md): every key of `nsh.json`. - [Web](https://nsh.dev/versions/v0.2.0/docs/web.md#apps-with-rust-components): Rust apps in the browser. - [Networking](https://nsh.dev/versions/v0.2.0/docs/networking.md#from-rust-components): `fetch` from Rust. - [CLI](https://nsh.dev/versions/v0.2.0/docs/cli.md#nsh-init): `nsh init --template rust` and `--template mixed`. Source: https://nsh.dev/versions/v0.2.0/docs/rust --- # CLI `nsh` is the tool an NSH app is built with, in Rust or TSX. Every subcommand that builds an app takes `[ENTRY]`: the app's directory (default: the current one, so `cd my-app && nsh run` works), which runs a Rust app's root component (`nsh.json` `"root"`) or a TSX app's `App.tsx`, or a TSX entry module. It reads `nsh.json` in the app's root and refreshes the type declarations in `.nsh/types`. ```text nsh init Create an app in Rust or TSX build Build an app: describe a Rust app's crates, or bundle a TSX app and compile it to QuickJS bytecode test Run an app's tests headless check Type check an app with TypeScript's native compiler dev Serve an app to dev hosts (simulators, devices) and push rebuilds run Like dev, and also run the app in a development host package Package an installable app from the prebuilt runtime keygen Create a signing key certify Certify a release key with a root key publish Build an app in release mode and sign it into a package (.nshpkg) verify Verify a package symbolicate Map the positions in a reported error stack to the app's sources revoke Sign a revocation list ``` `nsh --help` prints the flags of a command; `nsh -V` (`--version`) prints the version and the Git revision it was built from. ## Output Every command prints one line per step, and the details only when something fails: ```text ✓ built the Rust crate gallery-native (5.0s) ✓ built App.tsx (500 B js, 809 B bytecode, 21ms) ✓ built the development host for the iOS simulator (12.7s) • iOS Simulator (iPhone 16 Plus) connected, build 1 warn iOS Simulator: image failed: An SSL error has occurred and a secure connection to the server cannot be made. ✓ rebuilt App.tsx (18ms), 1 device ``` `✓` is a finished step, `→` where to connect, `•` what a device did; `warn` and `error` lines are problems, and plain lines are your app's own output (`console.log`). On a terminal a spinner shows the step that is running, and lines are colored (unless `NO_COLOR` is set); redirected to a file, only the result lines are written, without colors. The output of the tools NSH runs (cargo for an app's Rust crate, the development hosts) is printed below an `error` line when one of them fails. `-v, --verbose` (any command; also `NSH_VERBOSE=1`) prints everything as it comes: every log line with its level and source, cargo's output and the hosts' own. ## nsh init ```sh nsh init [DIR] [--name ] [--template ] [--nsh-source ] ``` Creates an app in `DIR` (default: the current directory, created if missing) from a template. Without `--template`, `nsh init` asks which one on a terminal (Enter picks `ts`) and takes `ts` elsewhere (a script, CI). | Template | What it creates | |---|---| | `rust` | An app written in Rust: a Cargo project (`Cargo.toml`, `src/lib.rs` with the root component, `src/main.rs` for `cargo run`), `nsh.json` with `"rust": "."` and `"root": "App"`, the root's test `App.test.tsx` with `tsconfig.json` and `.nsh/types`, `.gitignore` and a README, which also says how to add a TSX entry later. `nsh run` and `nsh package` mount the root with no QuickJS on every target (`nsh test` opens it for TypeScript tests), the browser included ([Building with Rust](https://nsh.dev/versions/v0.2.0/docs/rust.md)); `cargo run` starts it on macOS, Windows or Linux directly. | | `ts` | A TSX app: `App.tsx`, its test `App.test.tsx` (`nsh test`), `nsh.json`, `tsconfig.json`, `.gitignore` and `.nsh/types`. Files that exist are kept; it fails only if all of them exist already. | | `mixed` | A TSX app with a Rust crate of its own at its root: `App.tsx`, its test `App.test.tsx`, `tsconfig.json`, `nsh.json` with `"rust": "."`, `.gitignore`, `.nsh/types`, a README, and the crate beside `App.tsx` (`Cargo.toml`, `src/lib.rs`), whose `#[native_module("/native")]` exports a component, a module and a class that `App.tsx` uses. With no `"root"`, `App.tsx` is the entry. | | Flag | Description | |---|---| | `--name ` | Display name written to `nsh.json`. Default: the directory's name. The id becomes `com.example.` (letters and digits only, lower case), and the crates' import specifier `/native` (lower case, words joined by `-`). | | `--template