# 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) is a native UI framework for every platform. You build an app once, with one app model — components, signals, styles and the same API everywhere — and NSH runs it on **iOS, Android, macOS, Windows, Linux and the web**, each time as an app of that platform: made of its own views, edited with its own text system, used the way its users expect. Write the whole app in **Rust**, write screens in **TSX**, or combine both on one engine. On top of the same engine, apps you have shipped can take new UI at run time — written by your team or generated by a model — as signed modules, in a sandbox the host configures. > [!IMPORTANT] > NSH provides native views and interaction; the app owns its visual design. Components, state and APIs are shared across platforms, while colors, typography, spacing and shapes come from the app. Text input, focus and menus follow each device’s interaction conventions. See [Goals and design principles](https://nsh.dev/versions/main/docs/goals.md). ## What native means Native in NSH has two layers: the platform's foundations, and the platform's interaction. **The platform's foundations.** Each platform has a backend of its own, built on the UI that platform already has: | Platform | Views | Text | Text input | |---|---|---|---| | iOS | UIKit | CoreText | The system's text fields and text views | | Android | Android views | `StaticLayout` | `EditText` | | macOS | AppKit, Core Animation | CoreText | `NSTextField`, `NSTextView` | | Windows | Win32 windows drawn with Direct2D | DirectWrite | NSH's editor, with the system's input methods (IMM32) | | Linux | GTK 4 | Pango | GTK's `Text` and `TextView` | | Web | HTML elements styled with CSS | The browser's fonts | The browser's inputs | Text selection and its menu, input methods, accessibility and scrolling come from the platform, and what a new OS release brings to them reaches the app with it. Native components — a map, a web view, a video player, any view the host registers — are one more view in the same hierarchy, laid out, clipped and scrolled with the rest ([Native components](https://nsh.dev/versions/main/docs/native-components.md)). **The platform's interaction.** The components NSH provides in [`nsh/base`](https://nsh.dev/versions/main/modules/nsh-base.md) behave as each device's users expect, so the app writes them once: | | Desktops (and desktop browsers) | Phones and tablets | |---|---|---| | `Menu` | A popup at its trigger; submenus cascade beside their item | A sheet from the bottom; a submenu replaces its menu's items | | `ContextMenu` | A right click opens it at the pointer | A finger held on the trigger opens it as a sheet | | `Tooltip` | Shown when the pointer rests on the trigger | Shown when the app opens it | | `Popover` | A press outside closes it and reaches what is under the pointer | A press outside closes it | Pages, dialogs, the keyboard and the status bar follow the same idea: navigation stacks push and pop with each platform's motion, and the back button, Escape and swipe-back do what they do there ([Navigation](https://nsh.dev/versions/main/docs/navigation.md)). ## From phone to desktop to web Cross-platform in NSH means each device gets the app it expects, not the same screen scaled up or down: - **One app model.** The app's components, state, business logic and screens are shared; each backend maps them to its platform. - **Layouts that reshape.** Flexbox and CSS grid are laid out by [Taffy](https://github.com/DioxusLabs/taffy) on every platform, and Tailwind's breakpoints (`md:`, `lg:`) and the window's size (`window()` in Rust) change the structure, not only the size: a chat app shows its sidebar, conversations and messages side by side in a wide window, and one screen at a time on a phone ([Layout](https://nsh.dev/versions/main/docs/layout.md), [Tailwind](https://nsh.dev/versions/main/docs/tailwind.md)). - **The input each device expects.** A pointer, hover, right clicks, shortcuts and focus on the desktop; touches, gestures, the software keyboard and page navigation on phones. Where an app wants something of its own per platform, `desktop={{…}}` props and `platform.select` say so ([Platform-specific code](https://nsh.dev/versions/main/docs/platform-specific.md)). - **Checked on every target.** `nsh test` runs the app's tests headless, and the same tests in macOS, Windows, Linux and GPUI windows, an iOS simulator, an Android device or a browser, with the platform's own input ([Testing and tracing](https://nsh.dev/versions/main/docs/testing-and-trace.md)). Take an app like a chat client. Its developer builds it on the desktop with `nsh run`, opens it in an iOS simulator with `nsh run --target ios` to check the phone layout, then in a browser with `nsh run --target web` — the same project each time — and `nsh package` makes the installable apps and the website. One engineer can carry what used to take a team per platform, while the backend is built alongside. ## Rust first NSH is built in Rust, and Rust is a first-class language for writing apps with it: - **Whole apps in Rust.** UI, logic and components are Rust, compiled to native code on every platform, with Rust's ownership, types, threads and the crates.io ecosystem. Computation runs at native speed, and large data, parsing and system work stay in the language best suited to them ([Building with Rust](https://nsh.dev/versions/main/docs/rust.md)). - **TSX as the second path.** TSX suits screens written quickly, plugins, small apps and UI delivered at run time. It runs on the same engine as Rust, with the same primitives, base components and signals. - **Both together.** A Rust app can take TSX screens, and TSX screens import the app's Rust components, modules and classes. - **One toolchain.** A single `nsh` executable creates, builds, type-checks, runs with hot reload, tests, packages and signs apps for every target. It brings its own TSX bundler and TypeScript checker, so a developer coming from Rust, Swift, Kotlin or C++ installs Rust and `nsh`, and works ([Installation](https://nsh.dev/versions/main/docs/installation.md), [CLI](https://nsh.dev/versions/main/docs/cli.md)). ## An app The same counter, in Rust and in TSX: ```rust use nsh_core::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, a GTK widget on Linux, a retained view drawn with Direct2D on Windows and an HTML element on the web; a text becomes 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. ## Retained views NSH is retained: every element is a view the platform keeps, and an update changes only what changed. - **Fine-grained updates.** [Signals](https://nsh.dev/versions/main/docs/reactivity.md) like Solid's, implemented natively: components run once, and a change runs again only the expressions that read the changed signal. - **Only what changed crosses threads.** The changes of one turn are laid out together and handed to the main thread as one batch of view operations per frame; unchanged styles and frames are not sent, and the views nothing touched stay as they are. - **Animations run natively.** Timing, spring and decay animations, gestures and scroll-linked values are evaluated on the platform's frame clock, without calling into JS per frame ([Animation](https://nsh.dev/versions/main/docs/animation.md)). That suits the screens most apps are made of — chats, mail, forms, lists, documents, settings, an AI reply streaming in — where a few views change at a time and the rest wait. Such screens sit near idle between updates, and scrolling and animation hold 120 FPS on ProMotion displays. In the [NSH performance benchmark](https://github.com/longbridge/nsh-perf-bench), a 500-row quote table updated 1,000 times a second uses 18% of a core on Linux in NSH Rust, against 20% for the same screen written directly on [GPUI Fast](https://github.com/longbridge/gpui-fast). Heavy computation belongs in Rust: TSX runs in QuickJS, an interpreter, while a Rust module runs at native speed even when TSX calls it — sorting 100,000 rows takes about 3 ms that way, as in plain Rust ([Performance](https://nsh.dev/versions/main/docs/performance.md)). ## How it runs The toolchain is a single `nsh` binary. An app is written in Rust, in TSX, or both, and each takes its own path to the same Rust engine. ### Rust apps A Rust app is a crate, built entirely with Rust and Native, with no JavaScript runtime: 1. **Build.** `nsh` builds the runner of each target — the iOS, Android, macOS, Windows, Linux or web app — with your crate linked in. Components, state, handlers and the engine are compiled together into native code. 2. **Run.** The app's root component runs on the engine thread as Rust. The runner has no JavaScript engine: QuickJS is not part of a Rust app on any platform. ### TSX apps 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.** The engine thread runs the bytecode in [QuickJS](https://github.com/quickjs-ng/quickjs), one VM per view. Compiled JSX calls the native renderer directly, and JavaScript only describes the UI and handles events; parsing styles, resolving Tailwind classes, diffing, layout and text all happen in Rust. ### The difference | | Rust app | TSX app | |---|---|---| | Written in | Rust | TSX (TypeScript and JSX) | | Built into | Native code, linked into each target's runner | QuickJS bytecode, loaded by the runtime | | JavaScript engine | None | QuickJS, one VM per view | | While developing (`nsh run`) | Rebuilt and restarted on each save | Hot updates on each save | | After release | Ships with the app | Also delivered at run time as signed packages, in a sandbox | | Suits | Whole apps, heavy computation, system work | Screens written quickly, plugins, AI-generated UI | The two combine: a Rust app can take TSX screens, and TSX screens import its Rust components, modules and classes; such an app links QuickJS to run them ([Building with Rust](https://nsh.dev/versions/main/docs/rust.md)). ### The shared engine From the engine on, both paths are the same. Changes are collected per turn, laid out with Taffy (flexbox and CSS grid), and handed to the main thread as one batch of view operations per frame; 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, the browser's font metrics on the web) and only drawn on the main thread. On the web, the same engine runs in the page: the browser shows HTML elements styled with CSS, scrolls them, selects their text and handles input, and `nsh package --target web` publishes the app as a static site whose files are named by content, ready for any CDN ([Web](https://nsh.dev/versions/main/docs/web.md)). > [!NOTE] > The main thread never waits for the app's code. 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 set beforehand, such as `maxLength`, `popOnBack` or `closeOnSwipe`. Events reach the app's handlers afterwards. ## Dynamic UI and AI-generated UI NSH began as a way for native apps to take UI after release, and that stays one of its main uses. An app embeds an `NshAppView` (iOS), `NshView` (Android), `AppView` (macOS, Windows, Linux and GPUI apps) or `createPlayer` (a web page) wherever a module should appear, and NSH runs TSX modules 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/main/docs/signed-packages.md)). The host app ships the runtime; teams build modules against its API and deliver them on their own schedule. - **The host stays in control.** Each module runs in its own QuickJS VM, with memory, call-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/main/docs/sandbox-and-capabilities.md)). Several modules run side by side. - **It looks like the rest of the app.** Modules render with the platform's own UI, and 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/main/docs/native-components.md)). The same mechanism lets an app **answer with an interface**. A model writes a small TSX module (a form, a chart, a booking card), your server checks, builds and signs it, and the app shows it natively: ![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/main/diagrams/ai-flow-light.svg) On iOS, the host's part is a few lines: ```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 ``` ## AI-friendly NSH is designed for software written with AI, whole apps as much as generated screens: - **One app model for every platform.** A model learns one way to write UI, and the result runs on phones, desktops and the web; NSH adapts the interaction per platform, so nothing is generated six times. - **Rust or TSX, by task.** Rust for whole apps and performance-critical modules, TSX for screens, plugins and UI generated per request — on one engine. - **A precise, generated contract.** Every element, prop, style key and module has TypeScript declarations generated from the Rust sources (`.nsh/types`), and Rust APIs are typed, so generated code is checked by a compiler. These docs are served as plain text for models ([`llms.txt`](/llms.txt), [`llms-full.txt`](/llms-full.txt)). - **One command line for the whole loop.** An agent creates the app, builds it, runs it on a target, runs its tests, reads the result, fixes and packages it, all through `nsh`. `nsh test` runs headless, without a device, and reports errors and the views produced; the compiler also runs in-process (`nsh_bundler::compile_module`) and in the browser ([Playground](/playground)). - **Safe to run when it is not trusted.** Generated code delivered at run time is untrusted code: it arrives signed, runs in a VM with limits and reaches only the capabilities the host grants. ## Where it runs | Platform | Requires | Backend | Embedded with | |---|---|---|---| | [iOS](https://nsh.dev/versions/main/docs/ios.md) | iOS 15.0+ | UIKit | `NativeShell.xcframework` | | [Android](https://nsh.dev/versions/main/docs/android.md) | Android 9+ | Android views | `nsh.aar` | | [macOS](https://nsh.dev/versions/main/docs/macos.md) | macOS 14+ | AppKit | `AppView` | | [Windows](https://nsh.dev/versions/main/docs/windows.md) | Windows 10 1809+ | Win32, Direct2D | `AppView` | | [Linux](https://nsh.dev/versions/main/docs/linux.md) | GTK 4.14+ | GTK 4 | `AppView` | | [Web](https://nsh.dev/versions/main/docs/web.md) | Modern browsers | HTML | `createPlayer`, iframe | | [GPUI](https://nsh.dev/versions/main/docs/gpui.md) | Desktops | GPUI | `AppView` (GPUI, GPUI Kit apps) | | Mac Catalyst | macOS | UIKit | For development (`--target catalyst`) | ![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/main/diagrams/platforms-light.svg) ## Native Shell and GPUI Shell [Longbridge](https://longbridge.com) builds NSH for its own clients. The first need was a plugin layer for the mobile client, and [GPUI Shell](https://gpui-kit.com/shell), which makes [GPUI](https://gpui-kit.com) desktop apps extensible in JavaScript, was the starting point. On phones, UI drawn by GPUI itself would have had to rebuild what the platform already does — text selection and its menu (Copy, Share, Translate, Look Up), input methods, accessibility, the visual materials each iOS release brings — and every mobile team would have taken on a new UI stack. NSH therefore renders with each platform's own UI, and it grew from that plugin layer into a framework for whole apps on every platform. The two are positioned differently. GPUI Shell extends GPUI desktop applications; Native Shell is a framework for native apps on phones, desktops and the web, and its GPUI backend embeds NSH apps in GPUI and GPUI Kit apps ([GPUI](https://nsh.dev/versions/main/docs/gpui.md)). ## How it compares For whole apps, NSH sits among cross-platform frameworks such as React Native, Flutter, Qt, GPUI, Slint, Electron and Tauri; for UI delivered after release, among web views, server-driven UI and mini-program containers. Its choice is each platform's own UI and interaction under one app model, with Rust as the app language and TSX beside it. [Comparing approaches](https://nsh.dev/versions/main/docs/comparison.md) goes through each. ## Next steps - [Installation](https://nsh.dev/versions/main/docs/installation.md) and [Getting started](https://nsh.dev/versions/main/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/main/docs/rust.md): apps written in Rust, and TSX on top of them. - [Platforms](https://nsh.dev/versions/main/docs/platforms.md): what runs where, and how to run, package and embed on each platform. - [JSX and components](https://nsh.dev/versions/main/docs/jsx-and-components.md) and [Reactivity](https://nsh.dev/versions/main/docs/reactivity.md): the two ideas everything else builds on, in Rust and TSX. - [Signed packages](https://nsh.dev/versions/main/docs/signed-packages.md) and [Sandbox and capabilities](https://nsh.dev/versions/main/docs/sandbox-and-capabilities.md): deliver modules and decide what they may do. Source: https://nsh.dev/versions/main/docs --- # Goals and design principles Native Shell provides native views and interaction across iOS, Android, macOS, Windows, Linux and the web. The app owns its visual design. One component model serves whole apps, embedded features and dynamically delivered UI. ## Native foundations, app-owned design NSH builds on each platform's views, text and input infrastructure, and adapts interaction to the device: keyboard navigation, pointer and touch input, text editing, scrolling and accessibility ([Introduction](https://nsh.dev/versions/main/docs.md#what-native-means)). The app defines colors, typography, spacing, shapes, state feedback and animation. A settings screen can follow the app's brand on iOS and Android while input and navigation follow each device's conventions. Designers refine that visual language through styles and component parts. A native view is the rendering foundation. A system-styled control is a particular visual and behavioral choice. NSH's common controls expose shared behavior and customizable parts so applications can build their own design systems. ## Native integration in practice The native foundation includes rendering, frame-driven animation, input and accessibility semantics. On iOS and Android, [Input](https://nsh.dev/versions/main/elements/input.md) uses platform text fields: the platform handles editing, selection menus, input methods and supported autofill services. The app describes the field through `autoComplete`, for example `current-password` or `postal-code`, and the system provides the available suggestions according to its settings and services ([Forms and keyboard](https://nsh.dev/versions/main/docs/forms-and-keyboard.md)). Styling the field is compatible with keeping this system integration. This is an architectural distinction from drawing a text field entirely within a custom renderer: a custom renderer supplies its own integration with system editing and accessibility services, while NSH uses platform text controls where its backend provides them. Each backend's documentation defines its integration, including NSH's own editor on Windows and the GPUI embedding backend ([Platforms](https://nsh.dev/versions/main/docs/platforms.md)). [GPUI Kit](https://gpui-kit.com)'s separation of component behavior from visual design informs NSH's layers. NSH builds on that principle with platform view and input infrastructure. Native integration and app-owned design work together throughout this model. ## One component model across platforms A switch means the same thing on a phone, a desktop and the web. [`nsh/base`](https://nsh.dev/versions/main/modules/nsh-base.md) provides state, input handling, focus and accessibility; the app styles the [Switch](https://nsh.dev/versions/main/elements/switch.md)'s track, thumb and states. Buttons, checkboxes and radios follow the same principle. | Responsibility | Home | Example | |---|---|---| | Rendering, layout, reactivity and environment | `nsh-core`, `nsh` and runtime modules | Views, text, signals, styles | | Shared behavior and accessible structure | `nsh-base`, imported as `nsh/base` in TSX | `Button`, `Checkbox`, `Switch`, `Tabs` | | Visual design and styled components | The app or a community library | Branded controls, cards, form rows | | Platform-specific capabilities | `nsh/mobile`, `nsh/desktop` | Mobile navigation bar, desktop window controls | Common controls have one cross-platform home in `nsh/base`. Platform modules are scoped by purpose and platform semantics. [`TabBar`](https://nsh.dev/versions/main/modules/nsh-mobile.md#tabbar) serves mobile top-level navigation; [`Tabs`](https://nsh.dev/versions/main/elements/tabs.md) switches tabbed content across platforms. Their roles are distinct. `nsh/mobile` accepts capabilities with mobile-specific interaction semantics; differences in a common control’s appearance, size, tint or animation belong within its shared component and app styling. A notification or toast with a shared role should first be designed around one cross-platform contract, with presentation adapted internally. A shared component can adapt its interaction internally: a menu opens as a popup on a desktop and a sheet on a phone, while the app uses one API and supplies its appearance. System-managed presentations such as navigation headers and alerts follow their documented platform presentation rules. Apps can expose their own native components and services through the [embedding interfaces](https://nsh.dev/versions/main/docs/native-components.md). ## The unstyled foundation and the component-library layer NSH owns the primitives and the unstyled foundation (`nsh-core` and `nsh-base`). Applications and third-party libraries own the styled component-library layer — the role `gpui-component` plays in [GPUI Kit](https://gpui-kit.com). This division gives each product control over its visual language while reusing the difficult parts of component implementation. The experience behind this choice comes from [GPUI Kit](https://gpui-kit.com). Its styled components grew from work on Longbridge Pro. As applications needed increasingly different designs, customization pressure led users to fork components. Extracting the shared foundation into `gpui-base` made behavior reusable across those designs. Styled components remain useful as a design reference and for applications whose needs fit their customization surface; the foundation gives other applications direct control over presentation. NSH applies this lesson from the start. `nsh/base` owns complex behavior, interaction, reactive state, focus, accessibility and adaptive presentation, with tests at the layer that owns each behavior. This includes larger structures such as [Dock](https://nsh.dev/versions/main/elements/dock.md). Applications build diverse designs on that foundation through styles, states, slots and parts. Component contributions follow the same boundary: visual defaults and branded variants belong to application or third-party components. The documented system-managed presentations and `TextView` reading defaults retain their specific contracts ([Architecture](https://nsh.dev/versions/main/docs/architecture.md#three-layers)). ### What the Gallery demonstrates The website demos and Gallery are styled example applications. Their gradients, shadows, rounded corners, transitions and motion exercise NSH's customization capabilities and provide source examples. Those choices belong to the demos and Gallery; the reusable base components expose the behavior and parts underneath them. Applications can borrow the examples or build a different visual language. An independently distributed styled library would belong to the application/community layer described above. Gallery styling illustrates one design direction; suitability for a particular product is a design decision for that product. ## Rust and TSX on one engine Rust apps, TSX screens and mixed projects share primitives, base components, signals and layout. Rust provides compiled application logic and reusable native components. TSX supports rapid iteration and modules delivered at run time ([Building with Rust](https://nsh.dev/versions/main/docs/rust.md)). The engine updates bindings that depend on changed signals and batches view operations. Text layout, native animations and input belong to the engine and its platform backends ([Architecture](https://nsh.dev/versions/main/docs/architecture.md)). Performance is evaluated with reproducible application workloads ([Performance](https://nsh.dev/versions/main/docs/performance.md)). ## Embedding, dynamic delivery and AI An existing native app places an NSH view where a feature belongs and exposes its own native components and host services. Standalone apps use the same runtime and component model ([Platforms](https://nsh.dev/versions/main/docs/platforms.md)). Dynamically delivered TSX modules own their state, logic and layout. Signed packages establish which publishers the host trusts; sandbox limits and capabilities define what modules may do. The host chooses both trust and permissions ([Signed packages](https://nsh.dev/versions/main/docs/signed-packages.md), [Sandbox and capabilities](https://nsh.dev/versions/main/docs/sandbox-and-capabilities.md)). AI-generated UI follows that contract: generate TSX against declared APIs, check and test it, build and sign the module, then verify and run it under host policy. Generated declarations, model-readable documentation and CLI tests make the workflow inspectable and repeatable ([Testing and tracing](https://nsh.dev/versions/main/docs/testing-and-trace.md)). ## How these goals guide additions 1. Establish the user behavior and whether it is shared across platforms. 2. Put shared behavior in the existing primitive or base component, exposing state and parts for app customization. 3. Put capabilities with platform-specific semantics in their platform module, documenting applicability and composition with shared UI. 4. Build styled components in the app or community library. 5. Verify behavior, accessibility, layout and input on affected platforms, and document the public contract in both languages. These principles keep visual design flexible, shared component APIs coherent and platform integration explicit as NSH grows. Source: https://nsh.dev/versions/main/docs/goals --- # Comparing approaches NSH is a framework for whole apps on iOS, Android, macOS, Windows, Linux and the web, and a runtime for UI delivered to apps after release. This page compares it first with the cross-platform frameworks people write apps in, then with the ways apps take UI they did not ship with. Green means the capability is available, yellow that it needs more of the app's own work or a separate project, and red that it is not provided; the marks describe availability, not performance. Versions are each framework's documented minimums at the time of writing: check them for the release you use. Legend: Yes · Partial · No. ## Cross-platform frameworks | Capability | NSH | React Native | Flutter | Qt 6 | GPUI | GPUI Fast | Slint | |---|---|---|---|---|---|---|---| | Drawn by | Platform UI | Platform views | Own engine | Scene graph | Own, GPU | Own, GPU | Own renderer | | UI model | Retained | Retained | Retained | Retained | Immediate | Immediate + Retained | Reactive, retained | | Language | Rust, TSX | JS, TS | Dart | C++, QML | Rust | Rust | Slint + Rust, C++ | | iOS | 15+ | 15.1+ | 15+ | 17+ | | | Rust | | Android | 9+ | 7+ | 7+ | 9+ | | | | | macOS | 14+ | Separate | 12+ | 13+ | 12+ | 12+ | 13+ | | Windows | 10 1809+ | Separate | 10+ | 10 1809+ | 10+ | 10+ | 10+ | | Linux | GTK 4.14+ | | Debian 10+ | Debian 11+ | Vulkan | Vulkan | | | Web | HTML | Library | Canvas | Canvas | | | Canvas | | System text | | | | | | | | | Per-device UX | | | | | | | | | No JS runtime | Rust apps | | | C++ | | | | | Signed delivery | | OTA | | QML | Shell | Shell | | | License | Apache-2.0 | MIT | BSD-3-Clause | LGPL, GPL, commercial | Apache-2.0 | Apache-2.0 | GPL, commercial | Immediate mode builds the UI description again every frame; retained mode keeps it and updates only what changed. GPUI Fast combines the two on GPUI and runs where GPUI runs. "Shell" is [GPUI Shell](https://gpui-kit.com/shell). NSH is the one framework here that brings all of these together: each platform's own UI and text input, phones, desktops and the web from one app — the web as HTML — Rust apps with no JavaScript runtime, and signed, sandboxed UI delivered after release. ### What the rows mean - **Drawn by, UI model.** NSH mounts each platform's own views and keeps them (retained mode): a change updates only the views it touched. React Native also mounts platform views, on iOS and Android. Flutter, Qt Quick, GPUI and Slint draw with their own renderers; GPUI builds the element tree each frame (immediate mode), and [GPUI Fast](https://github.com/longbridge/gpui-fast) reuses what did not change. - **Platforms.** NSH's minimums are its own ([Platforms](https://nsh.dev/versions/main/docs/platforms.md)). The others come from [React Native 0.76](https://reactnative.dev/blog/2024/10/23/release-0.76-new-architecture) (iOS 15.1, Android 7 / API 24; macOS and Windows through the separate react-native-macos and react-native-windows projects, the web through the react-native-web library), [Flutter 3.47](https://docs.flutter.dev/reference/supported-platforms), [Qt 6.10](https://doc.qt.io/qt-6.10/supported-platforms.html), [Zed's requirements](https://zed.dev/docs/installation) for GPUI, and [Slint's platform guides](https://docs.slint.dev/latest/docs/slint/guide/platforms/mobile/general/) (iOS from Rust). "Canvas" means the framework draws into a canvas on the page; NSH's web apps are HTML elements styled with CSS, which the browser scrolls, selects and edits ([Web](https://nsh.dev/versions/main/docs/web.md)). - **System text.** NSH's text fields are the platform's editors — `UITextView`, `EditText`, `NSTextView`, GTK's `TextView`, the browser's — with the system's input methods, selection handles and editing menus; on Windows, NSH's own editor works with DirectWrite and the system's input methods (IMM32). React Native also uses the platform's text inputs. The self-drawn frameworks implement text editing themselves and connect it to the system's input methods. - **Per-device UX.** NSH's base components follow the device: the same `Menu` is a popup at its trigger with a pointer and a sheet from the bottom on a phone, and a `ContextMenu` opens on a right click or a held finger. In the others, adapting interaction per device is mostly the app's own work. - **No JS runtime.** A Rust app's runners link no JavaScript engine on any platform; TSX is the second path, on the same engine. Qt Quick's QML runs on Qt's JavaScript engine; Qt Widgets in C++ do not. - **Signed delivery.** NSH verifies signed `.nshpkg` packages against the keys it trusts and a revocation list, and runs each in a VM with the limits and capabilities the host grants ([Signed packages](https://nsh.dev/versions/main/docs/signed-packages.md), [Sandbox and capabilities](https://nsh.dev/versions/main/docs/sandbox-and-capabilities.md)). React Native's over-the-air updates ship trusted app code; Qt can load QML files; GPUI apps take JavaScript modules through [GPUI Shell](https://gpui-kit.com/shell). ### What sets NSH apart - **Native on every platform, in both senses.** Each backend builds on its platform's own views, text and input methods — UIKit, Android views, AppKit, Win32 with Direct2D, GTK 4, HTML — and each platform's interaction comes with them. Text selection, input methods and accessibility are the system's, and what a new OS release brings to them reaches the app ([Introduction](https://nsh.dev/versions/main/docs.md#what-native-means)). - **Phones, desktops and the web from one app.** Six platforms are backends of one framework, with layouts that reshape and input that follows the device; on the web, the app is HTML, published as a static site. - **Rust first, with no JavaScript runtime.** A whole app can be Rust from start to end, compiled to native code with Rust's types, threads and crates. TSX is the second path, on the same engine, for screens written quickly ([Building with Rust](https://nsh.dev/versions/main/docs/rust.md)). - **Retained views for everyday apps.** Chats, forms, lists and documents sit near idle at rest, because a change sends only the views it touched ([Introduction](https://nsh.dev/versions/main/docs.md#retained-views)). - **UI delivered after release, signed and sandboxed.** Apps built with NSH — and native apps written with each platform's own tools — take TSX modules at run time, written by a team or generated by AI ([Dynamic UI](https://nsh.dev/versions/main/docs.md#dynamic-ui-and-ai-generated-ui)). - **One toolchain, for people and agents.** One `nsh` executable creates, runs, tests on real platforms, packages and signs apps for every target. ### React Native React Native is the closest in intent: both reduce duplicated client work and use the platform's views. React Native's center is JavaScript or TypeScript with React and its package ecosystem, on iOS and Android, with desktop support in separate projects. NSH's center is Rust: its runtime, renderer and modules are Rust, Rust is an app language of its own, and TSX is one path among two; its six platforms, the desktops and the web included, are backends of the same framework. ### Flutter Flutter draws every pixel with its own engine, so an app looks the same on every platform. NSH takes the other path: the views, text and text input of each platform, and on the web HTML elements and the browser's own scrolling, selection and input. Text editing, input methods and selection are the platform's, rather than reimplemented per platform. ### Qt Qt is a mature toolkit with two UI technologies, Qt Widgets and Qt Quick, and long-standing tools and platform support. NSH differs in its base and its model: Rust as the core language with a TSX path, a toolchain designed for automated (and AI-driven) development, the web rendered as HTML, and mobile, desktop and web interaction adapted inside one component model. ### GPUI and GPUI Fast [GPUI](https://gpui-kit.com) is a Rust UI framework that draws with the GPU, built for demanding desktop apps; [GPUI Fast](https://github.com/longbridge/gpui-fast) is Longbridge's fork with retained rendering — it reuses unchanged views, layout and text measurements — scroll layers and native window composition. NSH builds on each platform's views instead, so text input, selection, input methods and system interaction are the platform's own, and it covers phones and the web as well; NSH's GPUI backend embeds NSH apps in GPUI and GPUI Kit apps ([GPUI](https://nsh.dev/versions/main/docs/gpui.md)). The two suit different loads. In the [NSH performance benchmark](https://github.com/longbridge/nsh-perf-bench) (Linux, a 500-row quote table, NSH on its GTK 4 backend), everyday loads cost the same: at 1,000 quotes a second, NSH Rust uses 18% of a core and GPUI Fast 20%. Under sustained, dense updates GPUI Fast is ahead: at 20,000 quotes a second it uses 25% of a core where NSH uses 73–78%, and frame-by-frame animation of 1,000 boxes costs it about a third of NSH's CPU. ### Slint Slint is a declarative UI toolkit with its own language and light renderers, for desktops and embedded devices, used from Rust, C++, JavaScript or Python. Both value native performance; NSH's route is each platform's own UI and interaction, HTML on the web, Rust and TSX as app languages, and AI-driven development across six platforms. ### Electron and Tauri Electron bundles Chromium and Tauri uses the system's web view: both build desktop apps (and, with Tauri, mobile apps) from web content, with the browser's text and input. NSH builds each platform's own views instead, and renders HTML only on the web itself. ## UI delivered after release 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. | Capability | NSH | Web view | Server-driven UI | Mini-programs | OTA updates | |---|---|---|---|---|---| | Drawn by | Platform UI | Browser engine | The app's views | Web engine + layer | Framework renderer | | Native look and behavior | | | | | | | Own state, logic and layout | | | | | | | Host's native components | | Bridge | | | | | Sandbox the host configures | | Same-origin | Schema | | | | Signed, revocable packages | | | | | | | Fit for AI-generated UI | | Web pages | | | | | Cost to the host app | One view and a library | A browser engine per view | A renderer per component | A container SDK | The whole app in the framework | - **Web view (H5).** A page in a browser engine: anything HTML can express, with the browser's look and a hand-written bridge to native code. - **Server-driven UI.** JSON that recombines the components and actions the app already ships: easy to generate, with no local state or logic beyond what the schema provides. - **Mini-program containers.** A platform-specific framework and API on a web engine, inside a large container SDK. - **Over-the-air updates** (React Native with a code-push service, for example). The whole framework, but updates are trusted app code, and the app has to be written in that framework. - **NSH.** A TSX module with its own state, logic, animation and layout, rendered with the platform's own UI, that can use the host's registered native views. It arrives as a signed package the app verifies, runs in its own VM with memory, call-time and node limits, and reaches only the network hosts, web pages and system settings the host grants ([Sandbox and capabilities](https://nsh.dev/versions/main/docs/sandbox-and-capabilities.md)). The host adds one view where the module appears ([Platforms](https://nsh.dev/versions/main/docs/platforms.md#embed-in-a-native-app)), and the same bundle runs on every platform. This is the reason NSH exists next to [GPUI Shell](https://nsh.dev/versions/main/docs.md#native-shell-and-gpui-shell). ## When another option fits better - The screen is dominated by sustained, dense updates of thousands of views — a market-data terminal streaming tens of thousands of quotes a second, a custom visualization redrawn every frame: GPUI Fast's retained GPU rendering uses a fraction of the CPU (see [GPUI and GPUI Fast](#gpui-and-gpui-fast)). - The app must look pixel-identical on every platform: a framework that draws everything itself, such as Flutter or Slint, gives that by design. - You need a large ecosystem of third-party packages today: React Native, Flutter and Qt have years of them. - 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. ## See also - [Introduction](https://nsh.dev/versions/main/docs.md): what NSH is, and what native means in it. - [Architecture](https://nsh.dev/versions/main/docs/architecture.md): what runs where inside NSH. - [Performance](https://nsh.dev/versions/main/docs/performance.md): the rules NSH follows, and how to measure an app. Source: https://nsh.dev/versions/main/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/main/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). [huacnlee/cargo](#building-from-a-checkout) is recommended for development. `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 armv7-linux-androideabi 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, symbols//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/main/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. For development we recommend [huacnlee/cargo](https://github.com/huacnlee/cargo), a drop-in Cargo fork: your Git worktrees share one build directory that stays under a size limit on its own, with nothing to configure. It replaces `~/.cargo/bin/cargo`, keeps `rust-toolchain.toml` and `cargo +nightly` working, and updates itself; its README says how to put rustup's back. ```sh curl -fsSL https://huacnlee.github.io/cargo/install.sh | sh # Windows: irm https://huacnlee.github.io/cargo/install.ps1 | iex ``` ```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/main/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/main/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/main/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/main/docs/project-structure.md#tsconfigjson-and-nshtypes). ## Next steps Continue with [Getting started](https://nsh.dev/versions/main/docs/getting-started.md) to create an app and run it. Source: https://nsh.dev/versions/main/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/main/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/main/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), and we recommend [huacnlee/cargo](https://github.com/huacnlee/cargo) for building); 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/main/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/main/docs/rust.md)). ### The root component The two templates make the same app, a counter: ```rust // src/lib.rs use nsh_base::theme; use nsh_core::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( view() .flex_1() .p_safe_area() .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"; 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::prelude::*`** brings the primitives (`view()`, `text()`, `scroll()`, `input()`, `image()`, ...), [signals](https://nsh.dev/versions/main/docs/reactivity.md) and the typed style methods (`.p_safe_area()` pads a view by the [safe area](https://nsh.dev/versions/main/elements/safe-area.md)); **`nsh_base`** has the unstyled base components (`button()`, the keyboard, dialogs) and `theme()`, the colors and radii the app's components draw with ([Theme](https://nsh.dev/versions/main/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/main/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/main/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` and the other [host elements](https://nsh.dev/versions/main/elements.md) are imported from `nsh` (`Input` from `nsh/base`); only `If` and `Each` need no import. - **`class`** takes [Tailwind](https://nsh.dev/versions/main/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/main/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/main/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/main/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/main/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/main/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/main/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/main/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/main/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/main/docs/macos.md#embedding-in-a-mac-app), [Windows](https://nsh.dev/versions/main/docs/windows.md#embedding-in-a-win32-app), [Linux](https://nsh.dev/versions/main/docs/linux.md#embedding-in-a-gtk-app) and [GPUI](https://nsh.dev/versions/main/docs/gpui.md), and [Platforms](https://nsh.dev/versions/main/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/main/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/main/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/main/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/main/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/main/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/main/docs/rust.md#testing)). ## Next steps - [Building with Rust](https://nsh.dev/versions/main/docs/rust.md): Rust apps, their theme, and TSX on top of a Rust crate. - [JSX and components](https://nsh.dev/versions/main/docs/jsx-and-components.md) and [Reactivity](https://nsh.dev/versions/main/docs/reactivity.md): components and signals, in Rust and TSX. - [Platforms](https://nsh.dev/versions/main/docs/platforms.md): what each platform supports, and its own page ([iOS](https://nsh.dev/versions/main/docs/ios.md), [Android](https://nsh.dev/versions/main/docs/android.md), [macOS](https://nsh.dev/versions/main/docs/macos.md), [Windows](https://nsh.dev/versions/main/docs/windows.md), [Linux](https://nsh.dev/versions/main/docs/linux.md)). - [Signed packages](https://nsh.dev/versions/main/docs/signed-packages.md) and [Sandbox and capabilities](https://nsh.dev/versions/main/docs/sandbox-and-capabilities.md): delivery and what modules may do. - [Project structure](https://nsh.dev/versions/main/docs/project-structure.md): what goes in `nsh.json`, assets, files. Source: https://nsh.dev/versions/main/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/main/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::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`, `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/main/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/main/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/main/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/main/docs/rust.md#theme), [Theme tokens](https://nsh.dev/versions/main/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/main/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/main/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/main/docs/rust.md#several-crates)). | | `web` | object | `package` | What a web package adds to its page. `web.host`: a JS module relative to `nsh.json` exporting `createHost(player)`, which answers the app's [`host`](https://nsh.dev/versions/main/modules/nsh.md#host) calls; `web.connect`: the hosts that module's own requests reach, as patterns of `capabilities.network.hosts`, which the page's Content-Security-Policy allows ([Web](https://nsh.dev/versions/main/docs/web.md#the-hosts-functions)). | | `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/main/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/main/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/main/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, the refs (`ViewRef`, `InputRef`, ...: 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/main/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/main/modules/nsh.md#token) | | `mobile.d.ts`, `desktop.d.ts` | One file per module (`nsh/mobile`, `nsh/desktop`) | | `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/main/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::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. ### Leaving parts out of the binary If you build your own executable with Cargo (embedding NSH, or your own `main`), you can leave optional parts out: the platform crates turn all of them on by default, so set `default-features = false` on the platform crate (and on `nsh-base`) and name what you keep from `js`, `shader`, `editor` (with `DiffView`), `rich-editor`, `plot`, `highlight`, `math`, `zlib` and `crypto`. An app that imports a part its build left out fails when it loads. What `nsh package` builds always includes everything. ### 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/main/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`, `Image`, `List`, `Canvas`, `WebView`, `Video` (`View`'s `selectable` for text selection); safe area insets (`safeArea`, `safeAreaInsets`) and the software keyboard (`Keyboard`, `keyboardFrame`); native animations; `PanResponder`; the environment (`platform`, `windowSize`, `colorScheme`, `fontScale`, `pixelRatio`, `locale`); screen reader and reduce motion state, announcements; the clipboard, the photo picker, storage, the host channel, keyboard shortcuts; components and functions the embedding app registers ([reference](https://nsh.dev/versions/main/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`), a text input (`Input`) and the system's alert dialog (`AlertDialog`); pages and dialogs set the status bar (`statusBar`) ([reference](https://nsh.dev/versions/main/modules/nsh-base.md)) | | `nsh/mobile` | What iOS and Android have: `TabBar`, `InputAccessoryView` (iOS), `BackHandler` (Android) ([reference](https://nsh.dev/versions/main/modules/nsh-mobile.md)) | | `nsh/desktop` | What macOS, Windows and Linux have: `Window`, `titleBar`, `Menu` ([reference](https://nsh.dev/versions/main/modules/nsh-desktop.md)) | The [modules reference](https://nsh.dev/versions/main/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::element::{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/main/docs/images-and-assets.md#pixel-densities)). | | `.svg` | An `ImageAsset` sized in points by the SVG's `width` and `height` (or `viewBox`). | | `.ttf`, `.otf`, `.woff`, `.woff2` | 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/main/docs/images-and-assets.md) and [Fonts](https://nsh.dev/versions/main/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/main/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 (in the browser, its JavaScript engine) | QuickJS and the engine, with your crate (in the browser, its JavaScript engine) | | Suits | Medium and large apps with demanding business logic | Apps and modules delivered at run time, quick iteration | A Rust core with parts that change often | | Edits | Rebuild and restart | Hot updates | Hot updates for TSX, rebuild and restart for Rust | | Start | `nsh init my-app --template rust` | `nsh init my-app --template ts` | `nsh init my-app --template mixed` | ## 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/main/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/main/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/main/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::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. On Linux, your crate selects optional platform features. Enable `webview` on your `nsh-platform-linux` dependency to use web views; this links WebKitGTK 6.0, which must be installed to build and run the app. The generated runner inherits that choice through Cargo feature unification. During `nsh run`, a change to the crate's Rust code builds the runner again and restarts the app; in the browser, the open page loads the new runtime by itself. `cargo run` starts the same root directly on the desktop backend (`src/main.rs`), and native apps that embed NSH mount a Rust root with `AppSource::Rust` on the `AppView` of macOS, Windows, Linux, GPUI and iOS. Code for every platform, the web included, uses NSH's services: `fetch` for HTTP ([Networking](https://nsh.dev/versions/main/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::element::desktop`. Call `set_title_bar(TitleBarStyle::Hidden, Some(height))`, mark the bar's background with `.window_drag(true)`, and reserve the native window controls' space with `title_bar()`. Its `top` is the bar's height, and `left` and `right` are the widths occupied by the system controls. Reading it subscribes to changes, including full screen. The platform handles window dragging and the user's title-bar double-click behavior; `.window_drag(false)` keeps a child out of its parent's drag region. ```rust use nsh_core::{ desktop::TitleBarStyle, element::{desktop::{close_window, set_title_bar, title_bar}}, prelude::*, }; fn window_bar() -> impl IntoElement { set_title_bar(TitleBarStyle::Hidden, Some(px(42.0))); view() .h(px(42.0)) .flex_row() .items_center() .window_drag(true) .pl(move || title_bar().left + px(12.0)) .pr(move || title_bar().right + px(12.0)) .child(text("My app").flex_1()) .child(text("Close").window_drag(false).on_press(close_window)) } ``` `set_title_bar(TitleBarStyle::Default, None)` restores the system title bar. `close_window()` follows the embedding host's normal window-close behavior. The module has the window's other functions (`set_title`, `minimize`, `toggle_full_screen`, ...) and the menus (`set_application_menu`, `popup_menu`) too: [nsh/desktop](https://nsh.dev/versions/main/modules/nsh-desktop.md). ## Files and folders On the desktops, Rust code shows the system's file dialogs and hands local paths to the system, each answer awaited in a task: `pick_paths` with an `OpenDialog` (`OpenDialog::files()`, `::folders()` or `::files_and_folders()`), `pick_save_path` with a `SaveDialog`, `open_path` (the file's default app), `reveal_path` (the item selected in Finder, File Explorer or the file manager) and `move_to_trash` (where the item went, when macOS says). The dialogs answer `Ok(None)` when the user cancels and an error when they cannot show (on Linux without a file chooser portal, and on iOS, Android and the web, where `open_path`, `reveal_path` and `move_to_trash` answer an error too). Every request that fails answers its error, never `false`. ```rust use nsh_core::{ file_dialog::{FileFilter, OpenDialog}, prelude::*, }; fn open_notes() { let picked = pick_paths( OpenDialog::files() .multiple(true) .filter(FileFilter::new("Markdown", ["md", "markdown"])) .button_label("Open"), ); spawn(async move { match picked.await { Ok(Some(paths)) => { for path in paths { let _ = reveal_path(path).await; } } Ok(None) => {} // Cancelled. Err(error) => log::warn!("no file dialog: {error}"), } }); } ``` ## 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(e.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/main/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`). A function that starts listening is named `on(event, listener)` and returns `Stop` (`Stop::new(|| …)`), which TSX gets as a function that stops it (`() => void`); it runs once, when TSX calls it or when what called `on` goes (a component, or the module's run under hot reload). An event's handler gets one object: a variant's value is its field `value`, which `#[args(price)]` on the variant names (`onSelect({ price })`), and `#[args(open, reason)]` names the fields of a variant of several. An argument of the wrong type throws a `TypeError` in the call, or rejects the Promise of one that returns a Promise; an `Err` throws (or rejects) with its message. A class's object owns its Rust value: it is freed when the object is collected, or at once with `ledger.dispose()` (also `using ledger = new Ledger()`). Name the TSX entry to run it, `nsh run App.tsx` or `nsh package App.tsx --target …`: TSX edits are hot updates, Rust edits rebuild and restart the app. The CLI bundles and compiles the TSX itself, knowing your crate's exports from its description; the development hosts and `nsh test`'s headless runner are built with your crate and QuickJS. `examples/rust-exports` in the repository is the reference for exporting: a small TSX app whose crate exports one component (props required, defaulted and optional, an event, children), one module (a function of each kind) and one class (a constructor, methods, a getter and a setter, a static, an `async` member, another instance as an argument, a callback Rust posts to, `dispose()` and `using`), each with a short doc comment, and tests, in Rust and in TypeScript (`App.test.tsx`), that press through the app and check what each one shows. `examples/gallery-rs` in the repository is a complete Rust app: every demo is Rust, `nsh run examples/gallery-rs --target web` (or `ios`, `android`, …) runs its root, `nsh test examples/gallery-rs` runs its tests headless, and `cargo run -p gallery-native` starts it on the desktop backend. ### 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/main/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/main/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/main/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/main/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/main/docs/web.md#browser-apis-from-rust). ## See also - [Project structure](https://nsh.dev/versions/main/docs/project-structure.md): every key of `nsh.json`. - [Web](https://nsh.dev/versions/main/docs/web.md#apps-with-rust-components): Rust apps in the browser. - [Networking](https://nsh.dev/versions/main/docs/networking.md#from-rust-components): `fetch` from Rust. - [CLI](https://nsh.dev/versions/main/docs/cli.md#nsh-init): `nsh init --template rust` and `--template mixed`. Source: https://nsh.dev/versions/main/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, and its Rust crates' with cargo test check Type check an app with TypeScript's native compiler, and its Rust crates with cargo check 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/main/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