# Native Shell
> Native Shell: AI-generated UI, native in your app, on every platform. Apps on iOS, Android, macOS, Windows and Linux, GPUI apps and web pages load signed, sandboxed TSX modules at run time and draw them with the platform’s own UI.
Apps are TSX modules run by QuickJS on a native Rust runtime; the toolchain is the single `nsh` binary (`nsh init my-app`, `nsh run my-app/App.tsx`). Every page below is also served as Markdown at its URL plus `.md`.
---
# Introduction
Native Shell (NSH for short) is designed for **AI-generated, dynamically delivered native UI**. A model writes a small module in TSX, and a native app loads it at run time and shows it with the platform's own UI, inside a sandbox the app controls. The same mechanism makes native apps extensible in general — plugins, features, whole screens delivered after release.
NSH runs on phones and desktops alike: iOS (UIKit), Android (Android views), macOS (AppKit), Windows (Win32 windows drawn with Direct2D) and Linux (GTK 4). Each platform has a native backend of its own, and the same module runs on all of them ([Platforms](https://nsh.dev/docs/platforms.md)).
> [!IMPORTANT]
> NSH is the layer inside an app for UI that is generated, changed or added after it ships. On a phone or a desktop, the app stays a native app, built and released the way it always was.
## Why it exists
**First: UI generated by AI.** An assistant inside your app can answer with an interface rather than with text. A model writes a small TSX module (a form, a chart, a booking card), your server checks, builds and signs it, and the app downloads it and shows it in an `NshAppView`: native UI, sandboxed, limited to what you granted, and with no app update. NSH is built around this use, so its design is AI friendly throughout:
- **A format models write well.** A module is one TSX file using signals, a handful of host elements and Tailwind classes — the vocabulary models already know from the web, with no build configuration and no NPM.
- **A precise, generated contract.** Every element, prop, style key and module has TypeScript declarations generated from the Rust sources (`.nsh/types`), so generated code can be type-checked, and a model can be given the exact API instead of guessing. These docs are also served as plain text for models ([`llms.txt`](/llms.txt), [`llms-full.txt`](/llms-full.txt)).
- **Checkable before it ships.** `nsh trace` runs a module headless, without a device, and reports errors and the views it produced; build errors come with a code frame. A generation loop can compile, run, read the result and retry. The compiler also runs in-process as a library (`nsh_bundler::compile_module`) and in the browser ([Playground](/playground)).
- **Safe to run when it is not trusted.** Generated code is untrusted code. Each module runs in its own QuickJS VM with memory, call-time and node limits, reaches only the capabilities the host grants, and arrives as a signed package the app verifies first.
- **Native without native code.** The output is the platform's own UI — UIKit, Android views, AppKit, GTK 4 or Win32 with Direct2D — with the system's text, input and accessibility, yet the model never writes Swift, Kotlin, Java or Rust, and the app never needs a new release.

**Second: extending native apps, and delivering UI dynamically.** The same mechanism serves any module, not only generated ones. The app embeds an `NshAppView` (iOS), `NshView` (Android) or `AppView` (macOS, Windows, Linux) wherever a module should appear, and NSH runs the module inside it:
- **Delivered after release.** A module is a signed package (`.nshpkg`) the app downloads at run time. The app checks its signature against the keys it trusts and against a revocation list before running it ([Signed packages](https://nsh.dev/docs/signed-packages.md)). A change needs no new binary and no store release.
- **The host stays in control.** Each module runs in its own sandboxed VM, with memory, time and node limits, and reaches only what the host grants: network hosts, web pages, the status bar ([Sandbox and capabilities](https://nsh.dev/docs/sandbox-and-capabilities.md)). Several modules can run side by side; there are no global singletons.
- **It looks like the rest of the app.** Modules render with the platform's own UI, so text, input, accessibility and scrolling are the system's. The host can register its own native views — a map, a player, a design-system component — for modules to use ([Native components](https://nsh.dev/docs/native-components.md)).
**Third: standalone apps.** The same modules can also be a whole app. `nsh package` turns them into an installable iOS, Android, macOS, Windows or Linux app with the prebuilt runtime, which is also the quickest way to try NSH: the [getting started](https://nsh.dev/docs/getting-started.md) guide does exactly that. It is a supported use, but not the one NSH is designed around.
## Why Native Shell, next to GPUI Shell
[Longbridge](https://longbridge.com) is building NSH as the plugin layer of its mobile client, and its desktop client needs the same layer. [GPUI Shell](https://gpui-kit.com/shell) makes [GPUI](https://gpui-kit.com) desktop apps extensible in JavaScript, and the first idea was to use it on phones too. It does not fit there, because its modules are drawn by GPUI itself, on the GPU, not with the platform's views:
- **System behavior would have to be rebuilt.** A self-drawn surface inside a native screen has none of the platform's text selection, nor the selection menu that comes with it (Copy, Share, Translate, Look Up, Select All), nor the system's input methods, accessibility and new visual materials each iOS release brings. Recreating them in a GPU renderer is a large, never-finished job, and the copies drift from the real thing.
- **Mobile teams would have to adopt a new UI stack.** Running a self-drawn layer at scale in an existing mobile client means its iOS and Android teams take on an unfamiliar renderer, its compatibility issues across OS versions and devices, and its bugs.
NSH therefore renders modules with each platform's own UI: UIKit on [iOS](https://nsh.dev/docs/embedding-ios.md), Android views on [Android](https://nsh.dev/docs/embedding-android.md), AppKit on [macOS](https://nsh.dev/docs/macos.md), Win32 windows drawn with Direct2D on [Windows](https://nsh.dev/docs/windows.md) and GTK 4 on [Linux](https://nsh.dev/docs/linux.md). One module format and one runtime run in all these hosts.
The two are positioned differently. GPUI Shell is for extending GPUI desktop applications; Native Shell is for native apps on every platform, phones and desktops alike.
## A module
A module is TypeScript and JSX:
```tsx
import { mount, signal, Text, View } from "nsh";
function Counter() {
const count = signal(0);
return (
{count()} count.update((n) => n + 1)}
activeOpacity={0.6}
class="rounded-lg bg-blue-600 px-5 py-3"
>
Tap
);
}
mount(Counter);
```
`` becomes a `UIView` on iOS, a `ViewGroup` on Android, an `NSView` on macOS and a GTK widget on Linux, `` a native text view, and the button's press feedback (`activeOpacity`) runs on the main thread without a round trip to JavaScript. When you tap, `count.update` changes one signal, and only the text that reads `count()` is updated: the component function does not run again.
The host app shows it with a few lines of native code ([iOS](https://nsh.dev/docs/embedding-ios.md), [Android](https://nsh.dev/docs/embedding-android.md), [macOS](https://nsh.dev/docs/macos.md#embedding-in-a-mac-app), [Windows](https://nsh.dev/docs/windows.md#embedding-in-a-win32-app), [Linux](https://nsh.dev/docs/linux.md#embedding-in-a-gtk-app)). On iOS:
```swift
let app = NshAppView() // a UIView: place it anywhere
try app.grantCapabilities(#"{ "network": { "hosts": ["api.example.com"] } }"#)
let info = try await app.loadPackage(at: downloadedFile, trust: trust) // verified, then run
```
Only the module's code is JavaScript. Everything it calls into — the reactive graph, the renderer, layout, text and every module it imports — is native, written in Rust.
## How a module runs
A module (an "app" in the CLI's terms) is a set of `.tsx` / `.ts` files with an entry, usually `App.tsx`. The toolchain is a single `nsh` binary:
1. **Build.** `nsh` bundles the module's files (with rolldown and Oxc) and compiles the JSX with its own compiler. Imports of `nsh` and `nsh/*` stay imports: the runtime serves them natively, so the bundle holds only your code. The bundle is compiled to QuickJS bytecode.
2. **Run.** On the device, the engine thread runs the bytecode in [QuickJS](https://github.com/quickjs-ng/quickjs). Compiled JSX calls the native renderer directly; changes are collected during one JavaScript turn, laid out with [Taffy](https://github.com/DioxusLabs/taffy) (flexbox and CSS grid), and handed to the main thread as one batch of view operations per frame.
3. **Render.** The main thread applies the batch to native views. Text is measured and laid out once on the engine thread (CoreText on Apple platforms, `StaticLayout` on Android, DirectWrite on Windows, Pango on Linux) and only drawn on the main thread.
Because JavaScript only describes the UI and handles events, the work done in the interpreter is small. Parsing styles, resolving Tailwind classes, diffing, layout and text all happen in Rust.
> [!NOTE]
> The main thread never waits for JavaScript. Questions the system asks synchronously (may this input accept another character, may the user swipe back, may this sheet close) are answered natively from props you set beforehand, such as `maxLength`, `preventBack` or `dismissible`. Events reach your code afterwards.
## What you get
- **Embedding.** `NshAppView` (Swift), `NshView` (Java/Kotlin) and the desktops' `AppView` (AppKit, GTK, Win32) put a module in a native app, as many as you like ([iOS](https://nsh.dev/docs/embedding-ios.md), [Android](https://nsh.dev/docs/embedding-android.md), [macOS](https://nsh.dev/docs/macos.md#embedding-in-a-mac-app), [Linux](https://nsh.dev/docs/linux.md#embedding-in-a-gtk-app), [Windows](https://nsh.dev/docs/windows.md#embedding-in-a-win32-app)).
- **Signed, revocable delivery.** Ed25519-signed packages, release keys certified by an offline root key, revocation lists ([Signed packages](https://nsh.dev/docs/signed-packages.md)).
- **A sandbox the host configures.** VM limits and capabilities granted per view, in the iOS and Android SDKs ([Sandbox and capabilities](https://nsh.dev/docs/sandbox-and-capabilities.md)).
- **The host's own views in modules.** Register native components from Swift, Java or Rust ([Native components](https://nsh.dev/docs/native-components.md)).
- **Native views.** Embedding a native map, web view or video player has no layering problems, accessibility uses the platform's own tree, and text input is the system's.
- **Fine-grained reactivity.** [Signals](https://nsh.dev/docs/reactivity.md) like Solid's, implemented natively. Components run once; only the expressions that read a changed signal run again.
- **Styles and Tailwind**, resolved natively: flexbox, CSS grid, shadows, gradients, filters, transitions ([Styling](https://nsh.dev/docs/styling.md), [Tailwind](https://nsh.dev/docs/tailwind.md)).
- **A mobile UI set** (`nsh/ui/mobile`) whose components are written in Rust and drawn by NSH, so they look the same on every platform: [Button](https://nsh.dev/components/button.md), [Card](https://nsh.dev/components/card.md), [ListView](https://nsh.dev/components/list-view.md), [navigation](https://nsh.dev/components/navigation.md), and more. Desktop layouts are built from the primitives with `desktop={{…}}` props and `Platform.select` ([Platform-specific code](https://nsh.dev/docs/platform-specific.md)), and a desktop UI set is on the way.
- **One binary toolchain.** A single `nsh` executable builds modules, writes their type declarations, serves them with hot module replacement, packages and signs them.
## Where it runs
| Platform | Status | Backend | Notes |
|---|---|---|---|
| iOS | Supported (iOS 15.0+) | UIKit | Devices and the simulator; embedded with `NativeShell.xcframework` ([iOS](https://nsh.dev/docs/embedding-ios.md)) |
| Android | Supported (API 28, Android 9+) | Android views | `arm64-v8a` and `x86_64`; embedded with `nsh.aar` ([Android](https://nsh.dev/docs/embedding-android.md)) |
| macOS | Supported (macOS 14+) | AppKit | Standalone apps; embedded with `AppView` ([macOS](https://nsh.dev/docs/macos.md)) |
| Windows | Supported (Windows 10 1809+) | Win32, Direct2D | Standalone apps; embedded with `AppView` ([Windows](https://nsh.dev/docs/windows.md)) |
| Linux | Supported (GTK 4.14+) | GTK 4 | Standalone apps; embedded with `AppView` ([Linux](https://nsh.dev/docs/linux.md)) |
| GPUI (macOS, Windows, Linux) | Supported | GPUI | Standalone apps; embedded with `AppView` in GPUI and GPUI Kit apps ([GPUI](https://nsh.dev/docs/gpui.md)) |
| Web | Supported (modern browsers) | HTML | Static sites; embedded in a page with `createPlayer` or an iframe ([Web](https://nsh.dev/docs/web.md)) |
| Mac Catalyst | For development | UIKit | The iPhone-sized window `nsh run` opens on a Mac, to see a module as on a phone |

## How it compares
The usual ways to put UI into an app after release are web views, server-driven UI described in JSON, and mini-program containers; for whole apps, cross-platform frameworks such as React Native, Flutter, Electron and Tauri. An NSH module renders native UI where a web view renders a page, carries its own state and logic where a server-driven schema can only recombine built-in components, and runs in a sandbox the host configures in code. [Comparing approaches](https://nsh.dev/docs/comparison.md) goes through each.
## Next steps
- [Installation](https://nsh.dev/docs/installation.md) and [Getting started](https://nsh.dev/docs/getting-started.md): get `nsh`, write a first module and run it on each platform.
- [Platforms](https://nsh.dev/docs/platforms.md): what runs where, and how to run, package and embed on iOS, Android, macOS, Windows and Linux.
- [Signed packages](https://nsh.dev/docs/signed-packages.md) and [Sandbox and capabilities](https://nsh.dev/docs/sandbox-and-capabilities.md): deliver modules and decide what they may do.
- [JSX and components](https://nsh.dev/docs/jsx-and-components.md) and [Reactivity](https://nsh.dev/docs/reactivity.md): the two ideas everything else builds on.
Source: https://nsh.dev/docs
---
# Comparing approaches
An app that wants UI it did not ship with — answers generated by a model, features from partners or other teams, screens that change without a release — has a handful of established options, on phones and desktops alike. They differ in what draws the UI, what the delivered code may do, and how much of the app has to change to host them. This page compares them with NSH, and then compares NSH with the cross-platform frameworks used to write whole apps.
## The options
| | What draws it | What it can express | Confinement | Fit for generated UI | Cost to the host app |
|---|---|---|---|---|---|
| **Web view (H5)** | A browser engine | Anything HTML, CSS and JS can | Same-origin rules; native access through a hand-written bridge | Models write HTML well; the result looks and behaves like a web page | A browser engine per view; a bridge to maintain |
| **Server-driven UI** | The app's own native views | Only the components and actions the app built in, described in JSON | Total: the schema is the limit | Easy to generate, little to express: no local state or logic beyond what the schema provides | A renderer for every component and its versions, in each app |
| **Mini-program containers** | Usually a web engine with a proprietary layer | A platform-specific framework and API | The container's permission model | Proprietary dialect, less known to models | A large container SDK |
| **Over-the-air updates of a cross-platform app** (React Native with a code-push service, for example) | That framework's renderer | The whole framework | None: updates are trusted app code | Code generated per request is not what these updates are for | The app has to be written in that framework |
| **NSH** | The platform's own UI: UIKit, Android views, AppKit, GTK 4, Win32 with Direct2D | Components with state, logic, animation and layout; host-registered native views | Signed packages, a VM per view with host-set limits, capabilities granted per view | Designed for it: one TSX module, typed API, headless checks | One view and a library in an existing native app |
## How NSH differs
- **The platform's own UI.** A module is made of the platform's views, so text selection and its menu, input methods, accessibility and scrolling are the system's own — the same reason NSH exists next to [GPUI Shell](https://nsh.dev/docs.md#why-native-shell-next-to-gpui-shell).
- **More than a schema.** Server-driven UI only recombines what the app already ships. An NSH module has its own state, logic, animations and layout, so a model can produce an interface the app's developers never anticipated.
- **Confined by the host.** Delivered code is untrusted. NSH checks signatures and revocations before running anything, gives each view its own VM with memory, call-time and node limits, and lets the host decide which network hosts, web pages and system settings a module may reach ([Sandbox and capabilities](https://nsh.dev/docs/sandbox-and-capabilities.md)).
- **Embedded, not adopted.** The app stays native and keeps its toolchain; NSH is a view placed where a module should appear ([Platforms](https://nsh.dev/docs/platforms.md#embed-in-a-native-app)).
- **One module, every platform.** The same bundle runs on iOS, Android, macOS, Windows and Linux, each time with that platform's own UI ([Platforms](https://nsh.dev/docs/platforms.md)).
## Cross-platform frameworks
NSH is designed for modules inside native apps, but the same modules can be packaged as whole apps for every platform, so it is natural to compare it with the frameworks people write cross-platform apps in. The comparison is about how each draws and where it runs, not about maturity: these frameworks have large ecosystems that NSH does not have.
| | What draws it | Platforms | App code | Code delivered after release |
|---|---|---|---|---|
| **React Native** | The platform's views on iOS and Android; desktops through separate projects | iOS, Android; Windows and macOS out of tree | JavaScript or TypeScript, with React | Over-the-air updates of trusted app code |
| **Flutter** | Its own engine draws every pixel | iOS, Android, macOS, Windows, Linux, web | Dart | Not part of the framework |
| **Electron** | Chromium, bundled with the app | macOS, Windows, Linux | HTML, CSS, JavaScript | Web content the app loads |
| **Tauri** | The system's web view | macOS, Windows, Linux, iOS, Android | HTML, CSS, JavaScript; Rust for the native side | Web content the app loads |
| **NSH** | The platform's own UI on each platform | iOS, Android, macOS, Windows, Linux | TSX modules; the runtime and components are Rust | Signed packages, verified and sandboxed per view |
What sets NSH apart:
- **Native UI on every platform, not one renderer everywhere.** Flutter draws everything itself, and Electron and Tauri show web pages; NSH builds each platform's own views, so text selection, input methods, accessibility and the system's look come from the platform.
- **Small modules inside native apps.** The frameworks above own the whole app. NSH is designed to sit inside an app written with the platform's own tools, as one view among others, and to run modules the app did not ship with, in a sandbox.
- **A light runtime.** No browser engine per app or per view; JavaScript only runs the module's own code, and everything it calls is native Rust.
## When another option fits better
- The UI is a document or an existing web page: a web view shows it as it is.
- The set of screens is fixed and small, and only content and order change: server-driven UI needs no runtime at all.
- The whole app is written in a cross-platform framework already: its own update mechanism ships its code.
- You need a large ecosystem of third-party packages, or a desktop UI kit with desktop controls today: NSH's components are the mobile set.
## See also
- [Introduction](https://nsh.dev/docs.md): why NSH exists and what it is designed for.
- [Architecture](https://nsh.dev/docs/architecture.md): what runs where inside NSH.
Source: https://nsh.dev/docs/comparison
---
# Installation
Everything NSH does goes through one command, `nsh`. It bundles and compiles apps, runs the dev server, launches development hosts and packages installable apps. You can use it from a prebuilt distribution, which needs no Rust, or build it from a checkout of the repository.
On top of `nsh`, 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/docs/gpui.md) | macOS, Windows or Linux, for that OS | A GPU (Metal, DirectX or Vulkan); on Linux, Wayland or X11 |
Mac Catalyst, `nsh run`'s default on macOS, 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.
## The prebuilt distribution
The distribution is a directory, `dist/nsh`, with the CLI and the prebuilt native runtimes it packages apps with:
```sh
dist/nsh/bin/nsh # the CLI
dist/nsh/runtime/ios/iphoneos/nsh-ios-app # runs packaged apps on iOS devices
dist/nsh/runtime/ios/iphonesimulator/{nsh-ios-app,nsh-ios-host}
dist/nsh/runtime/ios/maccatalyst/nsh-ios-host # the Mac Catalyst development host
dist/nsh/runtime/android/{release,dev}/ # classes.dex, lib//libnsh.so
dist/nsh/runtime/macos/{nsh-macos-host,nsh-macos-app} # the macOS (AppKit) development host and release runner
dist/nsh/runtime/linux/{nsh-linux-host,nsh-linux-app} # the Linux development host and release runner
dist/nsh/runtime/windows/{nsh-windows-host,nsh-windows-app}.exe # the Windows development host and release runner
dist/nsh/runtime/gpui/{nsh-gpui-host,nsh-gpui-app}[.exe] # the GPUI development host and release runner, 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/docs/gpui.md) runtime, built for that machine. All include the Android runtime (on Windows only when the Android SDK is found).
## Building from a checkout
You need Rust through [rustup](https://rustup.rs). The repository's `rust-toolchain.toml` selects the stable toolchain and installs the iOS and Android targets on first use.
```sh
git clone https://github.com/longbridge/nsh.git
cd nsh
cargo build -p nsh-cli # ./target/debug/nsh
```
Then build the development host of each target you want to run on, once, and again after you update the checkout:
```sh
cargo build -p nsh-ios-host --target aarch64-apple-ios-macabi # nsh run (Mac Catalyst window)
cargo build -p nsh-ios-host --target aarch64-apple-ios-sim # nsh run --target ios
cargo xtask android --dev # nsh run --target android
cargo build -p nsh-macos-host # nsh run --target macos (built for you in a checkout)
cargo build -p nsh-linux-host # nsh run on Linux (built for you in a checkout)
cargo build -p nsh-windows-host # nsh run on Windows (built for you in a checkout)
cargo build --manifest-path crates/gpui/Cargo.toml --target-dir target -p nsh-gpui-host # nsh run --target gpui (built for you in a checkout)
cargo build --profile web -p nsh-platform-web --target wasm32-unknown-unknown # nsh run --target web (built for you in a checkout)
```
For `nsh package`, build the release runners as well:
```sh
cargo build -p nsh-ios-app --target aarch64-apple-ios-sim # nsh package --target ios-simulator
cargo build -p nsh-ios-app --target aarch64-apple-ios # nsh package --target ios
cargo xtask android # nsh package --target android
cargo build -p nsh-macos-app # nsh package --target macos
cargo build -p nsh-linux-app # nsh package --target linux
cargo build -p nsh-windows-app # nsh package --target windows
cargo build --manifest-path crates/gpui/Cargo.toml --target-dir target -p nsh-gpui-app # nsh package --target gpui
# 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/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/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/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 trace`, `nsh dev`, `nsh check` | Yes | Yes | Yes |
| `nsh run` (Mac Catalyst, the default on macOS) | 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`, `nsh package --target gpui` (a package for the OS it is made on) | 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
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/docs/project-structure.md#tsconfigjson-and-nshtypes).
## Next steps
Continue with [Getting started](https://nsh.dev/docs/getting-started.md) to create an app and run it.
Source: https://nsh.dev/docs/installation
---
# Getting started
NSH's main use is hosting modules inside a native app (see [Introduction](https://nsh.dev/docs.md)). This guide follows that path: write a module, develop it with hot reload on the platforms you target, show it in your app, and ship an update as a signed package. It assumes `nsh` is installed and on your `PATH` ([Installation](https://nsh.dev/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 module in NSH's own development host: in a window on your computer, in the iOS simulator or on an Android device. You can stop there, or [package it as a standalone app](#or-ship-it-as-a-standalone-app).
## Create a module
```sh
nsh init my-app
```
A module is what a host app loads: one or more `.tsx` files with an entry. `nsh init` creates the directory if it is missing and writes:
| File | What it is |
|---|---|
| `App.tsx` | The entry module: renders the app with `mount` |
| `nsh.json` | The app's name, id, version and build number ([Project structure](https://nsh.dev/docs/project-structure.md#nshjson)) |
| `tsconfig.json` | TypeScript settings that resolve `nsh` to the declarations in `.nsh/types` |
| `.gitignore` | Ignores `.nsh/` and `out/` |
| `.nsh/types/` | Generated declarations of every NSH module, for your editor |
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. Files that already exist are kept, so you can run `nsh init` in a directory that has some of them.
### The entry module
```tsx
import { mount, signal, Text, View } from "nsh";
import { SafeAreaView } from "nsh/ui/mobile";
function App() {
const count = signal(0);
return (
{count()} count.update((c) => c + 1)}
activeOpacity={0.6}
accessibilityRole="button"
style={{ paddingHorizontal: 20, paddingVertical: 12, borderRadius: 10, backgroundColor: "#0969da" }}
>
Tap
);
}
mount(App);
```
Line by line:
- **`import { mount, signal, Text, View } from "nsh"`**: `nsh` is the core module: [signals](https://nsh.dev/docs/reactivity.md), `mount`, the other reactive primitives, and the host elements `View` and `Text`. It is served by the native runtime, not bundled.
- **`import { SafeAreaView } from "nsh/ui/mobile"`**: the mobile UI set. [`SafeAreaView`](https://nsh.dev/components/safe-area-view.md) pads its content so it stays clear of the notch, the status bar and the home indicator; on desktops, where the safe area is 0, it adds no padding.
- **`function App()`** is a component: a function that returns JSX. It runs **once**. `signal(0)` makes a value that changes over time.
- **`{count()}`** reads the signal inside JSX. The compiler sees a call and makes this text follow the signal: when `count` changes, the runtime updates this one text, nothing else.
- **``** is a native view that reacts to presses; `activeOpacity` fades it while pressed, natively. `View`, `Text`, `Image`, `Scroll`, `Input` and the other [host elements](https://nsh.dev/elements.md) are imported from NSH's modules, like any component; only `If` and `Each` need no import.
- **`style={{ … }}`** takes an object of camelCase style properties, with numbers in points. Layout is flexbox, and children stack in a column by default ([Layout](https://nsh.dev/docs/layout.md)).
- **`mount(App)`** renders the component into the app's surface.
> [!IMPORTANT]
> Text must be inside ``. A bare string in a `` (`Hello`) is not displayed.
## Develop it
### Run it
`nsh run` starts a dev server, builds the app, launches a development host that loads it, and keeps rebuilding while you edit. Logs from the app (`console.log`, errors) appear in the terminal.
Without `--target`, `nsh run` uses the machine you are on: a Win32 window on Windows, a GTK window on Linux, and on macOS a Mac Catalyst window that shows the app as an iPhone would. Pick another platform with `--target`:
| Target | Command | Runs from |
|---|---|---|
| iOS simulator | `nsh run my-app/App.tsx --target ios` | macOS with Xcode |
| Android | `nsh run my-app/App.tsx --target android` | macOS, Windows, Linux |
| macOS (AppKit) | `nsh run my-app/App.tsx --target macos` | macOS |
| Windows | `nsh run my-app/App.tsx` | Windows (the default) |
| Linux (GTK 4) | `nsh run my-app/App.tsx` | Linux (the default) |
| Mac Catalyst | `nsh run my-app/App.tsx` | macOS (the default) |
#### iOS simulator
```sh
nsh run my-app/App.tsx --target ios
nsh run my-app/App.tsx --target ios --device "iPhone 16 Pro"
```
`nsh` uses a booted simulator, or boots the newest iPhone if none is running, and installs the development host into it. `--device` picks a simulator by name or UDID.
#### Android
```sh
nsh run my-app/App.tsx --target android
```
Start an emulator or connect a device first (`adb devices` lists them; NSH uses the first one). `nsh` installs a development host APK (rebuilt only when the runtime changes) and forwards the dev server's port to the device with `adb reverse`, so it works over USB without a shared network.
#### macOS (AppKit)
```sh
nsh run my-app/App.tsx --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/docs/macos.md).
#### Windows (default on Windows)
```sh
nsh run my-app/App.tsx
```
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/docs/windows.md).
#### Linux (default on Linux)
```sh
nsh run my-app/App.tsx
```
The app opens in a GTK window (390×844 points). It needs GTK 4.14 or later; see [Linux](https://nsh.dev/docs/linux.md).
#### Mac Catalyst (default on macOS)
```sh
nsh run my-app/App.tsx
```
The app opens in an iPhone-sized window (390×844 points) on your Mac. This is the quickest way to see a module 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 with hot reload
Leave `nsh run` running and change the label in `App.tsx`:
```tsx
Add one
```
Save. The CLI rebuilds and sends a patch; 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. State survives when you edit a component that does not own it. Move the button into a component of its own:
```tsx
function TapButton(props: { onPress: () => void }) {
return (
Tap
);
}
```
Use ` count.update((c) => c + 1)} />` in `App`, tap a few times, then change `TapButton`'s color: the button updates and 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 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
Your app shows a module in a view of its own: `NshAppView` (a `UIView`) on iOS, `NshView` (an Android `View`) on Android, `AppView` on macOS, Windows and Linux. Place it like any other view, on any screen, as many times as you need. During development, point it at the dev server; in a release, load a built bundle or a downloaded package.
### iOS
Link `NativeShellDev.xcframework` in Debug and `NativeShell.xcframework` in Release ([Embedding in iOS apps](https://nsh.dev/docs/embedding-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: "qbc", subdirectory: "nsh")!)
#endif
```
`nsh dev my-app/App.tsx` 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/App.tsx --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/docs/embedding-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/App.tsx --release --out app/src/main/assets/nsh`
}
```
### macOS, Windows and Linux
Desktop apps written in Rust embed modules 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 loads a built bundle. See [macOS](https://nsh.dev/docs/macos.md#embedding-in-a-mac-app), [Windows](https://nsh.dev/docs/windows.md#embedding-in-a-win32-app), [Linux](https://nsh.dev/docs/linux.md#embedding-in-a-gtk-app) and [GPUI](https://nsh.dev/docs/gpui.md), and [Platforms](https://nsh.dev/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 the 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/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/docs/native-components.md).
## Ship updates as signed packages
A 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/App.tsx --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/docs/signed-packages.md).
## Or ship it as a standalone app
The same module can also be a whole app. `nsh package` builds an installable app around it from the prebuilt runtime, 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
```
This is a supported use on every platform, and the quickest way to put a module on a device or a desktop, but NSH is designed around hosting modules in native apps. Options, signing and icons: [Packaging](https://nsh.dev/docs/packaging.md).
## Try it in the terminal
`nsh trace` runs an app headless (no device) and prints the native view operations it produces and their timings, then taps the first pressable view twice:
```sh
nsh trace my-app/App.tsx
```
It is a quick way to check that an app starts without errors, and to see how much work an interaction causes ([Testing and trace](https://nsh.dev/docs/testing-and-trace.md)).
## Next steps
- [Platforms](https://nsh.dev/docs/platforms.md): what each platform supports, and its own page ([iOS](https://nsh.dev/docs/embedding-ios.md), [Android](https://nsh.dev/docs/embedding-android.md), [macOS](https://nsh.dev/docs/macos.md), [Windows](https://nsh.dev/docs/windows.md), [Linux](https://nsh.dev/docs/linux.md)).
- [Signed packages](https://nsh.dev/docs/signed-packages.md) and [Sandbox and capabilities](https://nsh.dev/docs/sandbox-and-capabilities.md): delivery and what modules may do.
- [JSX and components](https://nsh.dev/docs/jsx-and-components.md) and [Reactivity](https://nsh.dev/docs/reactivity.md): writing modules.
- [Project structure](https://nsh.dev/docs/project-structure.md): what goes in `nsh.json`, assets, files.
Source: https://nsh.dev/docs/getting-started
---
# Project structure
An NSH app is a directory with an entry module and an optional `nsh.json`. Everything else (more modules, images, fonts, JSON files) is found by following the entry's imports, so there is no file list to maintain.
```text
my-app/
├── App.tsx the entry module: calls mount()
├── nsh.json name, id, version, icon, Tailwind theme, capabilities
├── tsconfig.json TypeScript settings for editors
├── .gitignore .nsh/ and out/
├── 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
└── out/ generated: nsh build / nsh package output
```
Every command takes the entry as its argument (`nsh run my-app/App.tsx`). The directory that contains the entry is the app's root: `nsh.json` is read from there, asset paths are relative to it, and `.nsh/` is written there.
## The entry module
The entry is a normal module that ends by rendering the root component:
```tsx
import { mount } from "nsh";
import { Home } from "./components/Home";
function App() {
return ;
}
mount(App);
```
`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 next to the entry. Every key is optional; `nsh init` writes the first four.
```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` | Bundle identifier / application id, e.g. `com.example.app`. Default: `com.example.`. |
| `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. |
| `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/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/docs/device-and-environment.md#dark-mode)). |
| `theme` | string or object | every build | The theme of the `nsh/ui/mobile` components: a theme file's path relative to `nsh.json`, or the theme inline ([Components](https://nsh.dev/components.md)). |
| `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/docs/sandbox-and-capabilities.md)). |
The flags of `nsh package` and `nsh publish` (`--name`, `--id`, `--app-version`, `--build`, `--icon`) override the file's values ([CLI](https://nsh.dev/docs/cli.md)).
`tailwind`, `theme`, `appearance` and `capabilities` 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`
```
> [!WARNING]
> `nsh package` and `nsh publish` read `nsh.json` strictly and currently reject keys they do not know, including `theme`. If you set a component theme, package from a copy of the app without it until this is fixed.
### 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/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`, `trace`, `dev`, `run`, `package`, `publish`) writes the TypeScript declarations of everything NSH provides into `.nsh/types`, and `tsconfig.json` points the `nsh` imports there:
```json
{
"compilerOptions": {
"target": "ES2023",
"lib": ["ES2023"],
"module": "ESNext",
"moduleResolution": "Bundler",
"jsx": "preserve",
"jsxImportSource": "nsh",
"strict": true,
"noEmit": true,
"isolatedModules": true,
"skipLibCheck": true,
"types": [],
"paths": {
"nsh": ["./.nsh/types/nsh/index.d.ts"],
"nsh/*": ["./.nsh/types/nsh/*.d.ts"]
}
},
"include": ["**/*.ts", "**/*.tsx", ".nsh/types/nsh/globals.d.ts", ".nsh/types/nsh/assets.d.ts"]
}
```
| File in `.nsh/types/nsh/` | Declares |
|---|---|
| `index.d.ts` | `nsh`: `signal`, `computed`, `effect`, `mount`, `View`, `Text`, ... and re-exports the types below |
| `types.d.ts` | Styles (`ViewStyle`, `TextStyle`, ...), every element's props and events, `YNode` (what a `ref` receives) |
| `jsx-runtime.d.ts` | The JSX runtime TypeScript resolves through `jsxImportSource` (the `JSX` namespace) |
| `globals.d.ts` | `If` and `Each`; `console`, timers, `requestAnimationFrame`, `fetch` and the other [global APIs](https://nsh.dev/modules/globals.md) |
| `assets.d.ts` | What importing an image or font gives |
| `list.d.ts`, `animated.d.ts`, `device.d.ts`, ... | One file per module (`nsh/list`, `nsh/animated`, `nsh/device`, ...) |
| `ui/mobile.d.ts` | `nsh/ui/mobile` |
`lib` is `ES2023` without `DOM`: there is no `document` or `window`. The files 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 trace App.tsx`.
`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.
## Modules
Split the app into as many `.ts` and `.tsx` modules as you like and import them with relative paths; the extension may be left out:
```tsx
import { Header } from "./components/Header";
import { formatPrice } from "./lib/format";
import strings from "./strings.json";
```
All of them are bundled into one script. JSON files import as their parsed value.
### Loading code on demand
`import()` of one of the app's modules returns a promise of its exports, as in a browser:
```tsx
const open = async () => {
const { Settings } = await import("./screens/Settings");
screen.set(() => Settings);
};
```
Apps on devices ship one bundle, so the module is already there and the promise resolves at once. A web app published split (`nsh build --split`) gets the module, and what only it uses, as a separate file that is downloaded the first time `import()` asks for it ([Web](https://nsh.dev/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`; `View`, `Text`, `Scroll`, `Input` ([reference](https://nsh.dev/modules/nsh.md)) |
| `nsh/ui/mobile` | The mobile UI set: components, navigation, alerts, safe area ([Components](https://nsh.dev/components.md)) |
| `nsh/list` | `List`: virtualized, recycling lists |
| `nsh/animated` | Native animations |
| `nsh/canvas` | 2D drawing |
| `nsh/gesture` | `PanResponder` |
| `nsh/device` | `Platform`, `windowSize`, `colorScheme`, `fontScale`, `PixelRatio`, `I18nManager` |
| `nsh/accessibility` | Screen reader and reduce motion state, announcements |
| `nsh/image` | `Image` (the element), `Image.getSize`, `Image.prefetch` |
| `nsh/webview`, `nsh/video` | `WebView`, `Video` |
| `nsh/native` | Components the embedding app registers |
| `nsh/ios`, `nsh/android` | Platform-only APIs |
The [modules reference](https://nsh.dev/modules.md) documents each one. Importing a module NSH does not have (`nsh/foo`) is a build error.
### 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.
`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 are imported like modules. The bundler records each imported file and ships it with the app: `nsh build` copies them to `out/assets/`, `nsh package` puts them in the app, and during development the dev server serves them.
```tsx
import { Text } from "nsh";
import { Image } from "nsh/image";
import logo from "./assets/logo@2x.png"; // { uri: "asset:/assets/logo@2x.png", width: 64, height: 64 }
import Inter from "./assets/Inter.ttf"; // "Inter"
Hello
```
| Extension | Import gives |
|---|---|
| `.png`, `.jpg`, `.jpeg`, `.gif`, `.webp`, `.heic` | An `ImageAsset`: `{ uri, width, height }`, sized in points. An `@2x` / `@3x` suffix divides the pixel size by 2 / 3. |
| `.ttf`, `.otf` | The font's family name, a string. The font is registered before the first frame. |
| `.json` | The parsed JSON |
See [Images and assets](https://nsh.dev/docs/images-and-assets.md) and [Fonts](https://nsh.dev/docs/fonts.md).
## Generated directories
- **`.nsh/`**: the type declarations (`.nsh/types`) and what `nsh run` installs on simulators and devices (the Catalyst host app, the Android development APK). Safe to delete; it is recreated.
- **`out/`**: the default output of `nsh build` (`bundle.js`, `bundle.js.map`, `bundle.nshc`, `capabilities.json`, `assets/`) and `nsh package` (`.app`, `.ipa`, `.apk`). Change it with `-o`.
Source: https://nsh.dev/docs/project-structure
---
# CLI
`nsh` is the only tool an NSH app needs. Every subcommand that builds an app takes `[ENTRY]`: the entry module, or the app's directory, whose `App.tsx` is the entry (default: the current directory, so `cd my-app && nsh run` works). It reads `nsh.json` next to the entry and refreshes the type declarations in `.nsh/types`.
```text
nsh
init Create an app
build Bundle an app and compile it to QuickJS bytecode
trace Run an app headless and print the mount instructions and timings
check Type check an app with TypeScript's native compiler
dev Serve an app to dev hosts (simulators, devices) and push rebuilds
run Like dev, and also run the app in a development host
package Package an installable app from the prebuilt runtime
keygen Create a signing key
certify Certify a release key with a root key
publish Build an app in release mode and sign it into a package (.nshpkg)
verify Verify a package
revoke Sign a revocation list
```
`nsh --help` prints the flags of a command; `nsh --version` prints the version.
## 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)
→ dev server ws://127.0.0.1:8790/ws
devices on this network: ws://192.168.1.20:8790/ws
✓ 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 ]
```
Creates an app in `DIR` (default: the current directory, created if missing): `App.tsx`, `nsh.json`, `tsconfig.json`, `.gitignore` and `.nsh/types`. Files that exist are kept; it fails only if all of them exist already.
| 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). |
## nsh run
```sh
nsh run [ENTRY] [--target catalyst|ios|android|macos|linux|windows|gpui|web] [--device ] [--port ] [--release] [--trace] [--fps]
```
Starts the dev server (as `nsh dev`), launches a development host that loads the app from it, and rebuilds on every save. Hot module replacement patches the running app in place; logs and errors from the app are printed in the terminal. Stop it with Ctrl-C (or by closing the Catalyst, macOS, Linux, Windows or GPUI window). However it ends (Ctrl-C, `kill` / `pkill`, an error), it stops the host it started: the Catalyst app, the macOS, Linux, Windows or GPUI host, the development host in the simulator, or on the Android device.
| Flag | Default | Description |
|---|---|---|
| `--target ` | `catalyst` on macOS, `linux` on Linux, `windows` on Windows | `catalyst`: the Mac Catalyst host, an iPhone-sized window (390×844). `ios`: the host in an iOS simulator. `android`: the host on an Android device or emulator. `macos`: the AppKit host, a 1200×800 window ([macOS](https://nsh.dev/docs/macos.md)). `linux`: the GTK host, a 390×844 window ([Linux](https://nsh.dev/docs/linux.md)). `windows`: the Win32 host, a 1200×800 window ([Windows](https://nsh.dev/docs/windows.md)). `gpui`: the GPUI host, a 1200×800 window, on macOS, Windows or Linux ([GPUI](https://nsh.dev/docs/gpui.md)). `web`: the dev server's page, opened in the browser ([Web](https://nsh.dev/docs/web.md#develop-in-the-browser)). |
| `--device ` | | iOS simulator to use, by name (`"iPhone 16 Pro"`) or UDID. Default: a booted one, else the newest iPhone, which is booted. |
| `--port ` | `8790` | Dev server port. If it is taken, the next free one is used. |
| `--release` | | Minify and strip debug info. Release bundles have no hot module replacement: every save reloads the app. |
| `--trace` | | The host logs the timing of every commit (JS, layout, mount items) to the terminal. |
| `--fps` | | Shows the performance monitor over the app: frame rate, what frames and commits cost, CPU, GPU and memory ([macOS](https://nsh.dev/docs/macos.md#performance-monitor); `--target macos` only for now). |
With `--target android`, the first device `adb devices` lists is used; the dev server's port is forwarded to it with `adb reverse`. See [Getting started](https://nsh.dev/docs/getting-started.md#run-it) and [Dev server and debugging](https://nsh.dev/docs/dev-server-and-debugging.md).
## nsh dev
```sh
nsh dev [ENTRY] [--port ] [--release]
```
Runs only the dev server: it builds the app, watches the entry's directory, and pushes every rebuild to the hosts connected to it. It prints the URL for hosts on this machine and one per LAN address for devices, and, when the web runtime is there, the page browsers open to run the app ([Web](https://nsh.dev/docs/web.md#develop-in-the-browser)):
```text
✓ built App.tsx (3.2 KB js, 9.8 KB bytecode, 21ms)
→ dev server ws://127.0.0.1:8790/ws
browsers: http://127.0.0.1:8790/
devices on this network: ws://192.168.1.20:8790/ws
browsers on this network: http://192.168.1.20:8790/
```
Use it with hosts you start yourself, for example an app that embeds the development SDK (`NativeShellDev.xcframework`, `nsh-dev.aar`). Flags as for `nsh run`.
## nsh trace
```sh
nsh trace [ENTRY] [--tap ]... [--taps ] [--idle-ms ] [--wait ] [--duration ]
[--scroll ] [--width ] [--height ] [--safe-area ]
```
Runs the app headless, in the CLI's own process with the same core and runtime as a device but no views, and prints every mount instruction (create, insert, style, frame, text) and the time of each commit. Then it taps views (the ones `--tap` names, or the first pressable one) and prints what each tap changed. Use it to check that an app starts without errors, to count the work an interaction causes, and in CI ([Testing and trace](https://nsh.dev/docs/testing-and-trace.md)).
```text
✓ bundled App.tsx (3255 B in 12.6 ms)
perf commit: 56 ops, 412µs (list js 6µs), 49 mount items, 21 nodes
── initial render: 49 items
Create { id: 2, kind: View }
...
── tap #1 on view 20: 3 items
Text { id: 14, text: "1", content: 71x77 @ 0,0 }
```
| Flag | Default | Description |
|---|---|---|
| `--tap ` | | Tap a view; repeat it for several taps, in order. `id:`, `label:`, `text:`, `view:` (a view id from the output), or a bare string, matched as a `testID`, then a label, then a text. The first match on screen is tapped, or its nearest pressable ancestor (the button around a text). No match is an error. |
| `--taps ` | `2` | Without `--tap`: taps sent to the first pressable view. A tap that changes nothing is an error (exit code 1). |
| `--idle-ms ` | `1200` | Before tapping, wait until the app renders nothing for this long (at most 10 s, for apps with timers or animations). The default lets the engine's idle garbage collection run first, so retention bugs show up. |
| `--wait ` | | Run timers and animations exactly this long before the first tap and after each one, printing what they render, instead of waiting until the app is idle. |
| `--duration ` | `0` | Keep running this long after the last tap. |
| `--safe-area ` | `0` | Safe area insets in points, `top,right,bottom,left` (or one value for all four), e.g. `47,0,34,0` for a phone with a notch. |
| `--scroll ` | | Scrolls the first scroll view or list by this many points, 10 points per frame, and prints each frame's batch. |
| `--width ` | `390` | Viewport width in points. |
| `--height ` | `844` | Viewport height in points. |
`nsh trace` behaves as iOS: `Platform.OS` is `"ios"`, and `ios:` classes and `ios={{…}}` props apply.
## nsh check
```sh
nsh check [ENTRY] [--tsc ] [-- ...]
```
Type checks the app: refreshes `.nsh/types`, then runs `tsc -p --noEmit` with the app's `tsconfig.json`. It uses TypeScript 7, the native compiler, which needs no Node: the first run downloads it from the npm registry (about 9 MB), checks it against the checksum NSH records, and keeps it in NSH's cache (`~/Library/Caches/nsh` on macOS, `~/.cache/nsh` on Linux, `%LOCALAPPDATA%\nsh` on Windows, or `NSH_CACHE`). Type errors make it exit with a non-zero code.
| Flag | Description |
|---|---|
| `--tsc ` | Run this `tsc` instead (also `NSH_TSC`), for example one from `node_modules` or a copy for machines without network. |
| `-- ` | Passed on to `tsc`, e.g. `nsh check -- --pretty false`. |
## nsh build
```sh
nsh build [ENTRY] [-o ] [--release] [--split]
```
Bundles the app and compiles it to QuickJS bytecode, for apps that embed NSH and ship the bundle themselves.
| Flag | Default | Description |
|---|---|---|
| `-o, --out ` | `out` | Output directory. |
| `--release` | | Minify and strip debug info. |
| `--split` | | Put what `import()` loads into chunk files of its own, downloaded when first imported (web apps, see [Split bundles](https://nsh.dev/docs/web.md#split-bundles)). Without it, the bundle holds the whole app. |
It writes:
| File | Content |
|---|---|
| `bundle.js` | The bundle as JavaScript |
| `bundle.js.map` | Its source map (stack traces are mapped back to your sources) |
| `bundle.nshc` | The bundle as compiled bytecode: what apps load |
| `.js`, `.js.map`, `.nshc` | With `--split`: each chunk, its source map and its bytecode (`Screen.js` for `import("./Screen")`) |
| `capabilities.json` | The `capabilities` of `nsh.json`, if declared (removed otherwise) |
| `assets/` | Every imported image and font, at its path relative to the app root |
## nsh package
```sh
nsh package [ENTRY] --target ios|ios-simulator|android|macos|linux|windows|gpui|web [-o ] [flags]
```
Builds an installable app from the prebuilt runtime, in release mode. No Rust and no Xcode project or Gradle build are involved: NSH assembles the app bundle itself and uses Xcode's `actool` and `codesign` (iOS) or the SDK's `aapt2`, `zipalign` and `apksigner` (Android); for macOS it assembles an ad hoc signed `.app` with `codesign` (and `sips` for the icon); for Linux it copies the runner and writes a `.tar.gz` with `tar`; for Windows it copies the runner, embeds the icon and writes a `.zip`, with no tools; for GPUI it does what it does for this machine's OS, with the GPUI runner; for the web it writes a static site, with no tools. Metadata comes from `nsh.json`, overridden by the flags.
| Flag | Description |
|---|---|
| `--target ` | Required. `ios`: iOS devices, a signed `.ipa` (or an unsigned `.app` without `--sign`). `ios-simulator`: an ad hoc signed `.app`. `android`: a signed `.apk`. `macos`: an ad hoc signed `.app` ([macOS](https://nsh.dev/docs/macos.md#packaging)). `linux`: a directory `/` (the executable, `nsh/`, a `.desktop` file) and `.tar.gz` ([Linux](https://nsh.dev/docs/linux.md#packaging)). `windows`: a directory `\` (`.exe`, `nsh\`) and `.zip` ([Windows](https://nsh.dev/docs/windows.md#packaging)). `gpui`: what `macos`, `windows` or `linux` makes on this machine, with the GPUI runner ([GPUI](https://nsh.dev/docs/gpui.md#packaging)). `web`: a directory `/`, a static site (`index.html` and files named by their content) ([Web](https://nsh.dev/docs/web.md#publish-an-app)). |
| `-o, --out ` | Output directory (default `out`). |
| `--name ` | Display name. |
| `--id ` | Bundle identifier, e.g. `com.example.app`. |
| `--app-version ` | Version, e.g. `1.2.0`. |
| `--build ` | Build number. |
| `--icon ` | App icon: a square PNG, ideally 1024×1024. |
| `--sign ` | iOS devices: code signing identity, e.g. `"Apple Development: …"`. |
| `--profile ` | iOS devices: provisioning profile (`.mobileprovision`). Entitlements come from it. |
| `--keystore ` | Android: release keystore. Default: the debug key in `~/.android/debug.keystore` (created if missing). |
| `--key-alias ` | Android: key alias in the keystore. |
| `--keystore-password ` | Android: keystore password. Also read from `NSH_KEYSTORE_PASSWORD`. |
| `--run` | Install and launch the app afterwards: in the simulator (`ios-simulator`) or on the connected Android device; `macos`, `linux`, `windows` and `gpui` launch it; `web` serves the site on this machine and opens it in the browser. |
```sh
nsh package App.tsx --target ios-simulator --run
nsh package App.tsx --target ios --sign "Apple Development: Jane Doe (ABCDE12345)" --profile dev.mobileprovision
nsh package App.tsx --target android --keystore release.jks --key-alias upload
nsh package App.tsx --target macos
nsh package App.tsx --target linux
nsh package App.tsx --target windows
nsh package App.tsx --target gpui
nsh package App.tsx --target web
```
See [Packaging](https://nsh.dev/docs/packaging.md).
## Signing commands
These commands make and check signed packages (`.nshpkg`): app bundles that apps embedding NSH download and verify before running them. [Signed packages](https://nsh.dev/docs/signed-packages.md) explains the model (an offline root key certifies release keys for some app ids).
### nsh keygen
```sh
nsh keygen
```
Creates an Ed25519 key pair: `.secret` and `.pub` (e.g. `nsh keygen keys/root`). Keep a root's `.secret` offline; apps trust its `.pub`.
### nsh certify
```sh
nsh certify --root --key --apps ... [--days ] -o
```
Certifies a release key with a root key for some app ids and a time window; packages carry the certificate.
| Flag | Description |
|---|---|
| `--root ` | The root's secret key. |
| `--key ` | The release key's public key. |
| `--apps ` | App ids it may sign for: `*`, `com.example.*` or an id. Repeatable. |
| `--days ` | How long it may sign, from now (default 365). |
| `-o, --out ` | The certificate to write. |
### nsh publish
```sh
nsh publish [ENTRY] --key --cert [-o ] [--id ] [--app-version ] [--build ]
```
Builds the app in release mode and signs it into a package. Metadata comes from `nsh.json`, overridden by the flags.
| Flag | Description |
|---|---|
| `--key ` | The release key's secret key. |
| `--cert ` | Its certificate (`nsh certify`). |
| `-o, --out ` | The package to write. Default: `out/-.nshpkg`. |
| `--id ` | App id. |
| `--app-version ` | Version. |
| `--build ` | Build number: must grow with every release. |
### nsh verify
```sh
nsh verify [--root ]... [--revocations ]
```
Verifies a package as an app trusting the given roots would. Without `--root`, only the package's own signature is checked. It prints the package's SHA-256, which `nsh revoke --package` takes.
### nsh revoke
```sh
nsh revoke --root [--extend ] [--sequence ] [--key ]... [--package ]... -o
```
Signs a revocation list: release keys and packages that must not load any more.
| Flag | Description |
|---|---|
| `--root ` | The root's secret key. |
| `--extend ` | Keep the entries of this list (the last one published). |
| `--sequence ` | The list's number, higher than any published before. Default with `--extend`: that list's plus one. |
| `--key ` | A release key id to revoke. Repeatable. |
| `--package ` | A package SHA-256 to revoke. Repeatable. |
| `-o, --out ` | The list to write. |
## Environment variables
| Variable | Used by | Description |
|---|---|---|
| `ANDROID_HOME`, `ANDROID_SDK_ROOT` | Android targets | The Android SDK. Fallback: `~/Library/Android/sdk`. |
| `NSH_KEYSTORE_PASSWORD` | `package --target android` | The keystore password. |
| `NSH_RUNTIME` | `run`, `package` | A runtime directory to use instead of the one next to the CLI. |
| `NSH_VERBOSE` | every command | `1`: as `--verbose`. |
| `NO_COLOR` | every command | Set: no colors on terminals. |
Source: https://nsh.dev/docs/cli
---
# Platform overview
NSH runs on phones, desktops and the web: iOS, Android, macOS, Windows, Linux, GPUI and web pages. Each has a backend of its own that draws with the platform's own UI, so a module is UIKit views on an iPhone, AppKit views on a Mac, GTK 4 widgets on Linux, GPUI elements in a GPUI app and HTML in a browser, with the platform's text, input, accessibility and dark mode. Everything above the backend is shared: QuickJS, the reactive graph, the renderer, Taffy layout and the components are the same Rust code everywhere, so one bundle runs on all of them.
On every platform, NSH's main use is the same: modules embedded in a native app of your own, which loads them at run time. You develop them with hot reload (`nsh run`), and the same modules can also be packaged as an installable app (`nsh package`), a supported second use.
## At a glance
| Platform | Backend; text | Runs on | Develop | Package |
|---|---|---|---|---|
| [iOS](https://nsh.dev/docs/embedding-ios.md) | UIKit; CoreText | iOS 15.0+, devices and the simulator | `nsh run --target ios` | `.app` (simulator), signed `.ipa` |
| [Android](https://nsh.dev/docs/embedding-android.md) | Android views; `StaticLayout` | Android 9 (API 28)+, `arm64-v8a` and `x86_64` | `nsh run --target android` | signed `.apk` |
| [macOS](https://nsh.dev/docs/macos.md) | AppKit and Core Animation; CoreText | macOS 14+ | `nsh run --target macos` | ad hoc signed `.app` |
| [Windows](https://nsh.dev/docs/windows.md) | Win32 windows drawn with Direct2D; DirectWrite | Windows 10 1809+, Windows 11 | `nsh run` (the default on Windows) | a directory and a `.zip` |
| [Linux](https://nsh.dev/docs/linux.md) | GTK 4 widgets; Pango | GTK 4.14+ | `nsh run` (the default on Linux) | a directory and a `.tar.gz` |
| [GPUI](https://nsh.dev/docs/gpui.md) | GPUI; GPUI's text system | macOS, Windows, Linux (Vulkan) | `nsh run --target gpui` | as the desktop it runs on |
| [Web](https://nsh.dev/docs/web.md) | HTML and CSS; the browser's fonts | modern browsers, desktop and phone | `nsh run --target web` | a static site |
- **[GPUI](https://nsh.dev/docs/gpui.md)** draws modules with [GPUI](https://gpui-kit.com), the framework of the sibling project GPUI Kit, on macOS, Windows and Linux alike: one renderer for all three desktops, and an `AppView` that GPUI and GPUI Kit apps place in their own element tree.
- **[Web](https://nsh.dev/docs/web.md)** renders apps as HTML in a browser page: `nsh package --target web` publishes a static site for any web server or CDN, and a page of your own embeds an app with `createPlayer` or an iframe. It also runs the live demos and the [playground](/playground) on this site.
- **Mac Catalyst** runs the iOS backend in an iPhone-sized window (390×844) on a Mac, for development. It is `nsh run`'s default target on macOS: the quickest way to see how a module looks on a phone.
## Develop and package
`nsh run` picks the machine's own platform when you give no `--target`: Mac Catalyst on macOS, the GTK window on Linux and the Win32 window on Windows. Phones are reached from a computer: iOS from a Mac with Xcode, Android from macOS, Linux or Windows with the Android SDK ([Installation](https://nsh.dev/docs/installation.md#what-works-where)).
```sh
nsh run my-app --target macos # or ios, android, windows, linux, gpui, web, catalyst
nsh package my-app --target macos # or ios, ios-simulator, android, windows, linux, gpui, web
```
Desktop backends build for the machine they run on: package a Mac app on a Mac, a Windows app on Windows and a Linux app on Linux. [Packaging](https://nsh.dev/docs/packaging.md) covers every target's output, signing and icons.
## Embed in a native app
| Platform | Host view | Ships as | Host language |
|---|---|---|---|
| iOS | `NshAppView` (a `UIView`) | `NativeShell.xcframework` | Swift |
| Android | `NshView` (a `FrameLayout`) | `nsh.aar` | Java or Kotlin |
| macOS | `AppView` (an `NSView`) | the `nsh-platform-macos` crate | Rust |
| Windows | `AppView` (a child window) | the `nsh-platform-windows` crate | Rust |
| Linux | `AppView` (a GTK widget) | the `nsh-platform-linux` crate | Rust |
| GPUI (macOS, Windows, Linux) | `AppView` (a GPUI view) | the `nsh-platform-gpui` crate | Rust |
| Web | `createPlayer` (a page element) or an iframe | the runtime files from `nsh package --target web` | JavaScript or HTML |
Each view is one engine with its own VM, and an app can show several. The host registers its own native views for modules to use on every platform ([Native components](https://nsh.dev/docs/native-components.md)).
> [!NOTE]
> Desktop hosts embed NSH with Rust crates from the NSH repository; their `AppView` loads bundles from the app's own files (or source code), with the default limits. The iOS and Android SDKs also load [signed packages](https://nsh.dev/docs/signed-packages.md) with `NshTrust` and set limits and capabilities per view ([Sandbox and capabilities](https://nsh.dev/docs/sandbox-and-capabilities.md)).
## Writing for every platform
Most of an app needs no platform checks: elements, styles, layout and modules behave the same everywhere. Where platforms differ, NSH gives the difference one place ([Platform-specific code](https://nsh.dev/docs/platform-specific.md)):
- **Platform-only props** go in `ios={{…}}`, `android={{…}}`, `web={{…}}` or `desktop={{…}}` (macOS, Windows, Linux and desktop browsers).
- **`Platform.select`** takes a value per OS, or for `mobile` and `desktop` as a whole; `Platform.OS` is `"ios"`, `"android"`, `"macos"`, `"windows"`, `"linux"` or `"web"`.
- **Input follows the platform.** Phones get touches and the software keyboard; desktops get the mouse, the trackpad and the keyboard.
The components of [`nsh/ui/mobile`](https://nsh.dev/components.md) are drawn by NSH, so they also run on desktops, looking as they do on phones. A desktop layout is built from the primitives (`View`, `Text`, `Scroll`, `List`, …) and `Platform.select({ desktop: … })`, as the gallery's desktop layout is; a desktop UI set is on the way.
Each backend lists what it does not support yet: [macOS](https://nsh.dev/docs/macos.md#what-differs-on-macos), [Windows](https://nsh.dev/docs/windows.md#what-differs-on-windows), [Linux](https://nsh.dev/docs/linux.md#what-differs-on-linux), [GPUI](https://nsh.dev/docs/gpui.md#differences).
## See also
- [Getting started](https://nsh.dev/docs/getting-started.md): a first module, run on each platform.
- [Installation](https://nsh.dev/docs/installation.md): the tools each platform needs.
- [Architecture](https://nsh.dev/docs/architecture.md): what is shared and what each backend does.
Source: https://nsh.dev/docs/platforms
---
# iOS
NSH apps run on iOS with UIKit views and CoreText text, on devices and in the simulator, from iOS 15.0. The main use is
embedding: an existing app shows modules in an `NshAppView` and loads signed packages it downloads, under limits and
capabilities it sets. Running and packaging whole apps work too, as on the other platforms. On iOS,
`Platform.OS` is `"ios"` and the `ios={{…}}` props apply.
## Requirements
- **Xcode** on a Mac, with an iOS simulator runtime (`xcodebuild -downloadPlatform iOS` installs one). See
[Installation](https://nsh.dev/docs/installation.md#ios-and-mac-catalyst).
- Apps need **iOS 15.0** or later, on iPhone and iPad.
## Running an app
```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, installs the development host into it and keeps the app up
to date as you save, with logs in the terminal. `--device` picks a simulator by name or UDID. For a quicker loop
without a simulator, plain `nsh run` on a Mac opens the same UIKit backend in an iPhone-sized
[Mac Catalyst](https://nsh.dev/docs/getting-started.md#mac-catalyst-default-on-macos) window.
## Packaging
```sh
nsh package my-app --target ios-simulator --run # out/.app, installed and launched in the simulator
nsh package my-app --target ios --sign "Apple Distribution: Example Inc (ABCDE12345)" --profile AppStore.mobileprovision
```
`--target ios` writes a signed `.ipa` for devices, TestFlight or the App Store. Signing identities, provisioning
profiles and icons: [Packaging](https://nsh.dev/docs/packaging.md#ios-devices).
## Embedding in an iOS app
An existing iOS app can show NSH apps inside its own UI. The runtime ships as `NativeShell.xcframework`, a static
framework with a Swift module: you create an `NshAppView` (a `UIView`), add it anywhere in your view hierarchy and load
an app into it. Your native screens, navigation and the NSH app live side by side in one UIKit hierarchy.
Each `NshAppView` is one independent engine with its own JS VM; there are no global singletons, so an app can show
several NSH apps at once, for example one per tab.

## Get the framework
The distribution contains both variants:
```text
dist/nsh/embed/ios/NativeShell.xcframework release: bundles and signed packages
dist/nsh/embed/ios/NativeShellDev.xcframework also connects to a `nsh dev` server
```
From a checkout of NSH you can build just the frameworks:
```sh
cargo xtask ios-embed # writes dist/nsh/embed/ios/{NativeShell,NativeShellDev}.xcframework
```
Each XCFramework has three arm64 slices: iOS devices, the iOS simulator (Apple silicon) and Mac Catalyst. The minimum
deployment target is iOS 15.0. The Swift module is compiled with library evolution, so apps built with other Swift
compiler versions can import it, and the framework links everything it needs: you add nothing else.
## Add it to an Xcode project
1. Drag `NativeShell.xcframework` (or `NativeShellDev.xcframework` for development builds) into the app target's
**Frameworks, Libraries, and Embedded Content**. It is a static framework, so choose **Do Not Embed**.
2. In Swift, `import NativeShell`.
> [!TIP]
> Link `NativeShellDev.xcframework` in the Debug configuration and `NativeShell.xcframework` in Release, so only
> development builds can connect to a dev server.
## Ship a bundle with the app
Build the NSH app into a folder and add that folder to the app's resources as a folder reference named `nsh`:
```sh
nsh build App.tsx --release --out MyApp/nsh
```
`nsh build` writes:
| File | Contents |
|---|---|
| `bundle.nshc` | The app compiled to bytecode: the fastest to load. |
| `bundle.js` | The same app as JS source. |
| `bundle.js.map` | Its source map (stack traces point at your `.tsx` files). |
| `capabilities.json` | The [capabilities](https://nsh.dev/docs/sandbox-and-capabilities.md) the app declares, when `nsh.json` has any. |
| `assets/` | Images and fonts the app imports, at their paths relative to the entry. |
## Show the app
```swift
import NativeShell
import UIKit
final class EmbedController: UIViewController {
private let app = NshAppView()
override func viewDidLoad() {
super.viewDidLoad()
app.translatesAutoresizingMaskIntoConstraints = false
app.onError = { message in print("nsh error: \(message)") }
view.addSubview(app)
NSLayoutConstraint.activate([
app.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor),
app.leadingAnchor.constraint(equalTo: view.leadingAnchor),
app.trailingAnchor.constraint(equalTo: view.trailingAnchor),
app.bottomAnchor.constraint(equalTo: view.bottomAnchor),
])
guard let url = Bundle.main.url(forResource: "bundle", withExtension: "qbc", subdirectory: "nsh") else { return }
do {
try app.loadBundle(at: url)
} catch {
print("cannot load: \(error)")
}
}
// Let the embedded app's StatusBar decide the status bar.
override var preferredStatusBarStyle: UIStatusBarStyle { NshAppView.preferredStatusBarStyle }
override var prefersStatusBarHidden: Bool { NshAppView.prefersStatusBarHidden }
override var preferredStatusBarUpdateAnimation: UIStatusBarAnimation {
NshAppView.preferredStatusBarUpdateAnimation
}
}
```
The view's size is the app's window: NSH lays the app out at the view's bounds and again whenever they change. Loading
again replaces the running app; the app stops when the view is deallocated.
The complete sample, a UIKit app that shows an NSH app below its own native header, is `examples/embed-ios/main.swift`.
## `NshAppView`
`NshAppView` is a `UIView` subclass isolated to the main actor.
| Member | Description |
|---|---|
| `init(frame: CGRect = .zero, trace: Bool = false)` | Creates the view. `trace` logs the time of every commit. `init?(coder:)` supports storyboards and nibs. |
| `onError: ((String) -> Void)?` | Receives fatal app errors (a bundle that fails to load, uncaught JS exceptions, a VM stopped by a limit) on the main thread. Errors are always logged; development builds also show them over the app. |
| `limits: NshLimits` | The VM limits of the apps loaded from now on. |
| `grantCapabilities(_ json: String?) throws` | The most the apps loaded from now on may do, as JSON in the form of `nsh.json`'s `capabilities`; `nil` grants what each app declares. Throws `NshError` for invalid JSON. |
| `loadBundle(at url: URL) throws` | Runs a bundle written by `nsh build`. Assets come from `assets/` next to it; `bundle.js.map` and `capabilities.json` next to it are read too. |
| `run(_ package: NshPackage)` | Runs a verified signed package (see below). A package runs once. |
| `loadPackage(at:trust:) async throws -> NshPackage.Info` | Verifies the package at a URL off the main thread, then runs it. Throws why it was refused; the running app stays. |
| `connectDevServer(_ url: URL) throws` | Development: loads from a `nsh dev` server and reloads on every change. Throws with `NativeShell.xcframework`, which has no development support. |
| `register(component:_:)` | Registers a [native component](https://nsh.dev/docs/native-components.md). |
| `static preferredStatusBarStyle`, `prefersStatusBarHidden`, `preferredStatusBarUpdateAnimation` | What the running apps asked for with `StatusBar`: return them from the hosting view controller's overrides. |
> [!IMPORTANT]
> `bundle.nshc` (bytecode) loads only from inside the app bundle. QuickJS does not validate bytecode, so bytecode from
> anywhere else could corrupt memory: for a file outside the app bundle, load `bundle.js` (JS source), or better, a
> [signed package](https://nsh.dev/docs/signed-packages.md).
`limits` and `grantCapabilities` apply from the next load: set them before `loadBundle` / `run`.
## Limits
```swift
app.limits = NshLimits(memoryMB: 128, callBudget: 1, maxNodes: 50_000)
```
| `NshLimits` field | Default | Meaning |
|---|---|---|
| `memoryMB: UInt32?` | 256 | JS heap in MB. Running out stops the app. |
| `stackKB: UInt32?` | 4096 (also the maximum) | Interpreter stack in KB. Deeper recursion throws a `RangeError`. |
| `loadBudget: TimeInterval?` | 10 s | Time to evaluate the bundle. |
| `callBudget: TimeInterval?` | 2 s | Time of any other call into JS (one event, one timer, one frame). An overrun call is cut off. |
| `maxNodes: UInt32?` | 200 000 | Nodes the app may keep. |
`nil` keeps the default. What each limit does when it is hit: [Sandbox and capabilities](https://nsh.dev/docs/sandbox-and-capabilities.md).
## Capabilities
An app declares what it may reach in its `nsh.json`; the embedding app can grant less. A request must be allowed by
both:
```swift
try app.grantCapabilities("""
{
"network": { "hosts": ["api.example.com", "*.cdn.example.com"] },
"webview": { "hosts": ["help.example.com"] }
}
""")
try app.loadBundle(at: url)
```
Here the app may not set the status bar (no `statusBar`), even if its `nsh.json` asks for it. Details:
[Sandbox and capabilities](https://nsh.dev/docs/sandbox-and-capabilities.md).
## Signed packages
To run code the app downloads (updates, third-party mini apps), use [signed packages](https://nsh.dev/docs/signed-packages.md):
`nsh publish` writes a `.nshpkg` signed by a release key that your root key certified, and the app verifies it before
running it.
```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 root.pub from `nsh keygen`
Task {
do {
let info = try await app.loadPackage(at: downloadedFile, trust: trust)
print("running \(info.id) \(info.version) (build \(info.build))")
} catch {
print("refused: \(error)") // the running app stays
}
}
```
To verify first and run later, split the two steps:
```swift
let package = try await trust.openPackage(at: downloadedFile) // verifies off the main thread
// ... decide, ask the user, ...
app.run(package)
```
| API | Description |
|---|---|
| `NshTrust(stateDirectory: URL) throws` | Opens or creates the trust store. Its state (newest revocation list, highest build accepted per app) persists there, and packages' assets are extracted under it. Trusts no key until `addRoot`. Thread-safe. |
| `addRoot(_ publicKey: String) throws` | Trusts a root public key (base64, the contents of a `.pub` file). Several roots may be trusted. |
| `updateRevocations(_ json: String) throws` | Applies a revocation list written by `nsh revoke`, signed by a trusted root and newer than the one the store has. |
| `openPackage(at: URL) async throws -> NshPackage` | Verifies, extracts assets and accepts the package (older builds of that app are refused from then on). |
| `NshPackage.info` | `id`, `name`, `version`, `build` of the app inside. |
| `NshError` | The error every throwing call throws; `message` says why. |
## Development
With `NativeShellDev.xcframework`, point the view at a dev server instead of a bundle:
```swift
#if DEBUG
try app.connectDevServer(URL(string: "ws://127.0.0.1:8790/ws")!)
#else
try app.loadBundle(at: bundledURL)
#endif
```
Run `nsh dev App.tsx` on your Mac. The simulator shares the Mac's network, so `127.0.0.1` reaches the server; a
device uses the LAN address `nsh dev` prints (`devices on this network: ws://192.168.1.20:8790/ws`). Every save
updates the app in place, logs appear in the `nsh dev` terminal, and errors show over the app. See
[Dev server and debugging](https://nsh.dev/docs/dev-server-and-debugging.md).
> [!NOTE]
> On a device, the connection to your Mac is ordinary local network traffic over plain `ws://` and `http://` (assets).
> iOS may ask the user for local network access, and the app's App Transport Security settings must allow it.
## Notes
- **Threads.** `NshAppView` is main-actor isolated. `NshTrust`, `NshPackage` and `NshComponentContext` are thread-safe.
The JS engine runs on its own thread; the main thread never waits for it.
- **Status bar.** The embedded app's `StatusBar` changes what `NshAppView.preferredStatusBarStyle` and friends return;
the hosting view controller must forward them, as in the example above.
- **Safe area.** Apps use `SafeAreaView` or `useSafeAreaInsets()` from [`nsh/ui/mobile`](https://nsh.dev/components/safe-area-view.md)
as usual. An `NshAppView` placed below your own header gets the insets that apply to it, not the window's.
- **Mac Catalyst.** The same framework works in Catalyst apps.
## See also
- [Embedding in Android apps](https://nsh.dev/docs/embedding-android.md)
- [Native components](https://nsh.dev/docs/native-components.md)
- [Signed packages](https://nsh.dev/docs/signed-packages.md)
- [Sandbox and capabilities](https://nsh.dev/docs/sandbox-and-capabilities.md)
Source: https://nsh.dev/docs/embedding-ios
---
# Android
NSH apps run on Android with ordinary Android views and `StaticLayout` text, from Android 9 (API 28), on `arm64-v8a`
and `x86_64`. An existing app embeds modules in an `NshView` and loads signed packages it downloads, under limits and
capabilities it sets: that is the main use. Running and packaging whole apps work too, as on the other platforms. On Android, `Platform.OS` is
`"android"` and the `android={{…}}` props apply.
## Requirements
- **The Android SDK** (with `platforms;android-35`, build tools and `platform-tools`) and **a JDK**, on macOS, Windows
or Linux. See [Installation](https://nsh.dev/docs/installation.md#android).
- An emulator, or a device connected over `adb`.
## Running an app
```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 a
development host APK (rebuilt only when the runtime changes) and forwards the dev server's port with `adb reverse`, so
it works over USB without a shared network; the app updates as you save, with logs in the terminal.
## Packaging
```sh
nsh package my-app --target android --run # out/.apk, installed and launched
```
The APK is built with the SDK's build tools, without Gradle, and signed with the debug key unless you pass a release
keystore. Signing for release and icons: [Packaging](https://nsh.dev/docs/packaging.md#android).
## Embedding in an Android app
An existing Android app can show NSH apps inside its own UI. The runtime ships as an Android library, `nsh.aar`: you
create a `com.longbridge.nsh.NshView` (a `FrameLayout`), add it to any layout and load an app into it. The NSH app
renders with ordinary Android views next to your own.
Each `NshView` is one independent engine with its own JS VM; there are no global singletons, so several can run at
once.

## Get the library
The distribution contains both variants:
```text
dist/nsh/embed/android/nsh.aar release: bundles and signed packages
dist/nsh/embed/android/nsh-dev.aar also connects to a `nsh dev` server
```
The AAR holds the Java runtime and `libnsh.so` for `arm64-v8a` and `x86_64` (64-bit only). It declares the `INTERNET`
permission and needs **API 28** (Android 9) or newer.
Add it to the app module, for example from a `libs` folder:
```kotlin
// app/build.gradle.kts
dependencies {
debugImplementation(files("libs/nsh-dev.aar"))
releaseImplementation(files("libs/nsh.aar"))
}
```
## Ship a bundle with the app
Build the NSH app into the app's assets:
```sh
nsh build App.tsx --release --out app/src/main/assets/nsh
```
This writes `bundle.nshc` (bytecode), `bundle.js` (source), `bundle.js.map`, `capabilities.json` (when `nsh.json`
declares [capabilities](https://nsh.dev/docs/sandbox-and-capabilities.md)) and `assets/` (the images and fonts the app imports).
## Show the app
```java
import android.app.Activity;
import android.os.Bundle;
import android.util.Log;
import com.longbridge.nsh.NshView;
public final class MainActivity extends Activity {
private NshView nsh;
@Override
protected void onCreate(Bundle state) {
super.onCreate(state);
nsh = new NshView(this);
nsh.setErrorListener(message -> Log.e("nsh", message));
setContentView(nsh);
nsh.loadAssetBundle("nsh"); // runs assets/nsh/bundle.nshc
}
@Override
protected void onDestroy() {
nsh.destroy();
super.onDestroy();
}
}
```
In Kotlin:
```kotlin
class MainActivity : Activity() {
private lateinit var nsh: NshView
override fun onCreate(state: Bundle?) {
super.onCreate(state)
nsh = NshView(this)
nsh.setErrorListener { message -> Log.e("nsh", message) }
setContentView(nsh)
nsh.loadAssetBundle("nsh")
}
override fun onDestroy() {
nsh.destroy()
super.onDestroy()
}
}
```
`NshView` can also sit inside any layout, below your own toolbar for example:
```java
LinearLayout column = new LinearLayout(this);
column.setOrientation(LinearLayout.VERTICAL);
column.addView(toolbar);
column.addView(nsh, new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, 0, 1));
setContentView(column);
```
The view's size is the app's window: NSH lays the app out at the view's size and again whenever it changes. Loading
again replaces the running app. The complete sample is
`examples/embed-android/src/com/longbridge/nsh/embeddemo/MainActivity.java`.
## `NshView`
| Method | Description |
|---|---|
| `NshView(Context context)`, `NshView(Context context, boolean trace)` | Creates an empty view. `trace` logs the time of every commit. |
| `setErrorListener(ErrorListener listener)` | Receives fatal app errors (load failures, uncaught JS exceptions, a VM stopped by a limit) on the UI thread. They are always logged. |
| `setLimits(int memoryMb, int stackKb, int loadMs, int callMs, int maxNodes)` | VM limits of the apps loaded next; 0 keeps a default. |
| `setGrantedCapabilities(String json)` | The most the apps loaded next may do, as JSON in the form of `nsh.json`'s `capabilities`; `null` grants what each app declares. Returns `false` (and is ignored) for an invalid grant. |
| `loadAssetBundle(String dir)` | Runs `dir/bundle.nshc` from the APK's assets; app assets come from `dir/assets/`. Returns `false` if it cannot be read (also reported to the error listener). |
| `loadBundleFile(File file)` | Runs a `bundle.js` file; assets come from `assets/` next to it. Refuses `.nshc` files. |
| `runPackage(NshPackage pkg)` | Runs a verified signed package, replacing the running app. A package runs once. |
| `registerComponent(String name, NshComponent.Factory factory)` | Registers a [native component](https://nsh.dev/docs/native-components.md). |
| `connectDevServer(String url)` | Development: loads from a `nsh dev` server and reloads on every change. Returns `false` with the release AAR. |
| `handleBack()` | Lets the app handle a back press (closes modals, pops screens, runs `BackHandler` listeners); returns whether it did. |
| `destroy()` | Stops the app. The view can no longer be used. |
> [!IMPORTANT]
> Compiled bytecode (`.nshc`) only loads from the APK's own assets or from a signed package. Files on storage can be replaced,
> and QuickJS does not validate bytecode, so `loadBundleFile` takes JS source only. Code the app downloads should come
> as a [signed package](https://nsh.dev/docs/signed-packages.md).
`setLimits` and `setGrantedCapabilities` apply from the next load: call them before `loadAssetBundle` and friends.
## Limits
```java
// 128 MB heap, default stack, 10 s to load, 1 s per call, 50 000 nodes
nsh.setLimits(128, 0, 10_000, 1_000, 50_000);
```
The defaults are a 256 MB heap, a 4096 KB stack, 10 s to load, 2 s per call and 200 000 nodes. What each limit does
when it is hit: [Sandbox and capabilities](https://nsh.dev/docs/sandbox-and-capabilities.md).
## Capabilities
```java
nsh.setGrantedCapabilities(
"{\"network\": {\"hosts\": [\"api.example.com\"]}, \"webview\": {\"hosts\": [\"help.example.com\"]}}");
nsh.loadAssetBundle("nsh");
```
A request must be allowed both by what the app declares in its `nsh.json` and by the grant. See
[Sandbox and capabilities](https://nsh.dev/docs/sandbox-and-capabilities.md).
## Signed packages
Verify downloaded packages off the UI thread, then run them on it:
```java
NshTrust trust = new NshTrust(new File(getFilesDir(), "nsh"));
trust.addRoot(rootPublicKey); // the contents of root.pub from `nsh keygen`
executor.execute(() -> {
try {
NshPackage pkg = trust.openPackage(downloadedFile);
runOnUiThread(() -> nsh.runPackage(pkg));
} catch (NshPackage.RefusedException e) {
Log.w("nsh", "refused: " + e.getMessage()); // the running app stays
}
});
```
| API | Description |
|---|---|
| `new NshTrust(File stateDir)` | Opens or creates the trust store. Its state (newest revocation list, highest build accepted per app) persists there and packages' assets are extracted under it. Trusts no key until `addRoot`. Usable from any thread; `close()` when done. |
| `boolean addRoot(String publicKey)` | Trusts a root public key (base64, the contents of a `.pub` file). `false` for an invalid key. |
| `boolean updateRevocations(String json)` | Applies a revocation list from `nsh revoke`, signed by a trusted root and newer than the store's. |
| `NshPackage openPackage(File file) throws NshPackage.RefusedException` | Verifies, extracts the assets and accepts the package (older builds of the app are refused from then on). Slow for large packages: call it off the UI thread. |
| `NshPackage` | `appId`, `appName`, `version`, `build`; `close()` if it will not run. |
How keys, certificates and revocation work: [Signed packages](https://nsh.dev/docs/signed-packages.md).
## Development
With `nsh-dev.aar`:
```java
if (BuildConfig.DEBUG) {
nsh.connectDevServer("ws://127.0.0.1:8790/ws");
} else {
nsh.loadAssetBundle("nsh");
}
```
Run `nsh dev App.tsx` on your computer. For an emulator or a USB device, forward the port so the device's own
`127.0.0.1` reaches the server:
```sh
adb reverse tcp:8790 tcp:8790
```
Otherwise use the LAN address `nsh dev` prints. Assets come from the dev server over plain HTTP, so `nsh-dev.aar`
declares `android:usesCleartextTraffic="true"`, which the manifest merger applies to the app (a network security
config of the app may override it). The release `nsh.aar` does not.
## Activity setup
- **Back button.** On Android 13 and newer, `NshView` registers its own back callback while the app wants back presses
(an open modal, a pushed screen, a `BackHandler` listener). On Android 12 and older, forward the press:
```java
@Override
public void onBackPressed() {
if (!nsh.handleBack()) super.onBackPressed();
}
```
For predictive back on Android 13+, set `android:enableOnBackInvokedCallback="true"` on the ``, as
packaged NSH apps do.
- **Keyboard.** `NshView` scrolls focused inputs clear of the keyboard itself. Leave `android:windowSoftInputMode` unset
or use `adjustResize`; with `adjustPan` the system moves the whole window instead.
- **Safe area.** The view reports as insets only the parts of the system bars and the keyboard that overlap it, so an
`NshView` below a toolbar gets no top inset. For edge-to-edge content, draw behind the system bars and let the app use
`SafeAreaView` / `useSafeAreaInsets()`.
- **Configuration changes.** An activity recreated on rotation or a theme change destroys the view, and the app starts
again from scratch. To keep it running, declare the changes the activity handles itself; packaged NSH apps use:
```xml
```
`NshView` follows the new size, color scheme, font scale and layout direction itself.
- **Lifecycle.** Call `destroy()` when the hosting activity or fragment goes away.
## See also
- [Embedding in iOS apps](https://nsh.dev/docs/embedding-ios.md)
- [Native components](https://nsh.dev/docs/native-components.md)
- [Signed packages](https://nsh.dev/docs/signed-packages.md)
- [Packaging](https://nsh.dev/docs/packaging.md): a standalone APK without writing any Java
Source: https://nsh.dev/docs/embedding-android
---
# macOS
NSH apps also run as native Mac apps. The macOS backend renders them with AppKit views and Core Animation and lays out
text with CoreText; the core, the reactive graph, the renderer, Taffy layout and the `nsh/ui/mobile` components are the
same as on iOS and Android, so one bundle runs everywhere. On macOS, `Platform.OS` is `"macos"` and the
`desktop={{…}}` props apply. The window's title, a title bar of the app's own, the menu bar and context menus come from
[`nsh/desktop`](https://nsh.dev/modules/nsh-desktop.md).
The macOS backend behaves like a desktop app: mouse, trackpad and keyboard input, resizable windows, centered sheets
and a menu bar.
## Requirements
- **macOS 14** or later, where the app runs.
- Xcode's command line tools (`xcode-select --install`), to build NSH's macOS runtime from a checkout and to package
apps (`codesign`, `sips`). The distribution made with `cargo xtask dist` on a Mac ships the runtime in
`runtime/macos/`.
## Running an app
```sh
nsh run my-app --target macos
```
The app opens in an AppKit window (1200×800 by default), with hot reload, logs in the terminal and the error overlay as
on the other targets; Cmd+R reloads the latest build. The menu bar and the window show the app's name (nsh.json
`"name"`, else its directory's). In a checkout, `nsh run` builds the development host
(`nsh-macos-host`) first. Stopping `nsh run` closes the window, and closing the window ends `nsh run`. The host also
runs a built bundle on its own:
```sh
nsh build my-app --release -o build
dist/nsh/runtime/macos/nsh-macos-host --bundle build/bundle.nshc --size 1200x800 # in a checkout: target/debug/nsh-macos-host
```
`--size` is the window's initial size; the window can be resized.
### Performance monitor
`--fps` (`nsh run my-app --target macos --fps`, the host's `--fps`, a packaged app's `open -a MyApp --args --fps`, or
`AppOptions { fps: true, .. }` / `AppView::set_fps_monitor(true)` when embedding) shows a panel at the top right of the
app that updates twice a second:
| Row | Measures |
|---|---|
| `FPS` | Frames shown over the last second: display refreshes in which the app changed (an idle app reads low; that is not slowness). `max` is what the main thread could keep up with, then the display's rate (120 Hz on ProMotion screens). |
| `TICK` | The gaps between display refreshes as they arrived (mean, p95) and `gap`, the p95 gap between shown frames: one refresh (8.3 ms at 120 Hz) when nothing is late. |
| `MAIN` | Main-thread time per frame (applying the engine's changes, drawing text and canvases, animations, input), its 95th percentile, and `drop`: the share of frames that took longer than one refresh. |
| `JS` / `CORE` | Per commit: your code (handlers, timers, list rows) and NSH's core (layout, text, diffing); commits per second, native view changes per commit. |
| `HOP` | How long changes wait between the engine thread and the main thread; how busy the engine thread is. |
| `CPU` / `GPU` / `MEM` | The process: CPU (100% is one core), its GPU share, its memory footprint (Activity Monitor's figure). |
The bars below are the last 120 frames' main-thread time against one refresh (the line): green within half of it, amber
within it, red over. With `--trace` too, the host also logs the readings once a second (milliseconds as
mean/p50/p95/max). Measure release builds; the monitor costs nothing while it is off.
## Packaging
```sh
nsh package my-app --target macos [--run]
```
Writes an app bundle:
```text
out/.app/Contents/
Info.plist
MacOS/ the executable (nsh's release runner)
Resources/nsh/bundle.nshc your app, compiled
Resources/nsh/assets/… the assets it imports
Resources/nsh/app.json the window title and the app's appearance
Resources/AppIcon.icns the icon, when nsh.json names one (converted with sips)
```
The `.app` is signed ad hoc, so it runs on the Mac that built it. To distribute it, sign it with a Developer ID and
notarize it. `--run` opens the app after packaging it.
## Embedding in a Mac app
The `nsh-platform-macos` crate (from the NSH repository) gives a Rust Mac app `AppView`: one NSH engine as an `NSView`.
Each `AppView` is independent; several can run at once.
```rust
use std::rc::Rc;
use nsh_platform_macos::{
objc2::MainThreadMarker, objc2_app_kit::NSWindow, AppOptions, AppView, AssetBase, Bundle,
};
fn show(window: &NSWindow) -> Rc {
let mtm = MainThreadMarker::new().expect("main thread");
let view = AppView::new(mtm, AppOptions {
asset_base: Some(AssetBase::Directory("app/assets".into())),
..AppOptions::default()
});
// register native components here, before loading
let code = std::fs::read_to_string("app/bundle.js").expect("bundle");
view.load(Bundle::source(code).into());
window.setContentView(Some(&view.view()));
// Keep the view while the window shows it; `stop` it when the window closes.
view
}
```
`AppView::load` replaces whatever ran; `stop` ends the app. `asset_base` tells it where `asset:/` URIs resolve
(`AssetBase::Directory` or `AssetBase::Http`). Bytecode bundles (`bundle.nshc`) load with
`unsafe { Bundle::bytecode(bytes) }`: bytecode is trusted input, so only load it from your own app's files.
For an app that is only an NSH app, `run_app(source, options)` opens one `AppView` in a window with a standard menu
(About, Hide, Quit, Edit, Minimize, Zoom, Enter Full Screen, Close), which the app can replace with
`Menu.setApplicationMenu` ([`nsh/desktop`](https://nsh.dev/modules/nsh-desktop.md)), and runs until the window closes.
Native components are registered with `AppView::register_component`; see
[Native components](https://nsh.dev/docs/native-components.md#registering-on-macos-rust).
## What differs on macOS
The environment is a desktop's: the safe area and the keyboard height are 0, `Platform.constants.interfaceIdiom` is
`"desktop"`, `Platform.Version` is the macOS version, and dark mode follows the system's appearance live. See
[Device and environment](https://nsh.dev/docs/device-and-environment.md).
Input is a desktop's: the mouse, the trackpad (scrolling with momentum, pinch and rotation) and the keyboard.
`selectable` text is selected by dragging, double- and triple-clicking, and copied with Cmd+C. Escape, Option+Left,
Cmd+[ and the mouse's back button go back in a stack or ask a modal to close (Escape in a focused field or web page
only leaves it; a page gets these keys first, and keeps those it uses); page and form sheets are cards centered
in the window over a dimmed backdrop. A click anywhere in an `Input`, its padding too, puts the caret at the nearest
point. `Alert` and `ActionSheet` show an `NSAlert` sheet on the window: up to two alert buttons in a row (the cancel
button leftmost, the last regular one the default for Return), more buttons and an action sheet's options stacked in
their order with the cancel button last and no default; Escape presses the cancel button.
### Keyboard
| Keys | Do |
|---|---|
| Return | In an `Input`, what `submitBehavior` says: submit (`onSubmitEditing`), submit and leave the field, or a new line (multiline inputs) |
| Shift+Return, Option+Return, Ctrl+Return | A new line in a multiline input whose return submits |
| Tab, Shift+Tab (Ctrl too) | The next / previous field in screen order, within the newest modal; Option+Tab types a tab in a multiline input |
| Escape | Leaves the focused field; else clears a text selection, then goes back or asks the newest modal to close |
| Option+Left, Cmd+[, mouse back button | Go back (not while a field has the focus: there Option+Left moves the caret) |
| Option / Cmd + arrows, with Shift; Option+Delete, Cmd+Delete; Ctrl+A / E / K | Move, select and delete by word and line in a field, as in every Mac app |
| Cmd+Z, Shift+Cmd+Z, Cmd+X / C / V / A | Undo, redo, cut, copy, paste, select all (a secure field refuses cut and copy); Cmd+A / Cmd+C select and copy `selectable` text |
| Arrows; Page Up / Down, Space, Shift+Space, Option+Up / Down; Home / End, Cmd+Up / Down | Scroll the scroll view around the focused view or under the mouse by a line, a page, to the start / end (not while a field has the focus) |
| Cmd+W, Cmd+M, Ctrl+Cmd+F, Cmd+H, Option+Cmd+H, Cmd+Q | Close, minimize, full screen, hide, hide others, quit (`nsh package` and `nsh run` apps' menus) |
| Cmd+R | Reloads the latest build (`nsh run`) |
The app's own shortcuts ([`nsh/keymap`](https://nsh.dev/modules/nsh-keymap.md)) come before all of these, except Quit, Hide,
Minimize and Full Screen, which cannot be bound.
Shortcuts work with any keyboard layout (on a Cyrillic or Greek one, Cmd+C is the key that types C on a US layout).
A return that commits an input method's composition (Chinese, Japanese, ...) never submits. `onKeyPress` reports
what React Native reports: the typed text, `"Enter"`, `"Backspace"`, and `"Escape"`.
Not supported, or different:
- **Zooming scroll views** (`zoomTo`, the zoom props).
- **`mixBlendMode`**, and `brightness()` above 1.
- **Inputs**: content types and autofill, keyboard types, autocapitalization, letter spacing; input accessory views
are never shown (there is no software keyboard) and `keyboardShouldPersistTaps` is ignored.
- **`WebView`'s `scrollEnabled={false}`** keeps the mouse wheel and the trackpad from scrolling the page: they scroll
the app's scroll views around it, as on iOS. The page still scrolls with its keyboard (Space, the arrows) and its own
scripts, and trackpad swipes between its pages (`allowsBackForwardNavigationGestures`) stop as well.
- **Accessibility**: custom `accessibilityActions`.
- **The status bar** and **haptics** (no desktop equivalent).
## See also
- [CLI](https://nsh.dev/docs/cli.md): `nsh run --target macos`, `nsh package --target macos`
- [Packaging](https://nsh.dev/docs/packaging.md)
- [Platform-specific code](https://nsh.dev/docs/platform-specific.md)
Source: https://nsh.dev/docs/macos
---
# Windows
NSH apps also run on Windows. The Windows backend draws them in Win32 windows with Direct2D and lays out text with
DirectWrite; the core, the reactive graph, the renderer, Taffy layout and the `nsh/ui/mobile` components are the same
as on iOS and Android, so one bundle runs everywhere. On Windows, `Platform.OS` is `"windows"` and the
`desktop={{…}}` props apply. The window's title, a title bar of the app's own, the menu bar and context menus come from
[`nsh/desktop`](https://nsh.dev/modules/nsh-desktop.md).
## Requirements
- **Windows 10 1809** or later, or Windows 11.
- The **WebView2 Runtime** for web views. It comes with Windows 11 and with Microsoft Edge on Windows 10; without it a
`WebView` shows a placeholder.
- **Media Foundation codecs** for the video formats your app plays (videos play through Windows' own media stack).
- To build NSH's Windows runtime (from a checkout, or `cargo xtask dist` on Windows): Visual Studio Build Tools with
the C++ workload, the Windows SDK and LLVM (for QuickJS' bindings).
## Running an app
On Windows, `nsh run` opens the app in a window (1200×800 by default), with hot reload, logs in the terminal and the
error overlay as on the other targets:
```sh
nsh run my-app # --target windows is the default on Windows
```
In a checkout, `nsh run` builds the development host (`nsh-windows-host.exe`) first; the distribution made on Windows
ships it in `runtime\windows\`. Ctrl-C closes the window, and closing the window ends `nsh run`; the host never
outlives `nsh`, however it ends. The host also runs a built bundle on its own:
```sh
nsh build my-app --release -o build
dist\nsh\runtime\windows\nsh-windows-host --bundle build\bundle.nshc --size 390x844 # in a checkout: target\debug\nsh-windows-host
```
`--size` is the window's size in points (`x`).
## Packaging
```sh
nsh package my-app --target windows [--run]
```
Writes a directory and the same as an archive:
```text
out\\
.exe the executable (nsh's release runner), with the app's icon
nsh\bundle.nshc your app, compiled
nsh\assets\… the assets it imports
nsh\app.json the window title, the app id, the version and the appearance
out\.zip
```
The executable finds `nsh\` next to itself; copy the whole directory (or unpack the `.zip`) wherever the app should
live: nothing is installed. The icon from `nsh.json` is embedded in the executable (Explorer and the taskbar show it)
without any extra tools; packaging on another system keeps the default icon. The app id groups the app's windows in
the taskbar. `--run` launches the app after packaging it. The executable is not signed: sign it with your own
certificate (`signtool`) to distribute it.
## Embedding in a Win32 app
The `nsh-platform-windows` crate (from the NSH repository) gives a Rust Win32 app `AppView`: one NSH engine in a child
window of a window of yours. Each `AppView` is independent; several can run at once.
```rust
use std::rc::Rc;
use nsh_platform_windows::{
windows::{self, Win32::{Foundation::HWND, System::Ole::OleInitialize, UI::WindowsAndMessaging::MoveWindow}},
AppOptions, AppView, AssetBase, Bundle,
};
/// Called on the thread that runs your message loop, with your window.
fn embed(parent: HWND) -> windows::core::Result> {
// Once per thread: COM and OLE (the clipboard, image decoding).
unsafe { OleInitialize(None)? };
let view = AppView::new(
Some(parent),
AppOptions {
asset_base: Some(AssetBase::Directory("app/assets".into())),
app_id: "com.example.host".into(),
..AppOptions::default()
},
)?;
// register native components here, before loading
let code = std::fs::read_to_string("app/bundle.js").expect("bundle");
view.load(Bundle::source(code).into());
Ok(view)
}
// On WM_SIZE of `parent`:
// unsafe { MoveWindow(view.hwnd(), 0, 0, width, height, true) };
```
The view is a child window: your message loop runs it, and you place it with `MoveWindow` on its `hwnd()` when the
parent resizes. `AppView::load` replaces whatever ran; `stop` (or destroying the window) ends the app. `asset_base`
tells it where `asset:/` URIs resolve (`AssetBase::Directory` or `AssetBase::Http`). Bytecode bundles (`bundle.nshc`)
load with `unsafe { Bundle::bytecode(bytes) }` (or `Bundle::read_trusted(path)`, unsafe too): QuickJS bytecode is trusted input, so
only load it from your own app's files. A standalone window is `nsh_platform_windows::run_app(source, options)`.
Native components are registered with `AppView::register_component`; see
[Native components](https://nsh.dev/docs/native-components.md#registering-on-windows-rust).
## What differs on Windows
The environment is a desktop's: the safe area and the keyboard height are 0, `Platform.constants.interfaceIdiom` is
`"desktop"` and `Platform.Version` is `"10.0"` (on Windows 11 too: Windows kept the version number). Dark mode
follows Settings > Personalization > Colors live, `fontScale()` the Text size setting, reduce motion the animation
effects setting and the locale the user's regional format. See [Device and environment](https://nsh.dev/docs/device-and-environment.md).
### Keyboard
| Keys | Do |
|---|---|
| Return | In an `Input`, what `submitBehavior` says: submit (`onSubmitEditing`), submit and leave the field, or a new line (multiline inputs) |
| Shift+Return, Alt+Return | A new line in a multiline input whose return submits (Ctrl+Return submits) |
| Tab, Shift+Tab | The next / previous text field, within the newest modal; the field's text is selected, as in Windows' dialogs |
| Escape | Leaves the focused field; else clears a text selection, then goes back or asks the newest modal to close |
| Alt+Left, Browser Back key, mouse back button | Go back (also from a field) |
| Ctrl+arrows, Home / End, Ctrl+Home / End, with Shift; Ctrl+Backspace / Delete | Move, select and delete by word and line in a field, as in every Windows app |
| Ctrl+Z, Ctrl+Y, Ctrl+X / C / V / A; Shift+Delete, Ctrl+Insert, Shift+Insert | Undo, redo, cut, copy, paste, select all (a secure field refuses cut and copy); Ctrl+A / Ctrl+C select and copy `selectable` text |
| Arrows; Page Up / Down, Space, Shift+Space; Home / End, Ctrl+Home / End | Scroll the scroll view around the focused view or under the mouse by a line, a page, to the start / end (not while a field has the focus) |
| F11, Alt+F4 | Full screen, close the window (`nsh package` and `nsh run` apps) |
| Ctrl+R, F5 | Reload the latest build (`nsh run`) |
Shortcuts work with any keyboard layout (on a Cyrillic or Greek one, Ctrl+C is the key that types C on a US layout).
A return that commits an input method's composition (Chinese, Japanese, ...) never submits. `onKeyPress` reports
what React Native reports: the typed text, `"Enter"`, `"Backspace"`, and `"Escape"`; pasted text is not reported.
The app's own shortcuts ([`nsh/keymap`](https://nsh.dev/modules/nsh-keymap.md)) come before a focused field's keys, navigation
and scrolling; Alt+F4, F11 and Ctrl+Q cannot be bound.
Different, or not supported:
- **`WebView`**: a page inside a view with a 3D transform (perspective) is not shown, and a `backdropFilter` over a
page does not blur it. `source.baseUrl` takes `http(s)` URLs. A native component that brings a window of its own
stays above everything NSH draws.
- **Video**: the formats the installed codecs decode; no poster; the control bar always shows with `controls`.
- **Zooming scroll views** (`zoomTo`, the zoom props); the scroll indicator cannot be dragged.
- **`mixBlendMode` and `backdropFilter`** are drawn, but not inside a view that has a `filter` or a 3D transform;
`borderCurve: "continuous"` draws circular corners.
- **Text**: head and middle ellipses on a single line only (more lines get a tail ellipsis); color emoji from COLR
fonts only; small caps only where the font has them.
- **Text inputs**: keyboard types and autofill do not apply (there is no software keyboard); Tab moves between text
fields. A right click opens the edit menu (Undo, Cut, Copy, Paste, Delete, Select All), as on selectable text (Copy,
Select All).
- **Accessibility** (UI Automation): headers' titles and buttons, dialogs and toasts are not exposed; headings report
level 1.
- **Input accessory views** and the **status bar** (no desktop equivalent).
## See also
- [CLI](https://nsh.dev/docs/cli.md): `nsh run --target windows`, `nsh package --target windows`
- [Packaging](https://nsh.dev/docs/packaging.md)
- [Platform-specific code](https://nsh.dev/docs/platform-specific.md)
Source: https://nsh.dev/docs/windows
---
# Linux
NSH apps also run on Linux. The Linux backend renders them with GTK 4 widgets and lays out text with Pango; the core,
the reactive graph, the renderer, Taffy layout and the `nsh/ui/mobile` components are the same as on iOS and Android,
so one bundle runs everywhere. On Linux, `Platform.OS` is `"linux"` and the `desktop={{…}}` props apply. The window's title, a title bar of the app's own, the menu bar and context menus come from
[`nsh/desktop`](https://nsh.dev/modules/nsh-desktop.md).
## Requirements
- **GTK 4.14** or later. To build NSH's Linux runtime (from a checkout, or `cargo xtask
dist` on Linux) you need their development packages and clang: `libgtk-4-dev clang` on Debian and Ubuntu (24.04 or
later), `gtk4 clang` on Arch, `gtk4-devel clang` on Fedora.
- **WebKitGTK 6.0** (2.40 or later) for web views: `libwebkitgtk-6.0-dev` on Debian and Ubuntu, `webkitgtk-6.0` on
Arch, `webkitgtk6.0-devel` on Fedora to build the runtime. The development host and the release runner use it by
default, so packaged apps need WebKitGTK 6.0 where they run (`libwebkitgtk-6.0-4` on Debian and Ubuntu; GNOME
desktops have it). `cargo xtask dist` on a machine without its development files builds a runtime whose web views
are placeholders, and whose apps need only GTK.
- **GStreamer** plugins for the video formats your app plays (GTK plays video through GStreamer).
## Running an app
On Linux, `nsh run` opens the app in a GTK window (390×844 by default), with hot reload, logs in the terminal and the
error overlay as on the other targets:
```sh
nsh run my-app # --target linux is the default on Linux
```
In a checkout, `nsh run` builds the development host (`nsh-linux-host`) first; the distribution made on Linux ships
it in `runtime/linux/`. Ctrl-C closes the window, and closing the window ends `nsh run`. The host also runs a built
bundle on its own:
```sh
nsh build my-app --release -o build
dist/nsh/runtime/linux/nsh-linux-host --bundle build/bundle.nshc --size 1024x768 # in a checkout: target/debug/nsh-linux-host
```
`--size` is the window's default size; tiling window managers may give the window another one.
## Packaging
```sh
nsh package my-app --target linux [--run]
```
Writes a directory and the same as an archive:
```text
out//
the executable (nsh's release runner)
nsh/bundle.nshc your app, compiled
nsh/assets/… the assets it imports
nsh/app.json the window title and the application id
.desktop a desktop entry (StartupWMClass is the id)
.png the icon, when nsh.json names one
out/.tar.gz
```
The executable finds `nsh/` next to itself (a symbolic link to it, in `~/.local/bin` say, works too). GTK 4, Pango and
WebKitGTK 6.0 must be installed where the app runs. To show the app in a desktop's launcher, put the executable (or a link) on the
`PATH`, the `.desktop` file in `~/.local/share/applications` and the icon in an icon theme directory. `--run` launches
the app after packaging it.
## Embedding in a GTK app
The `nsh-platform-linux` crate (from the NSH repository) gives a Rust GTK app `AppView`: one NSH engine as a GTK widget.
Each `AppView` is independent; several can run at once.
```rust
use std::rc::Rc;
use nsh_platform_linux::{gtk4 as gtk, AppOptions, AppView, AssetBase, Bundle};
use gtk::prelude::*;
fn main() -> gtk::glib::ExitCode {
let app = gtk::Application::builder().application_id("com.example.host").build();
app.connect_activate(|app| {
let window = gtk::ApplicationWindow::builder().application(app).default_width(390).default_height(844).build();
let view = AppView::new(AppOptions {
asset_base: Some(AssetBase::Directory("app/assets".into())),
..AppOptions::default()
});
// register native components here, before loading
let code = std::fs::read_to_string("app/bundle.js").expect("bundle");
view.load(Bundle::source(code).into());
window.set_child(Some(&view.widget()));
// The window keeps the view; closing it stops the app.
let keep = std::cell::RefCell::new(Some(Rc::clone(&view)));
window.connect_destroy(move |_| if let Some(view) = keep.take() { view.stop() });
window.present();
});
app.run()
}
```
`AppView::load` replaces whatever ran; `stop` ends the app. `asset_base` tells it where `asset:/` URIs resolve
(`AssetBase::Directory` or `AssetBase::Http`). Bytecode bundles (`bundle.nshc`) load with `unsafe { Bundle::bytecode(bytes) }`:
QuickJS bytecode is trusted input, so only load it from your own app's files.
Native components are registered with `AppView::register_component`; see [Native components](https://nsh.dev/docs/native-components.md#registering-on-linux-rust).
## What differs on Linux
The environment is a desktop's: the safe area and the keyboard height are 0, `Platform.constants.interfaceIdiom` is
`"desktop"`, `Platform.Version` is the kernel's major and minor version, and dark mode follows the desktop's setting
(through the XDG settings portal) live. See [Device and environment](https://nsh.dev/docs/device-and-environment.md).
### Keyboard
| Keys | Do |
|---|---|
| Return | In an `Input`, what `submitBehavior` says: submit (`onSubmitEditing`), submit and leave the field, or a new line (multiline inputs) |
| Shift+Return, Alt+Return | A new line in a multiline input whose return submits (Ctrl+Return submits) |
| Tab, Shift+Tab | The next / previous field in screen order, within the newest modal |
| Escape | Leaves the focused field; else clears a text selection, then goes back or asks the newest modal to close |
| Alt+Left, Back key, mouse back button | Go back (also from a field) |
| Ctrl+arrows, Home / End, with Shift; Ctrl+Backspace / Delete | Move, select and delete by word and line in a field, as in every GTK app |
| Ctrl+Z, Ctrl+Shift+Z, Ctrl+X / C / V / A | Undo, redo, cut, copy, paste, select all (a secure field refuses cut and copy); Ctrl+A / Ctrl+C select and copy `selectable` text |
| Arrows; Page Up / Down, Space, Shift+Space; Home / End | Scroll the scroll view around the focused view or under the mouse by a line, a page, to the start / end (not while a field has the focus) |
| Ctrl+W, F11, Ctrl+Q | Close the window, full screen, quit (`nsh package` and `nsh run` apps) |
| Ctrl+R | Reloads the latest build (`nsh run`) |
The app's own shortcuts ([`nsh/keymap`](https://nsh.dev/modules/nsh-keymap.md)) are a window-wide shortcut controller; Ctrl+Q and
F11 cannot be bound.
Shortcuts work with any keyboard layout (on a Cyrillic or Greek one, Ctrl+C is the key that types C on a US layout).
A return that commits an input method's composition (Chinese, Japanese, ...) never submits. `onKeyPress` reports
what React Native reports: the typed text, `"Enter"`, `"Backspace"`, and `"Escape"`; pasted text is not reported.
Web views are WebKitGTK pages, drawn inside NSH's views: sheets, dialogs, toasts and views after them draw over a page,
and parents clip it and fade it. Their website data (cookies, storage, the cache) is kept per app, under
`~/.local/share//webkit` and `~/.cache//webkit`. The `webview` capability also applies to the pages of
frames: an `iframe` of a host your app was not granted stays empty. Escape in a page leaves it, as in a text field
(unless the page uses it); Alt+Left and the mouse's back button go back in the page while it has a history.
Not supported:
- **Video `rate`**: GTK plays at normal speed only. `onEnd` only comes for videos that do not loop.
- **Zooming scroll views** (`zoomTo`, the zoom props).
- **`mixBlendMode` and `backdropFilter`** (the background color still draws); `borderCurve: "continuous"` draws
circular corners.
- **Input accessory views** and the **status bar** (no desktop equivalent).
- **Accessibility**: `accessible` grouping, custom `accessibilityActions`; live regions are announced instead.
## See also
- [CLI](https://nsh.dev/docs/cli.md): `nsh run --target linux`, `nsh package --target linux`
- [Packaging](https://nsh.dev/docs/packaging.md)
- [Platform-specific code](https://nsh.dev/docs/platform-specific.md)
Source: https://nsh.dev/docs/linux
---
# GPUI
The GPUI backend draws NSH apps with [GPUI](https://gpui-kit.com) (GPUI Fast), the GPU-accelerated UI framework that
GPUI Kit is built on, on macOS, Windows and Linux alike. Its main use is embedding: an `AppView` is a GPUI view, so a
GPUI or GPUI Kit app shows NSH modules anywhere in its own element tree. The core, the reactive graph, the renderer,
Taffy layout and the `nsh/ui/mobile` components are the same as on every other platform, so one bundle runs
everywhere. `Platform.OS` is the OS it runs on (`"macos"`, `"windows"` or `"linux"`) and the `desktop={{…}}` props
apply.
Every element and module of the [macOS](https://nsh.dev/docs/macos.md) (AppKit), [Windows](https://nsh.dev/docs/windows.md) (Win32) and
[Linux](https://nsh.dev/docs/linux.md) (GTK) backends is there; where GPUI draws something its own way, [Differences](#differences)
says how.
## How it works
- **One element per surface.** A surface is one GPUI element that fills the space its parent gives it and paints the
whole view tree with GPUI's primitives: quads, shadows, paths, glyphs and images. NSH keeps one retained view per
node, as on Windows, and redraws only when a commit changed something.
- **Text is shaped on the engine thread** with GPUI's text system (the app's own fonts), broken into lines by NSH;
the main thread only paints the glyphs.
- **One hop per commit.** The engine runs on its own thread; each commit is one closure sent to GPUI's main thread,
which notifies the view.
- **Input** comes from GPUI's mouse, scroll, key and input method events; hit testing, presses, the responder system,
scrolling and text editing are the same logic as the Windows backend's.
## Requirements
- macOS, Windows or Linux, with a GPU GPUI can draw with (Metal on macOS, DirectX on Windows, Vulkan on Linux).
- On Linux: a Vulkan driver, Wayland or X11 with `libxkbcommon`, GTK 3 and WebKitGTK 4.1 (web views) and GStreamer
1.20 or later with its base and good plug-ins (videos). Web views need X11: on a Wayland desktop run the app under
Xwayland (`WAYLAND_DISPLAY=` empty), else they show a placeholder. Building the runtime from a checkout also needs
the development packages; on Debian and Ubuntu:
```sh
sudo apt-get install clang libclang-dev pkg-config libfontconfig-dev libfreetype-dev libwayland-dev libx11-xcb-dev \
libxcb1-dev libxkbcommon-dev libxkbcommon-x11-dev libzstd-dev libvulkan1 libgtk-3-dev libwebkit2gtk-4.1-dev \
libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev gstreamer1.0-plugins-good
```
## Running an app
```sh
nsh run my-app --target gpui
```
Opens the app in a GPUI window (1200×800 by default), with hot reload, logs in the terminal and the error overlay as
on the other targets. The development host (`nsh-gpui-host`) runs in place on every OS, with no app bundle around it.
In a checkout, `nsh run` builds it first; the distribution ships it in `runtime/gpui/`. The host also runs a built
bundle on its own:
```sh
nsh build my-app --release -o build
dist/nsh/runtime/gpui/nsh-gpui-host --bundle build/bundle.nshc --size 390x844 # in a checkout: target/debug/nsh-gpui-host
```
`--size` is the window's size in points (`x`).
## Packaging
```sh
nsh package my-app --target gpui [--run]
```
The package is laid out as the native backend's of the machine it is made on, with GPUI's release runner
(`nsh-gpui-app`) as the executable:
| Packaged on | Output |
|---|---|
| macOS | an ad hoc signed `.app`, as [`--target macos`](https://nsh.dev/docs/macos.md#packaging) makes |
| Windows | a directory `\` with `.exe` and `nsh\`, and `.zip`, as [`--target windows`](https://nsh.dev/docs/windows.md#packaging) makes |
| Linux | a directory `/` with the executable, `nsh/` and a `.desktop` file, and `.tar.gz`, as [`--target linux`](https://nsh.dev/docs/linux.md#packaging) makes |
As with the native backends, the runner is built for the machine it is built on: package a Mac app on a Mac, a
Windows app on Windows and a Linux app on Linux. `--run` launches the app after packaging it.
A Linux package links WebKitGTK 4.1 (web views) and GStreamer (videos) dynamically, so the machines it runs on need them installed, with GTK 3 and GStreamer's base and good plug-ins (Debian / Ubuntu: `libwebkit2gtk-4.1-0`, `libgstreamer1.0-0`, `libgstreamer-plugins-base1.0-0`, `gstreamer1.0-plugins-good`), besides a Vulkan driver and `libxkbcommon`.
## Embedding in a GPUI app
The `nsh-platform-gpui` crate (from the NSH repository) gives a Rust GPUI app `AppView`: one NSH engine as a GPUI
view (`Entity`), which fills the space its parent element gives it. Each `AppView` is independent; several
can run at once.
```rust
use gpui::{AppContext, Application, WindowOptions};
use nsh_platform_gpui::{App, AppOptions, AppView, Bundle};
gpui_platform::application().run(|cx| {
cx.open_window(WindowOptions::default(), |window, cx| {
cx.new(|cx| {
let view = AppView::new(AppOptions::default(), window, cx);
let code = std::fs::read_to_string("app/bundle.js").expect("bundle");
view.load(App::Js(Bundle::source(code)));
view
})
})
.unwrap();
});
```
Put the entity in your element tree like any other view (a GPUI Kit `Root`, a panel, a dock). `AppView::load`
replaces whatever ran; `stop` (or dropping the view) ends the app. `AppOptions::asset_base` tells it where `asset:/`
URIs resolve (`AssetBase::Directory` or `AssetBase::Http`). Bytecode bundles (`bundle.nshc`) load with
`unsafe { Bundle::bytecode(bytes) }` (or `Bundle::read_trusted(path)`, unsafe too): QuickJS bytecode is trusted input,
so only load it from your own app's files.
- `open_app_window(source, options, cx)` opens a window with one `AppView` in a running GPUI app.
- `run_app(source, options)` starts GPUI and runs one app in a window until it closes (what the release runner does).
- `AppView::register_component(name, factory)` registers a native component of yours before loading
([Native components](https://nsh.dev/docs/native-components.md)).
- `set_ui_sets(sets)` replaces the default `nsh/ui/mobile` set, for apps with Rust components of their own.
## Differences
What draws or behaves differently from the native desktop backends:
- **Transforms**: rotated, skewed and projected views turn their backgrounds, borders and shadows, but text stays
upright, and images, videos and canvases draw at their bounds.
- **Opacity** applies to each color rather than to a group, so overlapping children of a translucent view show
through each other; an image's opacity (a fade-in, a translucent view) changes in sixteenths.
- **Rounded clips** (`overflow: "hidden"` with a radius) round the backgrounds and images of the children, but text,
borders, videos and native components are cut by the rectangle.
- **Not drawn**: `filter: blur()`, `backdropFilter` and `mixBlendMode`; shadows show through translucent fills;
`borderCurve: "continuous"` draws circular corners.
- **Canvas text** is approximated: upright under rotations, made smaller rather than narrower for `maxWidth`,
gradients paint it in one color, `strokeText` fills, its shadows are not blurred, and composite operations and
`clearRect` do not apply to it.
- **Scroll views** do not zoom (`zoomTo` and the zoom props are ignored).
- **Video** plays on Linux (GStreamer); on macOS and Windows a video shows a placeholder and reports `onError`.
- **Web views** are native views above everything NSH draws at their place. On Linux they need X11 (see
[Requirements](#requirements)); where pages stay black under Xwayland ("Failed to create GBM buffer" in the log),
run the app with `WEBKIT_DISABLE_DMABUF_RENDERER=1`. On macOS and Windows clipping ancestors do not cut them and
failed loads report no `onError`. `source.baseUrl`, `scrollEnabled` and `allowsBackForwardNavigationGestures` are
not supported.
- **Accessibility** goes through AccessKit: screen readers can activate, focus, set values and scroll elements into
view everywhere; custom actions and the context-menu action are not passed on yet.
For comparison, each native backend's own list: [macOS](https://nsh.dev/docs/macos.md#what-differs-on-macos),
[Windows](https://nsh.dev/docs/windows.md#what-differs-on-windows), [Linux](https://nsh.dev/docs/linux.md#what-differs-on-linux).
## See also
- [CLI](https://nsh.dev/docs/cli.md): `nsh run --target gpui`, `nsh package --target gpui`
- [Packaging](https://nsh.dev/docs/packaging.md)
- [Platform overview](https://nsh.dev/docs/platforms.md)
Source: https://nsh.dev/docs/gpui
---
# Web (WebAssembly)
The NSH runtime also compiles to WebAssembly. The same engine runs in a browser page and renders to the DOM instead of UIKit or Android views: the core, the reactive graph, the renderer, Taffy layout, the `nsh/ui/mobile` components and QuickJS itself. `nsh package --target web` publishes an app as a static site for any web server or CDN ([Publish an app](#publish-an-app)), `nsh run --target web` runs it in the browser while you edit it ([Develop in the browser](#develop-in-the-browser)), and every live demo on this site is an app bundle from `nsh build` running on this runtime.
## What runs where
On every native platform the engine has its own thread and hands one `MountBatch` per commit to the main thread. A browser page has one thread, so the web build drives the same engine step by step from the page's event loop (`nsh_engine::LocalEngine`):
```text
pointer / keyboard / scroll events ─┐
setTimeout (the engine's deadline) ─┼─▶ nsh.wasm: engine turn (JS, core, layout) ─▶ one JSON batch ─▶ nsh.js: DOM
requestAnimationFrame ──────────────┘ ▲
native animation outputs ───┘
```
- **Layout is the core's.** Every view is an absolutely positioned element at the frame Taffy computed, so flexbox, grid, `aspectRatio` and percentages behave exactly as on devices. The browser's own layout is not involved.
- **Styles are turned into CSS in Rust.** Background, borders, radii, shadows (`shadow*` and `boxShadow`), gradients, filters, `backdropFilter`, transforms, `transformOrigin`, `outline`, `mixBlendMode`, transitions and the built-in keyframe animations become CSS properties. Tailwind classes, `active:`, `hover:` and `focus:` states and `dark:` all resolve natively, as they do on devices.
- **Text is laid out in Rust** with font measurements from the browser (`CanvasRenderingContext2D.measureText`) and drawn run by run at the positions the layout chose, so what was measured is what is drawn. Lines break after spaces, around CJK characters and at `\n`; `numberOfLines` truncates with an ellipsis.
- **Native animations run in Rust** on `requestAnimationFrame`: `Animated` timing, spring and decay, `interpolate`, native values bound to `opacity`, `transform` and `backgroundColor`, drags bound with `pan`, scroll-linked values and canvas arguments. JS is not involved per frame, as on devices. `LayoutAnimation`, style transitions, the `animate-*` keyframes and screen transitions move as on the desktop backends, and `prefers-reduced-motion` turns them off (screens fade).
- **Input:** presses with `activeOpacity` and long presses, the responder system (`onTouch*`, `PanResponder`), native drags and two-finger pinch and rotation, scroll views (the browser's scrolling), pull to refresh, zooming scroll views (pinch, ctrl+wheel and touchpad pinches, `zoomTo`), `` with controlled values, focus and submit, `Canvas`, `Stack` and `Screen` with a header, `Modal`.
- **System UI is drawn in the page:** `Alert` and `ActionSheet` look like the desktop backends' dialogs on desktop browsers and like iOS's on phones (with the `desktop` idiom, see below), and `Toast.show` shows a toast at the bottom. Custom fonts are measured again once the browser has loaded them, and an app waits for the fonts it imports before its first frame shows.
- **Modals** are presented as on the platform the browser is like: `pageSheet` and `formSheet` (`desktop={{ presentationStyle }}` or `web={{ presentationStyle }}`) are cards centered over the dimmed page in desktop and tablet browsers, the sizes the desktop backends use, closed by Escape, the browser's back button or a click outside (each asks through `onRequestClose`); in phone browsers they rise from the bottom to just below the top, as iPhone sheets do. `fullScreen` and transparent modals cover the app; `animationType`, `onShow` and `onDismiss` work as on devices.
## Keyboard, mouse and touch
Published apps behave as desktop apps in desktop browsers and as phone apps in phone browsers:
- **Wheels and touchpads** scroll the innermost scroll view along their axis that can still move (a vertical wheel scrolls a horizontal list when nothing vertical can), and stay on it until the scroll ends; wheel notches glide, paging and snapping views move one page per scroll. Ctrl + wheel and touchpad pinches pinch views with native pinch values instead of zooming the page. Dragging with the mouse does not scroll, as on the desktop platforms: mouse drags go to pans, the responder system and text selection. Scroll views report `onScrollBeginDrag` / `onScrollEndDrag`, `onMomentumScrollBegin` / `End` and `onScrollEnd` as on the desktop platforms.
- **Keyboard**: Tab and Shift+Tab move between pressables and inputs (a focus ring and `focus:` styles show the keyboard focus); Return presses the focused pressable, Space presses it when released; arrows, Page Up / Page Down, Space, Home and End scroll the scroll view around the focus or under the mouse; Escape leaves a field, closes the dialog, then goes back. `nsh/keymap` shortcuts work in the browser, matched on the spot (`mod` is Command in Safari and Chrome on a Mac, Control elsewhere).
- **Going back**: Escape, Alt+Left (Cmd+[ on a Mac) and the mouse's back button pop the innermost stack, or ask the newest modal to close, as the Android back button does; `preventBack` and `onBackPress` work as there. When the app is the whole page, every pushed screen and presented modal is a browser history entry, so the browser's back button goes back in the app.
- **Touch**: only scroll views pan; the page itself never scrolls, zooms or pulls to refresh under the app, and taps have no delay. In an iframe, swipes the app does not take scroll the embedding page. Drags on views with native pans, pinches and the responder system are the app's.
- **Accessibility**: `accessibilityRole`, `accessibilityLabel`, `accessibilityHint`, `accessibilityState`, `accessibilityValue`, `accessibilityLiveRegion` and `accessibilityElementsHidden` become ARIA; `AccessibilityInfo.announceForAccessibility` speaks through a live region and `setAccessibilityFocus` moves the focus. Views under a modal or alert are inert.
- **Selection and menus**: in desktop browsers text is selectable by default, as on a web page, except inside interactive elements (views with press or touch handlers: buttons, pressable rows, tabs) and text with `selectable={false}` or `select-none`; dragging the mouse over it selects it, Ctrl/Cmd+C copies it line by line and Ctrl/Cmd+A selects all of the text the selection is in. In phone and tablet browsers only `selectable` text is selectable, as in native apps. The browser's context menu appears only in selectable text and inputs.
## Publish an app
```sh
nsh package App.tsx --target web # out//: a static site
nsh package App.tsx --target web --run # and serve it on this machine, opened in the browser
```
`nsh package --target web` writes `out//` (`--out` for another directory), laid out like a frontend build: one
page and files named after their content.
| File | What it is |
|---|---|
| `index.html` | The app's page: its name as the title, the icons, the colors of its `appearance`, a loading indicator, and a message when the app cannot load or start |
| `manifest.webmanifest` | The name, id, icons and colors browsers install the app with |
| `nsh..js` | The DOM runtime, minified (79 KB, 22.5 KB with brotli) |
| `nsh..wasm` | The engine, the core, the mobile components and QuickJS without its compiler (about 3.5 MB, 1.2 MB gzipped, 0.93 MB with brotli) |
| `app..nshc` | The app, built as `nsh build --release --split` builds it, compiled to QuickJS bytecode: nothing is compiled in the browser, so it starts sooner |
| `harden..nshc` | The [sandbox's](https://nsh.dev/docs/sandbox-and-capabilities.md) hardening, compiled likewise (1.4 KB) |
| `chunks/..nshc` | What `import()` loads, compiled likewise, downloaded the first time it is imported ([split bundles](#split-bundles)) |
| `assets/..` | Every asset the app imports, loaded from its URL when it is used (fonts before the code that registers them runs, as text is measured with them: before the app starts, or with the chunk that imports them) |
| `icons/..png` | The icon of `nsh.json` at 32 (favicon), 180 (`apple-touch-icon`), 192 and 512 pixels |
`` is the start of the SHA-256 of the file's content: a file's name changes exactly when its content does, so a
new release changes only `index.html` and the files that really changed. Everything uses relative URLs, so the site
works from any directory of any host (`https://example.com/apps/counter/`).
### Deploy it
Upload the directory to any static host: an object store behind a CDN, Cloudflare Pages, Netlify, GitHub Pages,
nginx. Two rules make it load fast and update reliably:
| Files | `Cache-Control` |
|---|---|
| `index.html`, `manifest.webmanifest` | `no-cache` (revalidated on every load, so a release shows at once) |
| everything else | `public, max-age=31536000, immutable` |
Serve `.wasm` as `application/wasm` (browsers then compile it while it downloads) and let the host or CDN compress
text and WebAssembly (gzip or brotli). `nsh package --target web --run` serves the site this way on
`http://127.0.0.1:4173/`.
An app published for the web runs like a packaged native app: as a release, [hardened](https://nsh.dev/docs/sandbox-and-capabilities.md)
(no `eval`), with the capabilities its `nsh.json` declares. When they restrict `network`, the page also carries a
Content-Security-Policy `connect-src` with the declared hosts (and the page's own origin), so the browser itself
refuses requests and redirect hops to any other host. An error that keeps it from starting is shown on the page;
later errors go to the browser's console.
### Embed it with an iframe
There is no embedding API on the web: embed the published app with an `}>
…
```
## Platform variants in Tailwind
The `ios:`, `android:` and `web:` variants apply classes on one platform:
```tsx
…Title
```
## Prefer shared behavior
Before reaching for a platform check, consider whether the difference matters to your users. Elements and styles already map to each platform's native behavior where it should differ (scroll physics, text rendering, keyboards, accessibility), while staying the same where users expect consistency (your app's own look). Platform checks are best kept for platform conventions users rely on: Android's back button and toasts, iOS's sheets and large titles, autofill names, ripple versus opacity feedback.
## Related
- [`nsh/ios`](https://nsh.dev/modules/nsh-ios.md) and [`nsh/android`](https://nsh.dev/modules/nsh-android.md): the references.
- [`nsh/device`](https://nsh.dev/modules/nsh-device.md): `Platform`.
- [Native components](https://nsh.dev/docs/native-components.md): using views your host app provides on each platform.
- [Tailwind](https://nsh.dev/docs/tailwind.md): every variant.
Source: https://nsh.dev/docs/platform-specific
---
# Native Components
A native component is a view the **embedding app** registers by name: a map, a camera preview, a chart library, any
`UIView` on iOS, `android.view.View` on Android, `NSView` on macOS, `gtk::Widget` on Linux, or Direct2D drawing or a
child `HWND` on Windows. The NSH app declares it with `nativeComponent()` from
[`nsh/native`](https://nsh.dev/modules/nsh-native.md) and uses it like any other component. NSH lays it out with the rest of the
app, sends it props, delivers its events to handlers and forwards commands from a `ref`.
Because NSH renders with the system's own views, a native component is just one more subview in the same hierarchy:
there is no layering problem, it clips, scrolls and receives touches like a ``.
> [!NOTE]
> Native components exist only where an app embeds NSH ([iOS](https://nsh.dev/docs/embedding-ios.md), [Android](https://nsh.dev/docs/embedding-android.md),
> [macOS](https://nsh.dev/docs/macos.md#embedding-in-a-mac-app), [Linux](https://nsh.dev/docs/linux.md#embedding-in-a-gtk-app),
> [Windows](https://nsh.dev/docs/windows.md#embedding-in-a-win32-app)).
> A packaged standalone app (`nsh package`) or a development host registers none, so the element renders an empty
> view there (the name is logged once as unknown).
## How it fits together
1. The host app registers a factory under a name, such as `"embed.badge"`, on its `NshAppView` (iOS) or `NshView`
(Android). Each view has its own registry; there is nothing global.
2. The NSH app declares a component for that name with the props and events it uses.
3. When the element is created, NSH creates a container view (decorated, framed and hit-tested like any view), then
asks the factory for a component and puts the component's view inside the container, filling it.
4. The component receives every prop right after it is made, then each prop again when it changes.
5. The component emits events and may report its own size; JS calls its commands through a `ref`.
## Declaring the component in JS
```tsx
import { mount, signal, Text, type YNode } from "nsh";
import { nativeComponent } from "nsh/native";
import { SafeAreaView } from "nsh/ui/mobile";
/** The host app's badge: a label it draws, sized to its text. */
const Badge = nativeComponent<{ text: string; color?: string }, { tap: { count: number } }>("embed.badge", {
props: ["text", "color"],
events: ["tap"],
});
const colors = ["#8250df", "#0969da", "#1a7f37", "#cf222e"];
function App() {
const taps = signal(0);
let badge: YNode | undefined;
return (
Rendered by nsh (badge = node)}
text={taps() === 0 ? "Tap the native badge" : `Tapped ${taps()}×`}
color={colors[taps() % colors.length]}
onTap={(e) => {
taps.set(e.count);
badge?.command("pulse");
}}
style={{ alignSelf: "flex-start", borderRadius: 8, overflow: "hidden" }}
/>
{`JS saw ${taps()} taps`}
);
}
mount(App);
```
This is `examples/embedded/App.tsx`, which both embedding samples in the repository run.
### `nativeComponent(name, spec)`
```ts
function nativeComponent
, props?: P): () => void;
```
Renders `app` into the surface (the window, or the `NshAppView` / `NshView` the app is embedded in) and returns a function that removes it. Every app's entry file ends with `mount(App)`.
```tsx
mount(App);
```
## split
```ts
function split(
props: T,
keys: K,
): [Pick, Omit];
```
Splits a props object into the props named in `keys` and the rest. Both halves stay reactive: props whose values change are getters, and `split` keeps them as getters. Use it in a component that handles some props itself and passes the others on. Destructuring props (`const { label } = props`) would read them once and lose updates.
```tsx
import { Input, type InputProps, split, Text, View } from "nsh";
function LabeledInput(props: InputProps & { label: string }) {
const [own, rest] = split(props, ["label"]);
return (
{own.label}
);
}
```
## View, Text, Scroll, Input
```ts
function View(props: ViewProps): YNode;
function Text(props: TextProps): YNode;
function Scroll(props: ScrollProps): YNode;
function Input(props: InputProps): YNode;
```
The host elements `nsh` exports, written as tags: ``, ``, ``, ``. The compiler makes the
native view in place for a tag that names one of them through its import (an alias too), so importing them costs
nothing at run time. They are components like any other, so they can also be passed around (`const Tag = inline ? Text : View`).
The other elements come from their own modules: `Image` from [`nsh/image`](https://nsh.dev/modules/nsh-image.md), `List` from
[`nsh/list`](https://nsh.dev/modules/nsh-list.md), `Canvas`, `WebView` and `Video` from theirs. Their props are documented under
[Elements](https://nsh.dev/elements.md).
## Types
`nsh` also exports the types of the reactive API and of the host elements. The most used ones:
| Type | What it is |
|---|---|
| `Accessor` | A readable value: `() => T` |
| `Signal` | An `Accessor` with `set` and `update` |
| `SignalOptions` | Options of `signal` and `computed` (`equals`) |
| `Context` | What `context` returns |
| `Component
` | `(props: P) => JSX.Element` |
| `JSX` | The JSX namespace (`JSX.Element`) |
| `YNode` | What a `ref` receives: focus, scroll and other commands. See [Elements](https://nsh.dev/elements.md) |
| `ViewProps`, `TextProps`, `ImageProps`, `ScrollProps`, `InputProps`, `CommonProps`, `TouchProps` | Props of host elements |
| `ViewStyle`, `TextStyle`, `ImageStyle`, `LayoutStyle`, `StyleProp` | Style objects; see the [style reference](https://nsh.dev/modules/style-reference.md) |
| `ColorValue`, `DimensionValue`, `LengthValue`, `TransformStep` | Values inside styles |
| `GestureResponderEvent`, `LayoutEvent`, `ScrollEvent`, `PanEvent`, `TouchPoint` | Event payloads |
| `ImageAsset` | What importing an image file gives (`{ uri, width, height }`) |
| `OS` | `"ios" \| "android" \| "macos" \| "windows" \| "linux" \| "web"` |
| `AccessibilityRole`, `AccessibilityState` | Accessibility props |
```tsx
import { type GestureResponderEvent, type YNode, signal } from "nsh";
let field: YNode | undefined;
const onPress = (e: GestureResponderEvent) => field?.focus();
```
`StyleProp` accepts one style or a list merged left to right, skipping falsy entries:
```tsx
```
Source: https://nsh.dev/modules/nsh
---
# nsh/animated
`nsh/animated` animates on the UI thread. You create a **native value**, bind it (or values derived from it) to a style property, and start an animation; from then on every frame is computed natively, without running JavaScript. A busy JS thread cannot make the animation stutter, and an animation costs no JS work per frame.
```ts
import { Animated, createNativeValue, Easing, LayoutAnimation, NativeNode, NativeValue } from "nsh/animated";
```
See [Animation](https://nsh.dev/docs/animation.md) for the guide.
## What a native value can drive
| Where | How |
|---|---|
| `opacity` | `style={{ opacity: value }}` |
| `backgroundColor` | `style={{ backgroundColor: value.interpolate({ inputRange: [0, 1], outputRange: ["#fff", "#000"] }) }}` |
| `borderColor`, `color` | The same: a node of colors. `borderColor` colors every side; a `Text`'s `color` colors the whole text |
| `shadowColor`, `shadowOpacity`, `shadowRadius` | `style={{ shadowOpacity: value }}` (a node of colors for `shadowColor`) |
| `transform` | `translateX`, `translateY`, `scale`, `scaleX`, `scaleY`, `rotate`, `rotateX`, `rotateY`, `rotateZ` steps: `transform: [{ translateX: value }]` |
| Scroll position | `` and `` write the scroll offset into it each frame |
| Gestures | `pan={{ x, y }}`, `pinch={{ scale }}`, `rotation={{ angle }}` on any element: the gesture writes the values directly (see [Touch and gestures](https://nsh.dev/docs/touch-and-gestures.md)) |
| Canvas | Any number or color argument of a [canvas](https://nsh.dev/modules/nsh-canvas.md) drawing call |
Other style properties take plain values; to animate the layout itself (sizes, positions in the flow), use [`LayoutAnimation`](#layoutanimation).
## createNativeValue
```ts
function createNativeValue(value?: number): NativeValue;
```
Creates a native value starting at `value` (default `0`).
```tsx
import { mount, Text, View } from "nsh";
import { createNativeValue } from "nsh/animated";
function App() {
const x = createNativeValue(0);
let right = false;
const toggle = () => {
right = !right;
x.spring(right ? 200 : 0);
};
return (
Move
);
}
mount(App);
```
> [!NOTE]
> A native value lives on the UI thread and changes there every frame, so JS reads it asynchronously: `await value.getValue()` (or the promise `stop()` returns) gives the value at that moment. Keep the target in a variable or a signal when your code needs it right away, as above. Gesture events report the values at their end (`PanEvent.valueX` / `valueY`).
## NativeValue
```ts
class NativeValue extends NativeNode {
constructor(value: number);
set(value: number): void;
stop(): Promise;
getValue(): Promise;
setOffset(offset: number): void;
flattenOffset(): void;
extractOffset(): void;
timing(to: number, options?: { duration?: number; easing?: Easing; delay?: number }): Promise;
spring(to: number, options?: { stiffness?: number; damping?: number; mass?: number; velocity?: number }): Promise;
decay(options?: { velocity?: number; deceleration?: number; clamp?: [number, number]; bounce?: number }): Promise;
}
```
| Method | Description |
|---|---|
| `set(value)` | Jumps to `value`, stopping any running animation. |
| `stop()` | Stops the running animation where it is; resolves with the value there. |
| `getValue()` | Resolves with the current value (with its offset), read on the UI thread. |
| `setOffset(offset)` | An offset added to the value: bindings see value + offset; `set`, animations and drags move the value. |
| `flattenOffset()` | Adds the offset to the value and makes it 0. |
| `extractOffset()` | Moves the value into the offset and makes the value 0 (a drag then starts from where the last one ended). |
| `timing(to, options)` | Animates to `to` along an easing curve. `duration` in ms (default 300), `easing` (default `"easeInOut"`), `delay` in ms (default 0). |
| `spring(to, options)` | Springs to `to`. `stiffness` (default 170), `damping` (default 26), `mass` (default 1), `velocity` (initial velocity, units per second). |
| `decay(options)` | Coasts from `velocity` (units per second) and slows down by `deceleration` per millisecond (default 0.998). Use it after a fling, with the velocity from `onPanEnd`. With `clamp: [min, max]` it stops at a bound, or bounces back from it keeping `bounce` (0..1, default 0) of its speed. |
Each animation method returns a promise that resolves with `true` when the animation finished and `false` when it was stopped (by `stop`, `set` or another animation of the same value).
```ts
const opacity = createNativeValue(1);
const finished = await opacity.timing(0, { duration: 200 });
if (finished) visible.set(false);
```
## NativeNode
```ts
class NativeNode {
interpolate(config: {
inputRange: number[];
outputRange: number[] | string[];
extrapolate?: Extrapolation;
extrapolateLeft?: Extrapolation;
extrapolateRight?: Extrapolation;
easing?: Easing;
}): NativeNode;
diffClamp(min: number, max: number): NativeNode;
add(b: NativeNode | number): NativeNode;
sub(b: NativeNode | number): NativeNode;
mul(b: NativeNode | number): NativeNode;
div(b: NativeNode | number): NativeNode;
mod(b: NativeNode | number): NativeNode;
}
type Extrapolation = "extend" | "clamp" | "identity";
```
A node of the native animation graph: a `NativeValue`, or a value derived from others. Derived nodes are evaluated on the UI thread whenever their inputs change.
| Method | Description |
|---|---|
| `interpolate(config)` | Maps the input through piecewise ranges. `outputRange` holds numbers, colors (`"#ff0000"`, `"rgba(…)"`) or angles (`"45deg"`, `"1rad"`). Outside `inputRange` it clamps by default; `extrapolate` (or `extrapolateLeft` / `extrapolateRight`) can be `"extend"` (continue the line), `"clamp"` or `"identity"` (pass the input through). `easing` shapes each segment. |
| `diffClamp(min, max)` | Follows the input's changes but keeps the result within `min..max`: a header that hides while content scrolls up and comes back as soon as it scrolls down. |
| `add`, `sub`, `mul`, `div` | Arithmetic with another node or a constant. |
| `mod(b)` | Modulo, never negative for a positive `b` (keeping angles in 0..360). |
```tsx
const scroll = createNativeValue(0);
const header = scroll.diffClamp(0, 44);
{/* rows */}
```
```ts
const progress = createNativeValue(0);
const rotate = progress.interpolate({ inputRange: [0, 1], outputRange: ["0deg", "360deg"] });
const color = progress.interpolate({ inputRange: [0, 0.5, 1], outputRange: ["#22c55e", "#eab308", "#ef4444"] });
```
## Animated
```ts
const Animated: {
timing(value: NativeValue, config: { toValue: number; duration?: number; easing?: Easing; delay?: number }): CompositeAnimation;
spring(value: NativeValue, config: { toValue: number; stiffness?: number; damping?: number; mass?: number; velocity?: number }): CompositeAnimation;
decay(value: NativeValue, config?: { velocity?: number; deceleration?: number; clamp?: [number, number]; bounce?: number }): CompositeAnimation;
sequence(list: CompositeAnimation[]): CompositeAnimation;
parallel(list: CompositeAnimation[], config?: { stopTogether?: boolean }): CompositeAnimation;
stagger(ms: number, list: CompositeAnimation[]): CompositeAnimation;
delay(ms: number): CompositeAnimation;
loop(animation: CompositeAnimation, config?: { iterations?: number; resetBeforeIteration?: boolean }): CompositeAnimation;
add(a: NativeNode, b: NativeNode | number): NativeNode;
subtract(a: NativeNode, b: NativeNode | number): NativeNode;
multiply(a: NativeNode, b: NativeNode | number): NativeNode;
divide(a: NativeNode, b: NativeNode | number): NativeNode;
modulo(a: NativeNode, b: NativeNode | number): NativeNode;
diffClamp(a: NativeNode, min: number, max: number): NativeNode;
sampleSpring(config: SpringConfig & { from?: number }, fps?: number): number[];
sampleDecay(config: DecayConfig & { from?: number }, fps?: number): number[];
};
interface CompositeAnimation {
start(callback?: (result: { finished: boolean }) => void): Promise;
stop(): void;
reset(): void;
}
```
`Animated` builds animations out of steps (`timing`, `spring`, `decay`, `sequence`, `parallel`, ...). Nothing runs until `start()`; then the whole tree runs natively, and each step starts on the UI thread at the exact moment the previous one ends (no frame gap, no JS round trip between steps).
| Function | Description |
|---|---|
| `timing`, `spring`, `decay` | One animation of one value; same options and defaults as the [`NativeValue`](#nativevalue) methods, with the target as `toValue`. |
| `sequence(list)` | Runs the animations one after another. |
| `parallel(list, { stopTogether })` | Runs them at the same time. With `stopTogether` (default `true`), stopping one stops all. |
| `stagger(ms, list)` | Runs them in parallel, each starting `ms` after the previous one. |
| `delay(ms)` | Waits; useful inside `sequence`. |
| `loop(animation, { iterations, resetBeforeIteration })` | Repeats `animation` `iterations` times (forever by default), putting its values back before each run unless `resetBeforeIteration` is `false`. |
| `add`, `subtract`, `multiply`, `divide`, `modulo`, `diffClamp` | The same as the [`NativeNode`](#nativenode) methods, as functions. |
`CompositeAnimation`:
| Method | Description |
|---|---|
| `start(callback?)` | Runs it (again). Resolves, and calls `callback`, once for the whole tree with whether it finished rather than being stopped. |
| `stop()` | Stops it where it is. |
| `reset()` | Stops it and puts its values back where they were when it started. |
```ts
const ys = [0, 1, 2, 3].map(() => createNativeValue(0));
const hop = Animated.stagger(
80,
ys.map((y) =>
Animated.sequence([
Animated.timing(y, { toValue: -36, duration: 220, easing: Easing.out(Easing.quad) }),
Animated.spring(y, { toValue: 0, stiffness: 300, damping: 12 }),
]),
),
);
hop.start(({ finished }) => console.log(finished ? "done" : "stopped"));
```
## Easing
```ts
type Easing =
| "linear" | "ease" | "easeIn" | "easeOut" | "easeInOut"
| [number, number, number, number] // cubic Bézier x1, y1, x2, y2
| EasingCurve;
const Easing: {
linear: Easing;
ease: Easing;
bezier(x1: number, y1: number, x2: number, y2: number): Easing;
in(e: Easing): Easing;
out(e: Easing): Easing;
inOut(e: Easing): Easing;
sample(easing: Easing, count: number): number[];
quad: EasingCurve;
cubic: EasingCurve;
poly(n?: number): EasingCurve;
sin: EasingCurve;
circle: EasingCurve;
exp: EasingCurve;
elastic(bounciness?: number): EasingCurve;
back(s?: number): EasingCurve;
bounce: EasingCurve;
};
```
An easing is data, not a JS function: the runtime evaluates it natively. To draw one, `Easing.sample(easing, count)` gives its values at `count` evenly spaced points from 0 to 1; `Animated.sampleSpring` and `Animated.sampleDecay` give a spring's or decay's values one per frame (at `fps`, 60 by default) from `from` (0) until it ends. All three come from the native code the animations run. Wherever an `Easing` is accepted you can pass a name (`"easeOut"`), a cubic Bézier as four numbers, or a curve from the `Easing` object.
| Curve | Shape |
|---|---|
| `quad` | `t²` |
| `cubic` | `t³` |
| `poly(n)` | `tⁿ` (`n` defaults to 2) |
| `sin` | Sinusoidal |
| `circle` | Circular |
| `exp` | Exponential |
| `elastic(bounciness)` | Springy overshoot (`bounciness` defaults to 1) |
| `back(s)` | Pulls back before going; `s` is the overshoot (default 1.70158) |
| `bounce` | Bounces at the end |
The curves are in their "in" form (slow at the start). `Easing.out(curve)` reverses one, `Easing.inOut(curve)` makes it symmetric, `Easing.in(curve)` keeps it as is.
```ts
value.timing(1, { duration: 600, easing: Easing.out(Easing.back()) });
value.timing(1, { easing: Easing.bezier(0.2, 0, 0, 1) });
value.timing(1, { easing: "easeOut" });
```
## LayoutAnimation
```ts
const LayoutAnimation: {
configureNext(config: LayoutAnimationConfig, onEnd?: () => void): void;
create(duration: number, type?: LayoutAnimationType, property?: LayoutAnimationProperty): LayoutAnimationConfig;
Types: { easeInEaseOut; spring; linear; easeIn; easeOut };
Properties: { opacity; scaleXY };
Presets: { easeInEaseOut: LayoutAnimationConfig; linear: LayoutAnimationConfig; spring: LayoutAnimationConfig };
easeInEaseOut(onEnd?: () => void): void;
linear(onEnd?: () => void): void;
spring(onEnd?: () => void): void;
};
interface LayoutAnimationConfig {
duration: number; // ms
create?: LayoutAnimationAnimation; // views inserted
update?: LayoutAnimationAnimation; // views moved or resized
delete?: LayoutAnimationAnimation; // views removed
}
interface LayoutAnimationAnimation {
type?: "easeInEaseOut" | "spring" | "linear" | "easeIn" | "easeOut";
property?: "opacity" | "scaleXY" | "scale";
duration?: number;
delay?: number;
springDamping?: number; // 0..1, default 0.7
}
```
`LayoutAnimation.configureNext(config)` animates the layout changes of the next commit natively: views that move or resize glide to their new frames, new views appear (fading or scaling in, by `property`) and removed views disappear. Call it right before the state change that changes the layout.
| Member | Description |
|---|---|
| `configureNext(config, onEnd?)` | Animates the next commit's changes; `onEnd` runs when done. |
| `create(duration, type?, property?)` | A config using the same curve for all three phases. |
| `Presets.easeInEaseOut` | 300 ms, views fading in and out. |
| `Presets.linear` | 500 ms at constant speed. |
| `Presets.spring` | 700 ms; frames spring (damping 0.4), views fade linearly. |
| `easeInEaseOut(onEnd?)`, `linear(onEnd?)`, `spring(onEnd?)` | Shorthands for `configureNext(Presets.…)`. |
| `Types`, `Properties` | The names, as constants. |
```tsx
const open = signal(false);
const toggle = () => {
LayoutAnimation.configureNext(LayoutAnimation.Presets.spring);
open.update((o) => !o);
};
DetailsShown with a spring.
```
Source: https://nsh.dev/modules/nsh-animated
---
# nsh/list
`nsh/list` exports `List`: a virtualized list on a native scroll view. Only the rows near the viewport exist, and their cells are reused as you scroll. Which rows exist, where they are, columns, sticky headers and viewability are all computed natively, so scrolling a list of ten thousand items runs no JS except the row bindings that change when a cell is reused.
```ts
import { List, type ListProps, type ViewToken, type ViewabilityConfig } from "nsh/list";
```
See [Lists](https://nsh.dev/docs/lists.md) for the guide and [``](https://nsh.dev/elements/list.md) for the element's props.
## List
```ts
function List(props: ListProps): YNode;
```
```tsx
import { mount, Text, View } from "nsh";
import { List } from "nsh/list";
const contacts = Array.from({ length: 1000 }, (_, i) => ({ id: String(i), name: `Contact ${i}` }));
function App() {
return (
{(item, index) => (
{item().name}
)}
);
}
mount(App);
```
### Rows are recycled
`children` is a function that renders **one cell**. It runs once per cell, not once per item: when a cell scrolls out of view it is reused for another item, and `item()` and `index()` (both accessors) change. Read them inside JSX expressions (`{item().name}`) so the cell updates when it is reused.
> [!WARNING]
> Do not keep per-item state in signals created inside the row function: the cell is reused for other items and the signal would follow the cell, not the item. Keep it in your data, keyed by id (a signal holding a `Set` of selected ids, for example).
## Props
`ListProps` combines the list's own props with the scrolling props shared with ``.
### Data and rendering
| Prop | Type | Default | Description |
|---|---|---|---|
| `each` | `readonly T[]` | (required) | The items. Pass a signal's value (`each={items()}`) to follow changes; inserting items above the visible rows keeps those rows in place. |
| `children` | `(item: () => T, index: () => number) => JSX.Element` | (required) | Renders one row (see above). |
| `estimatedItemSize` | `number` | `50` | The size of rows not measured yet, in points, along the list's axis. Close estimates make the scroll indicator and `scrollToIndex` more accurate. |
| `numColumns` | `number` | `1` | Items per row (vertical lists): a grid. |
| `horizontal` | `boolean` | `false` | Scrolls sideways. |
| `inverted` | `boolean` | `false` | Lays out from the end, item 0 last (chat style); starts scrolled to the end and keeps its distance from the end as content changes. |
| `stickyHeaderIndices` | `number[]` | | Items that stick to the start while scrolled past (one column, not inverted). |
| `ListHeaderComponent` | `ListSlot` | | Rendered before the items. |
| `ListFooterComponent` | `ListSlot` | | Rendered after the items. |
| `ListEmptyComponent` | `ListSlot` | | Shown instead of the items while `each` is empty. |
| `ItemSeparatorComponent` | `ListSlot` | | Rendered between items (one column). |
| `keyExtractor` | `(item: T, index: number) => string` | `item.key`, `item.id`, or the index | The key of an item in viewability tokens. |
| `initialScrollIndex` | `number` | | Scrolls to this item before the first frame. |
| `contentContainerStyle` | `ListContentStyle` | | Padding and gaps of the content: `padding`, `paddingHorizontal`, `paddingVertical`, `paddingTop`, `paddingRight`, `paddingBottom`, `paddingLeft`, `gap`, `rowGap`, `columnGap` (numbers). |
| `style` | `StyleProp` | | The list's own style (give it a size: `flex: 1` or a height). |
| `ref` | `YNode \| ((node: YNode) => void)` | | See [ref methods](#ref-methods). |
`ListSlot` is a component (`Header`) or an element (``).
### Loading, refreshing and viewability
| Prop | Type | Default | Description |
|---|---|---|---|
| `onEndReached` | `() => void` | | Called when the end comes within `onEndReachedThreshold` viewports: load more. |
| `onEndReachedThreshold` | `number` | `1` | How close to the end (in viewport lengths) `onEndReached` fires. |
| `refreshing` | `boolean` | | The pull-to-refresh spinner shows. |
| `onRefresh` | `() => void` | | Enables pull to refresh; set `refreshing` while you load. |
| `onViewableItemsChanged` | `(info: { viewableItems: ViewToken[]; changed: ViewToken[] }) => void` | | Items that became or stopped being viewable. |
| `viewabilityConfig` | `ViewabilityConfig` | | When an item counts as viewable. |
```ts
interface ViewToken {
item: T;
key: string;
index: number;
isViewable: boolean;
}
interface ViewabilityConfig {
/** Percent of the viewport an item must cover to count as viewable. */
viewAreaCoveragePercentThreshold?: number;
/** Percent of an item that must be visible. Without either threshold, any visible pixel counts. */
itemVisiblePercentThreshold?: number;
/** Milliseconds items must stay viewable before they are reported. */
minimumViewTime?: number;
}
```
### Scrolling
| Prop | Type | Default | Description |
|---|---|---|---|
| `showsIndicator` | `boolean` | `true` | The scroll indicator shows. |
| `scrollEnabled` | `boolean` | `true` | The user can scroll it. |
| `bounces` | `boolean` | `true` | Overscroll past the edges. |
| `pagingEnabled` | `boolean` | `false` | Stops at multiples of the viewport. |
| `snapToInterval` | `number` | `0` | Stops at multiples of this length (0: off). |
| `snapToOffsets` | `number[]` | | Stops at these offsets. |
| `snapToAlignment` | `"start" \| "center" \| "end"` | `"start"` | Which point of an item aligns with the viewport with `snapToInterval`. |
| `disableIntervalMomentum` | `boolean` | `false` | A fling moves one snap point at most. |
| `decelerationRate` | `"normal" \| "fast" \| number` | platform | How fast a fling slows down (`0.998` / `0.99`, or the velocity kept per millisecond). |
| `contentInset` | `{ top?, right?, bottom?, left? }` | | Room around the content. |
| `contentOffset` | `{ x?, y? }` | | Where it starts. |
| `keyboardShouldPersistTaps` | `"never" \| "always" \| "handled" \| boolean` | `"never"` | Whether a tap dismisses the keyboard (`"never"`: the tap only dismisses it). |
| `keyboardDismissMode` | `"none" \| "on-drag" \| "interactive"` | `"none"` | Whether dragging dismisses the keyboard. |
| `scrollEventThrottle` | `number` | | Minimum milliseconds between `onScroll` events (at most one per frame anyway). |
| `scrollValue` | `NativeValue` | | A [native value](https://nsh.dev/modules/nsh-animated.md) fed with the scroll offset (main axis) on the UI thread, for scroll-linked effects. |
### Scroll events
| Event | Payload | When |
|---|---|---|
| `onScroll` | `ScrollEvent`: `{ x, y, contentWidth, contentHeight, width, height, zoomScale }` | While scrolling. |
| `onScrollBeginDrag` | `{ x, y }` | A drag starts. |
| `onScrollEndDrag` | `{ x, y }` | The finger lifts. |
| `onMomentumScrollBegin` | `{ x, y }` | A glide starts after the drag. |
| `onMomentumScrollEnd` | `{ x, y }` | The glide ends (also when a new drag catches it). |
| `onScrollEnd` | `{ x, y }` | Scrolling came to rest. |
Lists also take the props every element takes ([common props](https://nsh.dev/elements.md)).
## Ref methods
A list's `ref` receives a `YNode` with these methods:
| Method | Description |
|---|---|
| `scrollToIndex({ index, animated?, viewPosition?, viewOffset? })` | Scrolls item `index` into view. `viewPosition` places it: `0` at the start of the viewport (default), `0.5` in the middle, `1` at the end; `viewOffset` shifts it further. Resolved natively with the measured (or estimated) row sizes. |
| `scrollToOffset({ offset, animated? })` | Scrolls to a content offset along the list's axis. |
| `scrollToEnd({ animated? })` | Scrolls to the end of the content (the start of an inverted list). |
| `scrollTo({ x?, y?, animated? })` | Scrolls to an offset. |
```tsx
import { Text, View, type YNode } from "nsh";
import { List } from "nsh/list";
let list!: YNode;
list.scrollToIndex({ index: 60, viewPosition: 0.5 })}>Jump to 60
{(item) => {item().title}}
```
## Examples
A chat: newest message at the bottom, older messages loaded as you scroll up.
```tsx
{(m) => (
{m().text}
)}
```
Pull to refresh:
```tsx
const refreshing = signal(false);
const refresh = async () => {
refreshing.set(true);
items.set(await load());
refreshing.set(false);
};
{(item) => {item().title}}
```
## ListItemsProps
```ts
type ListItemsProps = Pick,
"each" | "children" | "keyExtractor" | "stickyHeaderIndices" | "initialScrollIndex"
| "onViewableItemsChanged" | "viewabilityConfig" | "refreshing" | "onRefresh"
| "onEndReached" | "onEndReachedThreshold"
| "ListHeaderComponent" | "ListFooterComponent" | "ListEmptyComponent">;
```
The data-and-rows half of `ListProps`: what you pass to a component that lays out a list itself, such as [`ListView`](https://nsh.dev/components/list-view.md) from `nsh/ui/mobile`.
Source: https://nsh.dev/modules/nsh-list
---
# nsh/canvas
`nsh/canvas` draws with an HTML Canvas 2D–style context. Your `draw` function **records** the drawing; the platform replays it natively (Core Graphics on iOS, `android.graphics.Canvas` on Android) at the screen's resolution. Numbers and colors in a drawing may be [native values](https://nsh.dev/modules/nsh-animated.md): when they animate, the platform redraws on its own, without running `draw` again.
```ts
import { Canvas, CanvasGradient, CanvasRenderingContext2D, Path2D } from "nsh/canvas";
```
See [Canvas](https://nsh.dev/docs/canvas.md) for the guide and [``](https://nsh.dev/elements/canvas.md) for its props.
## Canvas
```ts
function Canvas(props: CanvasProps): YNode;
interface CanvasProps extends ViewProps {
draw: (ctx: CanvasRenderingContext2D, size: CanvasSize) => void;
}
interface CanvasSize {
width: number; // points
height: number;
}
```
A view drawn with a 2D context. It takes every [view prop](https://nsh.dev/elements/view.md) (style, classes, touch handlers, accessibility, `ref`), plus:
| Prop | Type | Description |
|---|---|---|
| `draw` | `(ctx, { width, height }) => void` | Draws the canvas. Runs once the size is known, again when the signals it reads change, and again when the canvas resizes. Native values passed as numbers or colors animate without running it. |
Give the canvas a size through its style (`width` / `height`, `flex: 1`, `aspectRatio`, ...). Coordinates are in points, the origin at the top left.
```tsx
import { mount, signal, Text, View } from "nsh";
import { Canvas } from "nsh/canvas";
function App() {
const values = signal([12, 30, 22, 48, 36, 60, 52, 75]);
return (
{
const v = values();
const step = width / (v.length - 1);
ctx.strokeStyle = "#6366f1";
ctx.lineWidth = 3;
ctx.lineJoin = "round";
ctx.beginPath();
v.forEach((n, i) => (i ? ctx.lineTo(i * step, height - n * 2) : ctx.moveTo(0, height - n * 2)));
ctx.stroke();
}}
/>
values.update((v) => [...v.slice(1), Math.random() * 80])}>Next
);
}
mount(App);
```
## Animating without redrawing
Any argument typed `CanvasNumber` and any paint (`fillStyle`, `strokeStyle`) accepts a `NativeNode`. The canvas then reads the node while it draws, and redraws natively on every frame of its animation:
```tsx
import { createNativeValue, Easing } from "nsh/animated";
import { Canvas } from "nsh/canvas";
const progress = createNativeValue(0);
{
const c = width / 2;
ctx.lineWidth = 12;
ctx.lineCap = "round";
ctx.strokeStyle = "#6366f1";
ctx.beginPath();
// The end angle is a native node: animating `progress` redraws natively.
ctx.arc(c, c, c - 10, -Math.PI / 2, progress.mul(Math.PI * 2).sub(Math.PI / 2));
ctx.stroke();
}}
/>;
progress.timing(0.75, { duration: 900, easing: Easing.inOut(Easing.cubic) });
```
```ts
type CanvasNumber = number | NativeNode;
type CanvasPaint = string | CanvasGradient | NativeNode;
```
## CanvasRenderingContext2D
The context `draw` receives. Calls only record operations; the platform draws them after `draw` returns.
### State and transforms
```ts
readonly canvas: { readonly width: number; readonly height: number };
save(): void;
restore(): void;
translate(x: CanvasNumber, y: CanvasNumber): void;
scale(x: CanvasNumber, y: CanvasNumber): void;
rotate(angle: CanvasNumber): void;
transform(a: CanvasNumber, b: CanvasNumber, c: CanvasNumber, d: CanvasNumber, e: CanvasNumber, f: CanvasNumber): void;
setTransform(a: CanvasNumber, b: CanvasNumber, c: CanvasNumber, d: CanvasNumber, e: CanvasNumber, f: CanvasNumber): void;
resetTransform(): void;
```
| Member | Description |
|---|---|
| `canvas` | The canvas's size in points. |
| `save()` / `restore()` | Pushes / pops the drawing state (transform, clip, styles). |
| `translate(x, y)` | Moves the origin. |
| `scale(x, y)` | Scales the axes. |
| `rotate(angle)` | Rotates the axes; radians, clockwise. |
| `transform(a, b, c, d, e, f)` | Multiplies the transform by the matrix `[a c e; b d f]`. |
| `setTransform(a, b, c, d, e, f)` | Replaces the transform. |
| `resetTransform()` | Back to the identity. |
### Properties
| Property | Type | Description |
|---|---|---|
| `fillStyle` | `CanvasPaint` | Color, gradient or native color node used by `fill`, `fillRect`, `fillText`. |
| `strokeStyle` | `CanvasPaint` | The same for strokes. |
| `lineWidth` | `CanvasNumber` | Stroke width in points. |
| `lineCap` | `"butt" \| "round" \| "square"` | How strokes end. |
| `lineJoin` | `"miter" \| "round" \| "bevel"` | How stroke segments join. |
| `miterLimit` | `number` | Miter joins longer than this (in line widths) are beveled. |
| `lineDashOffset` | `CanvasNumber` | Where the dash pattern starts. |
| `globalAlpha` | `CanvasNumber` | Opacity of what is drawn next, 0..1. |
| `globalCompositeOperation` | `string` | How what is drawn next combines with the canvas (`"source-over"`, `"multiply"`, ...). |
| `shadowColor` | `string` | The color of shadows (transparent: none). |
| `shadowBlur` | `number` | Shadow blur in points. |
| `shadowOffsetX`, `shadowOffsetY` | `number` | Shadow offset in points. |
| `font` | `string` | CSS `font` shorthand, e.g. `"600 14px Inter"` (sizes in `px`, `pt` or `rem`). |
| `textAlign` | `"start" \| "end" \| "left" \| "right" \| "center"` | Where text sits horizontally relative to its x. |
| `textBaseline` | `"alphabetic" \| "top" \| "hanging" \| "middle" \| "ideographic" \| "bottom"` | Where text sits vertically relative to its y. |
Dashes: `setLineDash(segments: number[])` and `getLineDash(): number[]`.
### Paths
The context has a current path, built with the same methods as [`Path2D`](#path2d):
```ts
beginPath(): void;
moveTo(x, y): void;
lineTo(x, y): void;
quadraticCurveTo(cpx, cpy, x, y): void;
bezierCurveTo(cp1x, cp1y, cp2x, cp2y, x, y): void;
arc(x, y, radius, startAngle, endAngle, counterclockwise?: boolean): void;
arcTo(x1, y1, x2, y2, radius): void;
ellipse(x, y, radiusX, radiusY, rotation, startAngle, endAngle, counterclockwise?: boolean): void;
rect(x, y, w, h): void;
roundRect(x, y, w, h, radii?: CanvasNumber | CanvasNumber[]): void;
closePath(): void;
```
All coordinates are `CanvasNumber`s. Angles are in radians, clockwise from the x axis. `roundRect`'s `radii` is one radius or 2–4 as in CSS `border-radius` (top left first).
### Drawing
```ts
fill(path?: Path2D | "nonzero" | "evenodd", rule?: "nonzero" | "evenodd"): void;
stroke(path?: Path2D): void;
clip(path?: Path2D | "nonzero" | "evenodd", rule?: "nonzero" | "evenodd"): void;
fillRect(x, y, w, h): void;
strokeRect(x, y, w, h): void;
clearRect(x, y, w, h): void;
```
| Method | Description |
|---|---|
| `fill(path?, rule?)` | Fills the current path (or `path`) with `fillStyle`, by the nonzero (default) or even-odd rule. |
| `stroke(path?)` | Strokes the current path (or `path`) with `strokeStyle`. |
| `clip(path?, rule?)` | Restricts what is drawn next to the path. |
| `fillRect`, `strokeRect` | A rectangle, without touching the current path. |
| `clearRect` | Makes a rectangle transparent. |
### Text
```ts
fillText(text: string, x: CanvasNumber, y: CanvasNumber, maxWidth?: number): void;
strokeText(text: string, x: CanvasNumber, y: CanvasNumber, maxWidth?: number): void;
measureText(text: string): TextMetrics; // { width: number }
```
`measureText` is synchronous: it measures with the platform's text engine and the current `font`. Only `width` is reported.
```ts
ctx.font = "11px system-ui";
const label = "100";
ctx.fillText(label, 40 - ctx.measureText(label).width, 20);
```
### Gradients
```ts
createLinearGradient(x0: number, y0: number, x1: number, y1: number): CanvasGradient;
createRadialGradient(x0: number, y0: number, r0: number, x1: number, y1: number, r1: number): CanvasGradient;
createConicGradient(startAngle: number, x: number, y: number): CanvasGradient;
class CanvasGradient {
addColorStop(offset: number, color: string): void; // offset 0..1, any CSS color
}
```
```ts
const g = ctx.createLinearGradient(0, 0, 0, height);
g.addColorStop(0, "rgba(99, 102, 241, 0.4)");
g.addColorStop(1, "rgba(99, 102, 241, 0)");
ctx.fillStyle = g;
ctx.fillRect(0, 0, width, height);
```
### Images
```ts
drawImage(image: string | ImageAsset | { uri: string }, ...args: CanvasNumber[]): void;
```
Draws an image given as a URL, an imported asset (`import logo from "./logo.png"`) or `{ uri }`. It is loaded natively; the canvas draws again once it has loaded. Three forms, as in HTML:
- `drawImage(image, dx, dy)`: at its natural size.
- `drawImage(image, dx, dy, dw, dh)`: scaled into a rectangle.
- `drawImage(image, sx, sy, sw, sh, dx, dy, dw, dh)`: part of it (source in image pixels).
## Path2D
```ts
class Path2D {
constructor(path?: Path2D | string);
addPath(path: Path2D): void;
// and every path method: moveTo, lineTo, quadraticCurveTo, bezierCurveTo,
// arc, arcTo, ellipse, rect, roundRect, closePath
}
```
A reusable path to fill, stroke or clip. The constructor copies another `Path2D` or parses SVG path data natively:
```ts
const heart = new Path2D("M12 21s-7-4.5-9.5-9A5.5 5.5 0 0 1 12 6a5.5 5.5 0 0 1 9.5 6c-2.5 4.5-9.5 9-9.5 9z");
ctx.save();
ctx.scale(4, 4);
ctx.fillStyle = "#ef4444";
ctx.fill(heart);
ctx.restore();
```
## Not supported
Pixel access (`getImageData`, `putImageData`), `filter`, `isPointInPath` and SVG images are not available. An invalid color or font drops that one operation, as in HTML Canvas.
Source: https://nsh.dev/modules/nsh-canvas
---
# nsh/device
`nsh/device` tells the app about the device and its environment. `windowSize`, `colorScheme` and `fontScale` are reactive accessors: read them in JSX, `computed` or `effect`, and what reads them updates when the platform reports a change (rotation, a window resize, switching to dark mode, changing the text size).
```ts
import { colorScheme, fontScale, I18nManager, PixelRatio, Platform, windowSize } from "nsh/device";
```
See [Device and environment](https://nsh.dev/docs/device-and-environment.md) for the guide.
## windowSize
```ts
const windowSize: Accessor<{ width: number; height: number }>;
```
The size of the surface in points: the window, or the embedded `NshAppView` / `NshView`. Reactive.
```tsx
const wide = () => windowSize().width >= 600;
{/* ... */}
```
## colorScheme
```ts
const colorScheme: Accessor<"light" | "dark">;
```
The system appearance. Reactive. Tailwind's `dark:` variant follows it too (see [Tailwind](https://nsh.dev/docs/tailwind.md)).
```tsx
const dark = () => colorScheme() === "dark";
```
## fontScale
```ts
const fontScale: Accessor;
```
The user's preferred text size relative to the default (`1` is the default size; Dynamic Type on iOS, font size on Android). Reactive. Text already scales with it unless `allowFontScaling={false}`; read it when other sizes should follow, such as icons next to text.
## Platform
```ts
const Platform: {
readonly OS: "ios" | "android" | "macos" | "windows" | "linux" | "web";
readonly isDesktop: boolean;
readonly Version: string | number;
readonly isPad: boolean;
readonly isTV: boolean;
readonly isMacCatalyst: boolean;
readonly constants: {
readonly osVersion: string | number;
readonly interfaceIdiom: "phone" | "pad" | "mac" | "tv" | "desktop";
};
select(spec: PlatformSelectSpec): T | undefined;
};
```
The platform the app runs on, constant while it runs.
| Member | Description |
|---|---|
| `OS` | The operating system. iOS, the iOS simulator and Mac Catalyst report `"ios"`, browsers `"web"`. |
| `isDesktop` | A desktop OS (macOS, Windows, Linux), or a browser with a mouse and a wide window. |
| `Version` | iOS and desktops: the version string (`"17.5"`); Android: the API level (a number). |
| `isPad` | A tablet: an iPad, or an Android tablet. |
| `isTV` | A TV (Apple TV idiom, Android TV). |
| `isMacCatalyst` | Mac Catalyst in the Mac idiom. |
| `constants.osVersion` | The same as `Version`. |
| `constants.interfaceIdiom` | `"phone"`, `"pad"`, `"mac"`, `"tv"` or `"desktop"`. |
| `select(spec)` | The value for this platform (see below). |
### Platform.select
```ts
interface PlatformSelectSpec {
ios?: T;
android?: T;
macos?: T;
windows?: T;
linux?: T;
mobile?: T; // iOS and Android
desktop?: T; // macOS, Windows and Linux
native?: T; // every platform
default?: T;
}
```
Returns the value for the current OS (`ios`, `android`, `web`, ...), else `mobile` / `desktop`, else `native` (every platform but the web), else `default`, else `undefined`. In a browser, `mobile` or `desktop` follows the idiom.
```ts
const shadow = Platform.select({
ios: { shadowColor: "#000", shadowOpacity: 0.15, shadowRadius: 8 },
android: { elevation: 4 },
});
```
> [!TIP]
> For props that only one platform has, prefer the `ios={{…}}` / `android={{…}}` props of elements and components to branching on `Platform` (see [Platform-specific code](https://nsh.dev/docs/platform-specific.md)).
## PixelRatio
```ts
const PixelRatio: {
get(): number;
getFontScale(): number;
getPixelSizeForLayoutSize(size: number): number;
roundToNearestPixel(size: number): number;
};
```
Device pixels per point.
| Method | Description |
|---|---|
| `get()` | Device pixels per point (e.g. `3` on most iPhones). |
| `getFontScale()` | The same as `fontScale()`, without tracking. |
| `getPixelSizeForLayoutSize(size)` | `size` points in whole device pixels. |
| `roundToNearestPixel(size)` | `size` rounded to the nearest device pixel, in points. |
```ts
const hairline = 1 / PixelRatio.get();
```
`PixelRatio` is not reactive. A window can move to another screen (on Mac Catalyst, or an external display); read it in components that also read `windowSize()`, so they run again when the window changes.
## I18nManager
```ts
const I18nManager: {
isRTL: boolean; // reactive
doLeftAndRightSwapInRTL: true;
allowRTL(allow: boolean): void;
forceRTL(force: boolean): void;
getConstants(): { isRTL: boolean; doLeftAndRightSwapInRTL: boolean };
};
```
Right-to-left layout. In right-to-left layout, rows run from the right, and `left` / `right` style properties (`marginLeft`, `paddingRight`, ...) mean start and end. Changes apply at once; no restart is needed.
| Member | Description |
|---|---|
| `isRTL` | Whether the app lays out right to left. Reading it is tracked. |
| `doLeftAndRightSwapInRTL` | Always `true`: `left` and `right` swap in right-to-left layout. |
| `allowRTL(allow)` | Follow right-to-left languages (default `true`). |
| `forceRTL(force)` | Lay out right to left whatever the language. |
| `getConstants()` | `{ isRTL, doLeftAndRightSwapInRTL }`. |
```tsx
I18nManager.forceRTL(!I18nManager.isRTL)}>
{I18nManager.isRTL ? "Right to left" : "Left to right"}
```
## Related
- Safe area insets and the keyboard (`safeArea()`, `keyboard()`, `useSafeAreaInsets()`) are part of [`nsh/ui/mobile`](https://nsh.dev/components.md).
- Screen reader and reduced motion settings are in [`nsh/accessibility`](https://nsh.dev/modules/nsh-accessibility.md).
Source: https://nsh.dev/modules/nsh-device
---
# nsh/gesture
`nsh/gesture` exports `PanResponder`: drag gestures on top of the gesture responder system, with `onPanResponder…` callbacks that receive the touch event and a gesture state. It tracks the touches' centroid, the distance moved and the velocity for you.
```ts
import { PanResponder, type PanResponderGestureState } from "nsh/gesture";
```
> [!TIP]
> `PanResponder` delivers every move to JS, so it suits gestures that change app state (reordering, swipe-to-delete decisions). To move a view with the finger, the `pan={{ x, y }}`, `pinch` and `rotation` props are faster: the drag writes [native values](https://nsh.dev/modules/nsh-animated.md) on the UI thread and JS only hears `onPanStart` / `onPanEnd`. See [Touch and gestures](https://nsh.dev/docs/touch-and-gestures.md).
## PanResponder.create
```ts
const PanResponder: { create(config: PanResponderCallbacks): PanResponderInstance };
interface PanResponderInstance {
panHandlers: TouchProps;
}
```
Creates a pan responder. Spread its `panHandlers` on the view that should receive the gesture:
```tsx
import { mount, signal, View } from "nsh";
import { PanResponder } from "nsh/gesture";
function App() {
const pos = signal({ x: 0, y: 0 });
let start = { x: 0, y: 0 };
const pan = PanResponder.create({
onStartShouldSetPanResponder: () => true,
onPanResponderGrant: () => (start = pos()),
onPanResponderMove: (_, g) => pos.set({ x: start.x + g.dx, y: start.y + g.dy }),
onPanResponderRelease: (_, g) => console.log(`released at ${g.vx.toFixed(2)} pt/ms`),
});
return (
);
}
mount(App);
```
## Callbacks
Every callback receives the touch event (`GestureResponderEvent`, from [`nsh`](https://nsh.dev/modules/nsh.md#types)) and the gesture state.
```ts
type PanHandler = (e: GestureResponderEvent, gestureState: PanResponderGestureState) => R;
```
| Callback | Returns | Called when |
|---|---|---|
| `onStartShouldSetPanResponder` | `boolean` | A finger goes down: return `true` to become the responder. Asked from the touched view outwards. |
| `onStartShouldSetPanResponderCapture` | `boolean` | The same, asked from the outside in before the bubbling one (a parent taking the gesture from its children). |
| `onMoveShouldSetPanResponder` | `boolean` | A finger moves: return `true` to become the responder (e.g. only once `Math.abs(g.dx) > 10`). |
| `onMoveShouldSetPanResponderCapture` | `boolean` | The same, capture phase. |
| `onPanResponderGrant` | | This view is the responder now: the gesture starts. |
| `onPanResponderReject` | | Another view is the responder and would not let go. |
| `onPanResponderStart` | | A finger went down while responding. |
| `onPanResponderMove` | | The fingers moved. |
| `onPanResponderEnd` | | A finger lifted while responding. |
| `onPanResponderRelease` | | The last finger lifted: the gesture ended. |
| `onPanResponderTerminate` | | Another view (or a native gesture, like a scroll view) took the touches. |
| `onPanResponderTerminationRequest` | `boolean` | Another view wants the touches: return `false` to keep them (default `true`). |
| `onShouldBlockNativeResponder` | `boolean` | Whether to keep native scroll views from taking the touches while responding (default `true`). |
## PanResponderGestureState
```ts
interface PanResponderGestureState {
stateID: number;
moveX: number; // latest centroid of the touches (surface points)
moveY: number;
x0: number; // the centroid when the responder was granted
y0: number;
dx: number; // distance moved since the grant
dy: number;
vx: number; // velocity, points per millisecond
vy: number;
numberActiveTouches: number;
}
```
| Field | Description |
|---|---|
| `stateID` | Identifies the gesture. |
| `moveX`, `moveY` | Latest position of the touches' centroid, in surface points. |
| `x0`, `y0` | Where the centroid was when the responder was granted. |
| `dx`, `dy` | Distance moved since the grant. |
| `vx`, `vy` | Velocity in points per millisecond. |
| `numberActiveTouches` | Fingers down. |
## Inside scroll views
While a pan responder is the responder, native scroll views around it do not scroll (`onShouldBlockNativeResponder` defaults to `true`). To let a horizontal swipe coexist with vertical scrolling, become the responder only on horizontal moves:
```ts
const swipe = PanResponder.create({
onMoveShouldSetPanResponder: (_, g) => Math.abs(g.dx) > 10 && Math.abs(g.dx) > Math.abs(g.dy),
onPanResponderMove: (_, g) => offset.set(g.dx),
onPanResponderRelease: (_, g) => (Math.abs(g.dx) > 120 ? remove() : offset.set(0)),
onPanResponderTerminate: () => offset.set(0),
});
```
The underlying responder props (`onStartShouldSetResponder`, `onResponderGrant`, `onTouchStart`, ...) can also be used directly on any element; see [Elements](https://nsh.dev/elements.md).
Source: https://nsh.dev/modules/nsh-gesture
---
# nsh/image
`nsh/image` exports `Image`, the [``](https://nsh.dev/elements/image.md) element that shows images. The same `Image` also asks the native image loader about images without showing them: their size, and loading them before they are shown.
```ts
import { Image, type ImageSize } from "nsh/image";
```
`Image` is a component carrying two functions: `` renders the native image view (the compiler makes it in place, as it does ``), and `Image.getSize` / `Image.prefetch` are called on it directly.
```ts
const Image: ((props: ImageProps) => YNode) & {
getSize(source: string | { uri: string }): Promise;
prefetch(source: string | { uri: string }): Promise;
};
```
See [Images and assets](https://nsh.dev/docs/images-and-assets.md) for the guide.
## Image.getSize
```ts
getSize(source: string | { uri: string }): Promise;
interface ImageSize {
width: number; // pixels
height: number;
}
```
Loads the image and resolves with its size in pixels (of the source image, not a downsampled copy). The loaded bytes stay in memory, so the `` that shows it next does not download it again. The promise rejects if the image cannot be loaded.
```tsx
import { signal } from "nsh";
import { Image } from "nsh/image";
const url = "https://picsum.photos/seed/nsh/1200/800";
const ratio = signal(1);
Image.getSize(url).then(({ width, height }) => ratio.set(width / height));
;
```
## Image.prefetch
```ts
prefetch(source: string | { uri: string }): Promise;
```
Loads an image ahead of showing it. Resolves with `true` once its bytes are in memory (`false` when it could not be loaded). Use it for the next page of a gallery or the image behind a tap.
```ts
const next = photos.slice(index + 1, index + 4);
next.forEach((p) => Image.prefetch(p.url));
```
## Related
- [``](https://nsh.dev/elements/image.md): `src`, `resizeMode`, `defaultSource`, `onLoad`, `onProgress`, `onError` and the other props.
- Importing an image file (`import logo from "./logo.png"`) gives an `ImageAsset` (`{ uri, width, height }`, sized in points) that `` and canvas `drawImage` accept.
> [!NOTE]
> When your app declares `capabilities` in `nsh.json`, `getSize` and `prefetch` of remote URLs must be allowed under `network`, like `fetch`. See [Sandbox and capabilities](https://nsh.dev/docs/sandbox-and-capabilities.md).
Source: https://nsh.dev/modules/nsh-image
---
# nsh/clipboard
`nsh/clipboard` reads and writes the text on the system clipboard: `UIPasteboard` on iOS, `ClipboardManager` on Android, `navigator.clipboard` on the web.
```ts
import { Clipboard } from "nsh/clipboard";
```
```ts
const Clipboard: {
setString(text: string): void;
getString(): Promise;
hasString(): Promise;
};
```
The user can also copy from a [selectable text](https://nsh.dev/elements/text.md) themselves; `Clipboard` is for copying from code, such as a Copy button.
## Clipboard.setString
```ts
setString(text: string): void;
```
Puts `text` on the clipboard, replacing what was there.
```tsx
import { signal, Text, View } from "nsh";
import { Clipboard } from "nsh/clipboard";
const copied = signal(false);
{
Clipboard.setString(code);
copied.set(true);
}}
>
{copied() ? "Copied" : "Copy"};
```
## Clipboard.getString
```ts
getString(): Promise;
```
Resolves with the clipboard's text, or `""` when it holds none. On iOS, reading the clipboard can show the system's paste notice ("App pasted from …"), and on Android 10 and later only the app in the foreground can read it. Read it when the user asks to paste, not on every screen.
```ts
const pasted = await Clipboard.getString();
if (pasted) code.set(pasted.trim());
```
## Clipboard.hasString
```ts
hasString(): Promise;
```
Resolves with whether the clipboard holds text, without reading it: no paste notice on iOS. Use it to decide whether to show a Paste button. On the web, browsers only tell by reading, so it may ask the user for permission.
```ts
const canPaste = signal(false);
Clipboard.hasString().then((has) => canPaste.set(has));
```
> [!NOTE]
> When your app declares `capabilities` in `nsh.json`, the clipboard must be granted with `"clipboard": true`. Without it `setString` does nothing, `getString` resolves `""` and `hasString` `false`. See [Sandbox and capabilities](https://nsh.dev/docs/sandbox-and-capabilities.md).
Source: https://nsh.dev/modules/nsh-clipboard
---
# nsh/keymap
`nsh/keymap` binds the app's keyboard shortcuts: a keystroke such as `"mod+k"` and the function it runs.
```ts
import { keymap } from "nsh/keymap";
```
```ts
const keymap: {
bind(keystroke: string, handler: (keystroke: string) => void, options?: KeyBindingOptions): () => void;
bind(bindings: Record void>, options?: KeyBindingOptions): () => void;
label(keystroke: string): string;
};
interface KeyBindingOptions {
when?: "always" | "notEditing";
repeat?: boolean;
}
```
Shortcuts work on macOS, Windows, Linux and in browsers (the browser's own keys, such as Ctrl+T or Cmd+W, never reach the page). On iOS and Android, `bind` keeps the bindings but no key fires them yet, so the same code runs everywhere.
## keymap.bind
```ts
bind(keystroke: string, handler: (keystroke: string) => void, options?: KeyBindingOptions): () => void;
```
Runs `handler` when `keystroke` is pressed and returns a function that removes the binding. The handler gets the keystroke as you wrote it, in its case.
```tsx
import { mount, signal, Text } from "nsh";
import { keymap } from "nsh/keymap";
const open = signal(false);
keymap.bind("mod+k", () => open.set(true));
keymap.bind("escape", () => open.set(false));
mount(() => {open() ? "Search…" : "Press ⌘K"});
```
Pass an object to bind several keystrokes at once; the function returned removes them all:
```ts
const unbind = keymap.bind({
"mod+s": save,
"mod+shift+p": openPalette,
f5: refresh,
});
```
A binding made while a component renders is removed with the component, so a screen's shortcuts live as long as the screen:
```tsx
function Editor() {
keymap.bind("mod+enter", send);
return ;
}
```
With the dev server, editing a module removes the bindings its top-level code made before it runs again, so hot reload never leaves stale shortcuts behind.
## keymap.label
```ts
label(keystroke: string): string;
```
Returns `keystroke` as menus and hints show it where the app runs: `"⇧⌘L"` on macOS and iOS (and in a browser on a Mac, iPhone or iPad), `"Ctrl+Shift+L"` on Windows, Linux and other browsers, for `"mod+shift+l"`. Use it to show an app's shortcuts with the keys that work:
```tsx
Search {keymap.label("mod+k")}
```
It throws a `TypeError` for an invalid keystroke, as `bind` does.
## Keystrokes
A keystroke is modifiers and a key joined by `+`: `"mod+k"`, `"ctrl+shift+p"`, `"alt+enter"`, `"escape"`, `"f5"`, `"cmd+-"` (Command and minus).
| Modifier | macOS | Windows | Linux |
|---|---|---|---|
| `mod` (also `cmdorctrl`) | Command (⌘) | Ctrl | Ctrl |
| `cmd` (also `command`, `meta`, `super`, `win`) | Command (⌘) | Windows key | Super |
| `ctrl` (also `control`) | Control (⌃) | Ctrl | Ctrl |
| `alt` (also `option`, `opt`) | Option (⌥) | Alt | Alt |
| `shift` | Shift (⇧) | Shift | Shift |
In a browser, keys follow the platform the browser runs on: `mod` is Command on a Mac, iPhone or iPad and Ctrl elsewhere.
Use `mod` for an app's own shortcuts: it is the key each platform's apps use for them (⌘K on macOS, Ctrl+K on Windows and Linux), so one spelling fits everywhere. The Windows key and Super mostly belong to the system, so `cmd` is rarely right outside macOS.
The key is a letter, digit or punctuation key, written as the character it types on a US keyboard without Shift (`k`, `1`, `,`, `/`, `[`; `"shift+/"` for `?`, `"shift+k"` for Shift+K), or a name: `plus` (the `=` / `+` key, also written `=`), `minus` (also `-`), `space`, `enter` (`return`), `escape` (`esc`), `tab`, `backspace`, `delete` (`del`), `insert`, the arrows `up`, `down`, `left`, `right` (`arrowup`, …), `home`, `end`, `pageup`, `pagedown`, `f1` to `f24`. Case does not matter: `"Mod+Shift+K"` is `"mod+shift+k"`, and a capital letter is the same key, not Shift (`"mod+K"` is `"mod+k"`). Lowercase is the recommended style, which these pages use. `"mod-k"` and `"mod++"` are errors whose messages give the right spelling. Menu shortcuts in `nsh/desktop` take the same keystrokes.
Shortcuts work with any keyboard layout: on a Cyrillic or Greek layout, `mod+k` is the key that types K on a US layout, as menu shortcuts are.
`bind` throws a `TypeError` that says what is wrong with an invalid keystroke (`keymap.bind: invalid keystroke "cmd+kk": unknown key "kk"`), and refuses the keystrokes the system keeps for itself, so an app can never break them:
| Platform | Reserved |
|---|---|
| macOS, iOS | Cmd+Q (quit), Cmd+H (hide), Option+Cmd+H (hide others), Cmd+M (minimize), Ctrl+Cmd+F (full screen) |
| Linux, Windows, Android | Ctrl+Q (quit), Alt+F4 (close), F11 (full screen) |
| Web | None (the browser keeps its own keys) |
## While typing
A text input with the keyboard focus needs its keys: typing, moving the caret, deleting, and select all, cut, copy, paste, undo and redo. So by default a binding does not fire while a text input (or a web view) has the focus if its keystroke is one of these: plain keys and keys with only Shift or Option, the arrows, Home, End, Page Up / Down, Return, Tab, Escape, Backspace, Delete, and `mod+a`, `mod+c`, `mod+v`, `mod+x`, `mod+z`. Other keystrokes with Command or Control, and the function keys, fire wherever the focus is.
`when` changes that:
| `when` | Fires |
|---|---|
| (default) | As above |
| `"always"` | Also in text inputs, before the input sees the key |
| `"notEditing"` | Never while a text input has the focus |
```ts
// Sends from the message field too.
keymap.bind("mod+enter", send, { when: "always" });
// Only when the user is not typing.
keymap.bind("mod+e", archive, { when: "notEditing" });
```
## Which binding runs
A key that fires a binding is consumed: it is not typed, the system does not beep, and NSH's own keys do not see it (Escape going back, Tab moving the focus, the arrows scrolling). When several bindings have the same keystroke, the newest one that may fire runs, so a dialog can bind Escape over the screen's binding and the screen's comes back when the dialog's goes. Bindings are checked natively the moment the key is pressed; the handler runs right after, on the JS thread.
Holding a key down fires its binding once, until the key is released; with `repeat: true` it fires again on every auto-repeat (to step through a list, say):
```ts
keymap.bind("alt+down", nextItem, { repeat: true });
```
Menu shortcuts give way to a binding of the same keystroke: the application menu's ([`nsh/desktop`](./nsh-desktop)) on every desktop, and on macOS the built-in menus' too (Cmd+W, Cmd+C, ...). While you develop with `nsh run`, Cmd+R / Ctrl+R always reloads the app, even if it binds `mod+r`; a packaged app can bind it.
Source: https://nsh.dev/modules/nsh-keymap
---
# nsh/native
`nsh/native` connects your app to native views that the host app provides. An iOS or Android app that embeds NSH can register its own views (a map, a chart, a camera preview) by name; `nativeComponent` gives you a typed component for one of them. Because NSH renders with real native views, such a component sits in the view hierarchy like any other view: it is laid out by NSH, clips and scrolls with its parents, and overlaps correctly.
```ts
import { nativeComponent } from "nsh/native";
```
See [Native components](https://nsh.dev/docs/native-components.md) for registering the native side, and [Embedding on iOS](https://nsh.dev/docs/embedding-ios.md) / [Embedding on Android](https://nsh.dev/docs/embedding-android.md).
## nativeComponent
```ts
function nativeComponent
(
name: string,
spec: NativeComponentSpec
,
): (props: NativeComponentProps
) => YNode;
interface NativeComponentSpec
{
/** Its own props: sent to the component (any JSON value), each when it changes. */
props: readonly (keyof P & string)[];
/** The events it emits. */
events?: readonly (keyof E & string)[];
}
```
Returns a component for the native component the embedding app registered as `name` (Swift `NshAppView.register(component:)`, Java `NshView.registerComponent`).
- `P` is the type of its own props. Each prop listed in `spec.props` is sent to the native side as JSON when it changes; setting it to `undefined` removes it.
- `E` maps event names to their payloads. An event `regionChange` becomes the handler prop `onRegionChange`.
- The component also takes every [view prop](https://nsh.dev/elements/view.md) except `children`: `style`, `class`, layout, accessibility, touch handlers and `ref`.
```ts
type NativeComponentProps
= Omit & P & NativeEventHandlers;
type NativeEventHandlers = { [K in keyof E & string as `on${Capitalize}`]?: (data: E[K]) => void };
```
### Size
The component is laid out like a view, at the size its style gives it. Where the style leaves a dimension open, it uses the size the native component reports (as an image uses its natural size).
### Commands
Its `ref` receives a `YNode`; `ref.command(name, args)` calls command `name` of the native component with `args` (any JSON value):
```ts
map.command("zoomTo", { level: 3 });
```
### When nothing is registered
Where nothing is registered under `name` (a different host app, the dev host, the browser), the component renders an empty view and a warning is logged once. Keep the app usable without it, or check for your host before using it.
## Example
The badge here is a native view of the host app, registered as `embed.badge`. NSH lays it out, sends it `text` and `color`, receives its `tap` events, and calls its `pulse` command:
```tsx
import { mount, signal, type YNode } from "nsh";
import { nativeComponent } from "nsh/native";
import { SafeAreaView } from "nsh/ui/mobile";
const Badge = nativeComponent<{ text: string; color?: string }, { tap: { count: number } }>("embed.badge", {
props: ["text", "color"],
events: ["tap"],
});
function App() {
const taps = signal(0);
let badge: YNode | undefined;
return (
(badge = node)}
text={taps() === 0 ? "Tap the native badge" : `Tapped ${taps()}×`}
color="#8250df"
onTap={(e) => {
taps.set(e.count);
badge?.command("pulse");
}}
style={{ alignSelf: "flex-start", borderRadius: 8, overflow: "hidden" }}
/>
);
}
mount(App);
```
> [!NOTE]
> Props, events and command arguments cross to the native side as JSON. Registered components are the host app's own code: NSH's [sandbox limits and capabilities](https://nsh.dev/docs/sandbox-and-capabilities.md) do not apply to what they do.
Source: https://nsh.dev/modules/nsh-native
---
# nsh/accessibility
`nsh/accessibility` reads the user's accessibility settings (is a screen reader running, did they ask for less motion) and talks to the screen reader. The accessibility props of elements (`accessibilityLabel`, `accessibilityRole`, ...) are documented under [Elements](https://nsh.dev/elements.md); the guide is [Accessibility](https://nsh.dev/docs/accessibility.md).
```ts
import { AccessibilityInfo, reduceMotion, screenReaderEnabled } from "nsh/accessibility";
```
## screenReaderEnabled
```ts
const screenReaderEnabled: Accessor;
```
Whether a screen reader (VoiceOver, TalkBack) is running. Reactive: what reads it updates when the user turns the screen reader on or off.
```tsx
Next photo
}>
Swipe for the next photo
```
## reduceMotion
```ts
const reduceMotion: Accessor;
```
Whether the user asked for less motion (Reduce Motion on iOS, Remove animations on Android). Reactive. Tailwind classes follow it too with the `motion-reduce:` variant.
```ts
const open = () => {
if (reduceMotion()) panel.set(0);
else panel.spring(0);
};
```
## AccessibilityInfo
```ts
const AccessibilityInfo: {
announceForAccessibility(message: string): void;
setAccessibilityFocus(node: YNode): void;
isScreenReaderEnabled(): Promise;
isReduceMotionEnabled(): Promise;
addEventListener(
event: "screenReaderChanged" | "reduceMotionChanged",
listener: (enabled: boolean) => void,
): { remove(): void };
};
```
The system's accessibility settings, read once or followed with listeners. `screenReaderEnabled()` and `reduceMotion()` are their reactive forms; prefer them in components.
| Member | Description |
|---|---|
| `announceForAccessibility(message)` | The screen reader reads `message` (a VoiceOver announcement, TalkBack). Use it for results that appear without focus moving, such as "Saved". |
| `setAccessibilityFocus(node)` | Moves the screen reader's focus to `node` (an element's `ref`). |
| `isScreenReaderEnabled()` | Resolves with whether a screen reader runs. |
| `isReduceMotionEnabled()` | Resolves with whether reduced motion is on. |
| `addEventListener(event, listener)` | Calls `listener` with the new value when the setting changes; returns a subscription to `remove()`. |
```tsx
import { cleanup, Text, View, type YNode } from "nsh";
import { AccessibilityInfo } from "nsh/accessibility";
function Saved() {
let note!: YNode;
const save = () => {
AccessibilityInfo.announceForAccessibility("Saved");
AccessibilityInfo.setAccessibilityFocus(note);
};
const sub = AccessibilityInfo.addEventListener("screenReaderChanged", (on) => console.log("screen reader", on));
cleanup(() => sub.remove());
return (
SaveYour changes are kept on this device.
);
}
```
Source: https://nsh.dev/modules/nsh-accessibility
---
# nsh/ios
`nsh/ios` holds APIs that only iOS has. The module can be imported on every platform, so one bundle runs everywhere: elsewhere its components render nothing, and `isAvailable` tells you whether you are on iOS.
```ts
import { InputAccessoryView, isAvailable } from "nsh/ios";
```
iOS-only *props* of elements and components are not here: they go in the `ios={{…}}` prop (for example ``). See [Platform-specific code](https://nsh.dev/docs/platform-specific.md).
## isAvailable
```ts
const isAvailable: boolean;
```
`true` on iOS, the iOS simulator and Mac Catalyst; `false` elsewhere.
## InputAccessoryView
```ts
function InputAccessoryView(props: InputAccessoryViewProps): YNode | null;
interface InputAccessoryViewProps {
nativeID: string;
style?: StyleProp;
class?: string;
children?: JSX.Element;
}
```
A bar over the keyboard (UIKit's `inputAccessoryView`), shown while an input whose `ios={{ inputAccessoryViewID }}` matches its `nativeID` has focus. It is as wide as the window and as tall as its content, and moves with the keyboard natively.
| Prop | Type | Description |
|---|---|---|
| `nativeID` | `string` | Inputs with this `inputAccessoryViewID` show the bar. |
| `style` | `StyleProp` | The bar's style (its background, for example). |
| `class` | `string` | Tailwind classes. |
| `children` | `JSX.Element` | The bar's content. |
It renders nothing where it is placed in the tree, and nothing at all on Android, so you can place it next to the input it belongs to.
```tsx
import { Input, signal, Text, View, type YNode } from "nsh";
import { InputAccessoryView } from "nsh/ios";
function Note() {
const text = signal("");
let input: YNode | undefined;
return (
(input = n)}
value={text()}
onChangeText={(v) => text.set(v)}
placeholder="Write a note"
ios={{ inputAccessoryViewID: "note-bar" }}
/>
{text().length} characters input?.blur()} style={{ fontWeight: "600" }}>Done
);
}
```
See also [Forms and keyboard](https://nsh.dev/docs/forms-and-keyboard.md).
Source: https://nsh.dev/modules/nsh-ios
---
# nsh/android
`nsh/android` holds APIs that only Android has. The module can be imported on every platform, so one bundle runs everywhere: elsewhere its functions do nothing, and `isAvailable` tells you whether you are on Android.
```ts
import { BackHandler, isAvailable, setStatusBarBackgroundColor, Toast } from "nsh/android";
```
Android-only *props* of elements and components are not here: they go in the `android={{…}}` prop (for example ``). See [Platform-specific code](https://nsh.dev/docs/platform-specific.md).
## isAvailable
```ts
const isAvailable: boolean;
```
`true` on Android; `false` elsewhere.
## Toast
```ts
const Toast: {
show(message: string, duration?: "short" | "long" | null): void;
};
```
Shows a short message at the bottom of the screen (Android's toast). `duration` is `"short"` by default.
```ts
import { Platform } from "nsh/device";
import { Toast } from "nsh/android";
import { Alert } from "nsh/ui/mobile";
const notify = (message: string) =>
Platform.OS === "android" ? Toast.show(message) : Alert.alert("Done", message);
```
## BackHandler
```ts
const BackHandler: {
addEventListener(event: "hardwareBackPress", listener: () => boolean | void): { remove(): void };
exitApp(): void;
};
```
The system back button and back gesture. Modals and navigation stacks handle the back press first (a modal closes, a stack pops); only when none of them takes it do `BackHandler`'s listeners run.
| Member | Description |
|---|---|
| `addEventListener("hardwareBackPress", listener)` | `listener` returns `true` when it handled the press. The most recently added listener runs first. If no listener handles it, the app exits. Returns a subscription to `remove()`. |
| `exitApp()` | Leaves the app. |
```tsx
import { cleanup, signal } from "nsh";
import { BackHandler } from "nsh/android";
import { Alert } from "nsh/ui/mobile";
function Editor() {
const dirty = signal(false);
const sub = BackHandler.addEventListener("hardwareBackPress", () => {
if (!dirty()) return false; // let the app go back or exit
Alert.alert("Discard changes?", undefined, [{ text: "Keep" }, { text: "Discard", style: "destructive" }]).then(
(i) => i === 1 && BackHandler.exitApp(),
);
return true;
});
cleanup(() => sub.remove());
// ...
}
```
> [!NOTE]
> The listener runs after the system has delivered the press: the main thread never waits for JS. While at least one listener is registered, NSH takes back presses that modals and stacks leave, and exits the app itself when no listener returns `true`.
## setStatusBarBackgroundColor
```ts
function setStatusBarBackgroundColor(backgroundColor: string, animated?: boolean | null): void;
```
Sets the status bar's color, below what rendered `` components set (the last mounted `StatusBar` wins over this base value). It has an effect on Android versions before edge-to-edge became mandatory (API 35); from API 35 the status bar is transparent and your content draws behind it.
```ts
setStatusBarBackgroundColor("#1e293b", true);
```
See [`StatusBar`](https://nsh.dev/components/status-bar.md) for the component.
Source: https://nsh.dev/modules/nsh-android
---
# nsh/desktop
`nsh/desktop` holds what only desktops have: the window and its title bar, the application menu and context menus. It works on macOS, Windows and Linux. The module can be imported on every platform, so one bundle runs everywhere: on iOS and Android its functions do nothing, and `isAvailable` tells you whether you are on a desktop.
```ts
import { isAvailable, Menu, titleBar, Window } from "nsh/desktop";
```
## isAvailable
```ts
const isAvailable: boolean;
```
`true` on macOS, Windows and Linux; `false` elsewhere.
## Window
```ts
const Window: {
setTitle(title: string): void;
setTitleBar(style: "default" | "hidden", height?: number): void;
minimize(): void;
toggleMaximize(): void;
toggleFullScreen(): void;
close(): void;
};
```
| Member | Description |
|---|---|
| `setTitle(title)` | The window's title: the title bar, the taskbar or Dock, the window switcher. |
| `setTitleBar(style, height?)` | `"hidden"` removes the system's title bar: the app fills the window up to its top edge and draws a title bar of its own. The system's window controls stay (macOS's traffic lights, Windows' minimize / maximize / close buttons, GTK's window buttons). `height` is your bar's height in points: the controls are centered in it (Windows' caption buttons fill it); without it they take the platform's height. `"default"` brings the system's title bar back. |
| `minimize()` | Minimizes the window. |
| `toggleMaximize()` | Maximizes the window, or restores it when it is maximized. |
| `toggleFullScreen()` | Enters or leaves full screen. |
| `close()` | Closes the window; the app ends with it. |
## A title bar of your own
With the title bar hidden, the app draws its own bar and says which parts of it move the window with `desktop={{ windowDrag: true }}`:
```tsx
import { Input, Text, View } from "nsh";
import { titleBar, Window } from "nsh/desktop";
Window.setTitleBar("hidden", 40); // a bar 40 points tall
function TitleBar() {
return (
My App
);
}
```
Pressing a view with `windowDrag` drags the window as the system's title bar would: a double click maximizes it (or does what the user set: on macOS, "Double-click a window's title bar to"), and on Windows and Linux a right click opens the window menu.
- The nearest view that sets `windowDrag` decides: `desktop={{ windowDrag: false }}` keeps a part of a draggable view clickable.
- Text fields and views with press or touch handlers (`onPress`, `onTouch*`, responders) inside a draggable view stay clickable without it: the search field and any buttons in the example above work as usual.
- `windowDrag` also works with the system's title bar: a sidebar or toolbar can move the window too.
### titleBar
```ts
const titleBar: Accessor<{ height: number; left: number; right: number }>;
```
The hidden title bar's area, in points (reactive): its `height`, and the widths the system's window controls take at its `left` and `right` ends. Lay your bar out with it so that nothing goes under the controls. All three are 0 while the system shows its title bar (and in full screen, and off desktops).
| | `height` | `left` | `right` |
|---|---|---|---|
| macOS | `height`, else the title bar's (28 or 32, by macOS version) | The traffic lights' width | 0 |
| Windows | `height`, else 32 | 0 | The three caption buttons (138) |
| Linux | `height`, else the header bar's | The window buttons at the start (the desktop's button layout) | The window buttons at the end |
On Windows the caption buttons are drawn by NSH in Windows 11's style and follow the app's appearance; hovering the maximize button shows Windows 11's snap layouts. The window keeps its resize borders, shadow and rounded corners on every platform.
## Menu
```ts
const Menu: {
setApplicationMenu(items: MenuItem[] | null): void;
popup(items: MenuItem[], at?: { x: number; y: number }): Promise;
};
```
| Member | Description |
|---|---|
| `setApplicationMenu(items)` | Sets the application menu: one submenu per top-level item (`File`, `Edit`, ...). On macOS it is the menu bar, whose first menu is the app menu (AppKit titles it with the app's name). On Windows and Linux it is the window's menu bar, which is not shown while the title bar is hidden; its shortcuts still work, and a title bar of the app's own can open its menus with `Menu.popup`. `null` restores the platform's default menu. |
| `popup(items, at?)` | Shows a context menu with its top left corner at `at` (the window's coordinates, in points), or at the pointer. The promise resolves once the menu closes, whether an item was chosen or not: a button that opened it can stay pressed until then. |
Call `setApplicationMenu` again to change the menu. Inside an `effect` it follows the signals it reads: a check mark or an enabled state that depends on app state stays current.
### Menu items
```ts
type MenuItem =
| { type: "separator" }
| {
label?: string;
role?: MenuRole;
shortcut?: string;
enabled?: boolean;
checked?: boolean;
onSelect?: () => void;
submenu?: MenuItem[];
};
```
| Field | Description |
|---|---|
| `label` | What the item shows. A role's item has a label of its own without it. |
| `onSelect` | Called when the item is chosen, by a click or its shortcut. |
| `shortcut` | Its keyboard shortcut: modifiers and one key joined by `+`, such as `"mod+n"`, `"mod+shift+z"`, `"alt+f4"` or `"f11"`. The modifiers are in the table below. Keys are letters, digits and punctuation, written as the character the key types on a US keyboard without Shift (`,`, `/`; `"shift+/"` for `?`), or a name: `plus` (the `=` / `+` key, also `=`), `minus` (also `-`), `space`, `enter` (`return`), `escape` (`esc`), `tab`, `backspace`, `delete` (`del`), `insert`, the arrows `up`, `down`, `left`, `right` (`arrowup`, …), `home`, `end`, `pageup`, `pagedown`, `f1`–`f24`. Case does not matter (`"Mod+Shift+N"` is `"mod+shift+n"`; a capital letter is the same key, not Shift), and lowercase is the recommended style. An invalid shortcut (`"mod-n"`, `"mod++"`, `"mod+?"`) throws an error that gives the right spelling. `nsh/keymap` takes the same keystrokes. Menus show it in the platform's own style (`⇧⌘N` on macOS, `Ctrl+Shift+N` on Windows). |
| `enabled` | `false` grays the item out (default `true`). |
| `checked` | Set (to `true` or `false`) for an item with a check mark. |
| `submenu` | Makes the item a submenu (it needs a `label`). |
| `role` | A standard item the platform carries out (below). |
`null`, `undefined` and `false` items are left out, so `cond && { … }` works in a list.
Modifiers:
| Modifier | macOS | Windows | Linux |
|---|---|---|---|
| `mod` (also `cmdorctrl`) | Command (⌘) | Ctrl | Ctrl |
| `cmd` (also `command`, `meta`, `super`, `win`) | Command (⌘) | Windows key | Super |
| `ctrl` (also `control`) | Control (⌃) | Ctrl | Ctrl |
| `alt` (also `option`, `opt`) | Option (⌥) | Alt | Alt |
| `shift` | Shift (⇧) | Shift | Shift |
Use `mod` for an app's own shortcuts: it is the key each platform's apps use for them (⌘N on macOS, Ctrl+N on Windows and Linux), so one spelling fits everywhere. The Windows key and Super mostly belong to the system (Win+E, the GNOME overview), so `cmd` is rarely right outside macOS.
Roles:
| Role | Does | Platforms |
|---|---|---|
| `about`, `hide`, `hideOthers`, `showAll` | The app menu's standard items | macOS (left out elsewhere) |
| `quit` | Quits the app | All |
| `undo`, `redo`, `cut`, `copy`, `paste`, `selectAll` | Edit the focused text field (its own shortcuts stay its own) | All |
| `minimize`, `zoom`, `toggleFullScreen`, `close` | The window | All |
A role's item has the platform's label and shortcut unless it sets its own (macOS adds the app's name to `about`, `hide` and `quit`).
Menu shortcuts come first, as in native apps: a key an item takes never reaches the focused text field or NSH's own keys. Only a binding of [`nsh/keymap`](./nsh-keymap) of the same keystroke comes before them. So give the editing shortcuts (`mod+c`, `mod+v`, `mod+x`, `mod+a`, `mod+z`) only to the edit roles, which pass them to the text field, and avoid keys without a modifier (`escape`, the arrows, letters), which would take them from text fields and from back navigation.
```tsx
import { effect, signal } from "nsh";
import { Platform } from "nsh/device";
import { Menu } from "nsh/desktop";
const mac = Platform.OS === "macos";
const wrap = signal(true);
effect(() => {
Menu.setApplicationMenu([
mac && {
label: "My App",
submenu: [{ role: "about" }, { type: "separator" }, { role: "hide" }, { role: "hideOthers" }, { type: "separator" }, { role: "quit" }],
},
{
label: "File",
submenu: [
{ label: "New", shortcut: "mod+n", onSelect: () => createDocument() },
{ label: "Open…", shortcut: "mod+o", onSelect: () => openDocument() },
!mac && { type: "separator" },
!mac && { role: "quit", label: "Exit" },
],
},
{
label: "Edit",
submenu: [{ role: "undo" }, { role: "redo" }, { type: "separator" }, { role: "cut" }, { role: "copy" }, { role: "paste" }],
},
{
label: "View",
submenu: [
{ label: "Word Wrap", checked: wrap(), onSelect: () => wrap.update((w) => !w) },
{ role: "toggleFullScreen" },
],
},
]);
});
```
A context menu, from a button's press:
```tsx
Menu.popup([
{ label: "Rename", onSelect: rename },
{ label: "Duplicate", onSelect: duplicate },
{ type: "separator" },
{ label: "Delete", onSelect: remove },
])
}
>
More
```
> [!NOTE]
> Menus are the system's (NSMenu, Win32 menus, GTK popover menus): they look and behave like every other app's, follow the system's (or the app's) appearance and are keyboard accessible. `examples/desktop` is an app with a title bar of its own, an application menu and context menus.
Source: https://nsh.dev/modules/nsh-desktop
---
# nsh/webview
`nsh/webview` exports `WebView`: a web page shown by the platform's own browser engine (`WKWebView` on iOS and Mac Catalyst, `android.webkit.WebView` on Android). It is a native view laid out like any other, so it scrolls, clips and overlaps other views correctly.
```ts
import { WebView, type WebViewNavigation, type WebViewProps } from "nsh/webview";
```
See [Web view and video](https://nsh.dev/docs/web-view-and-video.md) for the guide and [``](https://nsh.dev/elements/web-view.md) for its props.
## WebView
```ts
const WebView: (props: WebViewProps) => YNode;
interface WebViewProps extends Omit, WebViewHostProps {
source: string | { uri: string } | { html: string; baseUrl?: string };
ios?: IOSWebViewHostProps;
desktop?: DesktopWebViewHostProps;
}
```
```tsx
import { mount, signal, Text, View, type YNode } from "nsh";
import { WebView, type WebViewNavigation } from "nsh/webview";
function Browser() {
const nav = signal(null);
let web!: YNode;
return (
web.goBack()} style={{ opacity: nav()?.canGoBack ? 1 : 0.4 }}>Back web.reload()}>Reload{nav()?.title ?? ""} nav.set(n)} />
);
}
mount(Browser);
```
## Props
`WebView` takes the [view props](https://nsh.dev/elements/view.md) (style, classes, layout, accessibility, `ref`; no children), plus:
| Prop | Type | Default | Description |
|---|---|---|---|
| `source` | `string \| { uri: string } \| { html: string; baseUrl?: string }` | (required) | A URL, `{ uri }`, or inline HTML with an optional base URL for its relative links. |
| `injectedJavaScript` | `string` | | Runs after each page loads. |
| `injectedJavaScriptBeforeContentLoaded` | `string` | | Runs before each page's own scripts. |
| `userAgent` | `string` | platform's | The user agent string. |
| `scrollEnabled` | `boolean` | `true` | The user can scroll the page. |
| `javaScriptEnabled` | `boolean` | `true` | Pages run scripts. |
| `ios` | `{ allowsBackForwardNavigationGestures?: boolean }` | | iOS only: swipe from the edges to go back and forward. |
| `desktop` | `{ allowsBackForwardNavigationGestures?: boolean }` | | macOS: two-finger trackpad swipes go back and forward; Windows: touchscreen swipes; Linux: touchpad swipes. |
## Events
| Event | Payload | When |
|---|---|---|
| `onLoadStart` | `WebViewNavigation` | A page starts loading. |
| `onLoad` | `WebViewNavigation` | A page finished loading. |
| `onError` | `{ message: string }` | A page failed to load. |
| `onNavigationStateChange` | `WebViewNavigation` | The URL, title, loading state or history changed. |
| `onMessage` | `{ data: string }` | The page called `window.ReactNativeWebView.postMessage(data)`. |
```ts
interface WebViewNavigation {
url: string;
title: string;
loading: boolean;
canGoBack: boolean;
canGoForward: boolean;
}
```
## Ref methods
| Method | Description |
|---|---|
| `reload()` | Reloads the page. |
| `goBack()` | Goes back in its history. |
| `goForward()` | Goes forward in its history. |
| `stopLoading()` | Stops loading. |
| `injectJavaScript(script)` | Runs `script` in the page. |
| `postMessage(data)` | Sends `data` (a string) to the page: a `message` event on `window`, with `data` as `event.data`. |
## Talking to the page
The page sends strings to the app with `window.ReactNativeWebView.postMessage(…)`; the app sends strings to the page with `ref.postMessage(…)`. Send JSON for structured data.
```tsx
const page = `
Post to the app
`;
let web!: YNode;
const message = signal("");
message.set(e.data)} />
web.postMessage("hi from the app")}>Post to the page
```
## Capabilities
When your app declares `capabilities` in `nsh.json`, a web view may only show and navigate to the hosts listed under `webview`; other top-level navigations are refused natively. An app without a `capabilities` key may show anything. See [Sandbox and capabilities](https://nsh.dev/docs/sandbox-and-capabilities.md).
```json
{
"capabilities": {
"webview": { "hosts": ["example.com", "*.example.com"] }
}
}
```
Source: https://nsh.dev/modules/nsh-webview
---
# nsh/video
`nsh/video` exports `Video`: a video player built on the platform's player (`AVPlayer` on iOS and Mac Catalyst, `MediaPlayer` on Android). Playback is controlled with props, so pausing is a signal away.
```ts
import { Video, type VideoProps } from "nsh/video";
```
See [Web view and video](https://nsh.dev/docs/web-view-and-video.md) for the guide and [``](https://nsh.dev/elements/video.md) for its props.
## Video
```ts
const Video: (props: VideoProps) => YNode;
interface VideoProps extends Omit, VideoHostProps {
source: string | { uri: string };
}
```
```tsx
import { mount, signal, Text, View, type YNode } from "nsh";
import { Video } from "nsh/video";
function Player() {
const paused = signal(true);
const time = signal("");
let video!: YNode;
return (
time.set(`${e.currentTime.toFixed(1)} / ${e.duration.toFixed(1)} s`)}
/>
paused.update((p) => !p)}>{paused() ? "Play" : "Pause"} video.seek(0)}>Restart{time()}
);
}
mount(Player);
```
## Props
`Video` takes the [view props](https://nsh.dev/elements/view.md) (style, classes, layout, accessibility, `ref`; no children), plus:
| Prop | Type | Default | Description |
|---|---|---|---|
| `source` | `string \| { uri: string }` | (required) | The video's URL. |
| `paused` | `boolean` | `false` | Playback is paused. |
| `muted` | `boolean` | `false` | No sound. |
| `volume` | `number` | `1` | 0..1. |
| `rate` | `number` | `1` | The playback speed. |
| `repeat` | `boolean` | `false` | Plays again from the start at the end. |
| `controls` | `boolean` | `false` | Shows the platform's playback controls. |
| `resizeMode` | `"cover" \| "contain" \| "stretch" \| "center"` | | How the video fits its box. |
| `progressUpdateInterval` | `number` | `250` | Milliseconds between `onProgress` events. |
## Events
| Event | Payload | When |
|---|---|---|
| `onLoadStart` | | The video starts loading. |
| `onLoad` | `{ duration: number; width: number; height: number }` | It is ready to play: its length in seconds and its size. |
| `onProgress` | `{ currentTime: number; duration: number }` | While playing, every `progressUpdateInterval` ms. Seconds. |
| `onBuffer` | `{ isBuffering: boolean }` | Playback waits for data, or resumes. |
| `onEnd` | | Playback reached the end. |
| `onError` | `{ message: string }` | The video could not be loaded or played. |
## Ref methods
| Method | Description |
|---|---|
| `seek(seconds)` | Jumps to `seconds`. |
```ts
const aspect = signal(16 / 9);
aspect.set(e.width / e.height)} style={{ width: "100%", aspectRatio: aspect() }} />;
```
## Capabilities
When your app declares `capabilities` in `nsh.json`, remote video sources must be allowed under `network`, like `fetch`; a refused source shows nothing. See [Sandbox and capabilities](https://nsh.dev/docs/sandbox-and-capabilities.md).
Source: https://nsh.dev/modules/nsh-video
---
# Global APIs
Besides the ECMAScript standard library, an NSH app's JS environment has a set of standard globals: `fetch` with `Headers`, `Request` and `Response`; the Streams Standard; `URL` and `URLSearchParams`; `TextEncoder` / `TextDecoder`; `Blob`, `File` and `FormData`; `EventTarget`, `Event` and `AbortController`; timers, `requestAnimationFrame` and `console`; and `Intl` for number and date formatting. They are implemented natively in Rust, follow the WHATWG standards, and need no import, so the many JavaScript libraries built on these standards (HTTP clients, URL and encoding helpers, stream utilities) work unchanged.
This page lists every global an app has. UI is built with [elements](https://nsh.dev/elements.md) and [components](https://nsh.dev/components.md), and everything else an app uses comes from the [modules](https://nsh.dev/modules.md) it imports.
The TypeScript declarations of all of these are generated into `.nsh/types/nsh/globals.d.ts` (see [Project structure](https://nsh.dev/docs/project-structure.md)).
| Group | Globals |
|---|---|
| [Console and timing](#console-and-timing) | `console`, `performance` |
| [Timers and frames](#timers-and-frames) | `setTimeout`, `setInterval`, `clearTimeout`, `clearInterval`, `queueMicrotask`, `requestAnimationFrame`, `cancelAnimationFrame` |
| [Base64 and errors](#base64-and-errors) | `atob`, `btoa`, `DOMException` |
| [Events and abort signals](#events-and-abort-signals) | `EventTarget`, `Event`, `CustomEvent`, `AbortController`, `AbortSignal` |
| [URLs](#urls) | `URL`, `URLSearchParams` |
| [Text encoding](#text-encoding) | `TextEncoder`, `TextDecoder` |
| [Blobs and form data](#blobs-and-form-data) | `Blob`, `File`, `FormData` |
| [Fetch](#fetch) | `fetch`, `Headers`, `Request`, `Response` |
| [Number and date formatting](#number-and-date-formatting) | `Intl.NumberFormat`, `Intl.DateTimeFormat`, `toLocaleString` |
| [Streams](#streams) | `ReadableStream`, `WritableStream`, `TransformStream`, readers, writers, controllers, `ByteLengthQueuingStrategy`, `CountQueuingStrategy`, `TextEncoderStream`, `TextDecoderStream` |
## Console and timing
```ts
declare var console: {
log(...data: unknown[]): void;
info(...data: unknown[]): void;
debug(...data: unknown[]): void;
warn(...data: unknown[]): void;
error(...data: unknown[]): void;
};
declare var performance: {
/** Milliseconds since the JS context started, with sub-millisecond precision. */
now(): number;
};
```
`console` writes one line per call to the runtime's log (target `js`): `log` and `info` at info level, `debug`, `warn` and `error` at their levels. Arguments are joined with spaces; strings print verbatim, errors with their stack, functions as `[Function name]`, and objects and arrays as JSON (falling back to `String(value)` for cycles).
While a device is connected to the dev server, its log lines appear in the terminal running `nsh run` / `nsh dev`, prefixed with the device name; see [Dev server and debugging](https://nsh.dev/docs/dev-server-and-debugging.md).
> [!NOTE]
> Only the five methods above exist. There is no `console.table`, `console.group`, `console.time` or `console.assert`, and no `%s` / `%o` format substitutions.
`performance.now()` is the clock `requestAnimationFrame` passes to its callbacks. `performance` has no other members.
## Timers and frames
```ts
function setTimeout(handler: (...args: any[]) => void, timeout?: number, ...args: any[]): number;
function setInterval(handler: (...args: any[]) => void, timeout?: number, ...args: any[]): number;
function clearTimeout(id: number | undefined): void;
function clearInterval(id: number | undefined): void;
function queueMicrotask(callback: () => void): void;
/** Runs `callback` at the next display frame with its time in milliseconds. */
function requestAnimationFrame(callback: (time: number) => void): number;
function cancelAnimationFrame(id: number): void;
```
Timers behave as in browsers: a missing, negative or non-finite delay is 0, the returned id is a positive integer, and clearing an unknown id does nothing. `handler` must be a function (string handlers are not evaluated). Timers do not clamp nested delays to 4 ms.
> [!WARNING]
> The extra `...args` of `setTimeout` / `setInterval` are declared but currently not passed to the handler. Use a closure instead: `setTimeout(() => save(id), 100)`.
`requestAnimationFrame` callbacks run once, at the next frame the engine produces, all with the same `performance.now()` time. A callback that requests another frame is called at the following frame, so the usual loop works:
```ts
let start = 0;
function tick(time: number) {
if (!start) start = time;
progress.set(Math.min(1, (time - start) / 300));
if (time - start < 300) requestAnimationFrame(tick);
}
requestAnimationFrame(tick);
```
> [!TIP]
> Driving an animation from JS frame by frame runs your code on every frame in QuickJS. For animations of styles and transforms, prefer [nsh/animated](https://nsh.dev/modules/nsh-animated.md), whose drivers run natively without calling back into JS (see [Animation](https://nsh.dev/docs/animation.md)).
## Base64 and errors
```ts
function atob(data: string): string;
function btoa(data: string): string;
class DOMException extends Error {
constructor(message?: string, name?: string);
readonly name: string;
readonly message: string;
readonly code: number;
}
```
`atob` / `btoa` convert between binary strings and Base64 as in browsers. `DOMException` is what aborted operations reject with: an `AbortController.abort()` without a reason gives a `DOMException` named `"AbortError"`, and `AbortSignal.timeout` one named `"TimeoutError"`.
## Events and abort signals
```ts
class EventTarget {
constructor();
addEventListener(type: string, listener: EventListenerOrEventListenerObject | null,
options?: AddEventListenerOptions | boolean): void;
removeEventListener(type: string, listener: EventListenerOrEventListenerObject | null,
options?: { capture?: boolean } | boolean): void;
dispatchEvent(event: Event): boolean;
}
interface AddEventListenerOptions {
capture?: boolean;
once?: boolean;
passive?: boolean;
signal?: AbortSignal;
}
class Event {
constructor(type: string, init?: { bubbles?: boolean; cancelable?: boolean; composed?: boolean });
readonly type: string;
readonly target: EventTarget | null;
readonly currentTarget: EventTarget | null;
readonly eventPhase: number; // NONE 0, CAPTURING_PHASE 1, AT_TARGET 2, BUBBLING_PHASE 3
readonly bubbles: boolean;
readonly cancelable: boolean;
readonly defaultPrevented: boolean;
readonly composed: boolean;
readonly isTrusted: boolean;
readonly timeStamp: number;
returnValue: boolean;
cancelBubble: boolean;
readonly srcElement: EventTarget | null; // legacy: `target`
initEvent(type: string, bubbles?: boolean, cancelable?: boolean): void; // legacy
composedPath(): EventTarget[];
preventDefault(): void;
stopPropagation(): void;
stopImmediatePropagation(): void;
}
class CustomEvent extends Event {
constructor(type: string, init?: EventInit & { detail?: T });
readonly detail: T;
}
class AbortController {
readonly signal: AbortSignal;
abort(reason?: any): void;
}
class AbortSignal extends EventTarget {
readonly aborted: boolean;
readonly reason: any;
onabort: ((event: Event) => void) | null;
throwIfAborted(): void;
static abort(reason?: any): AbortSignal;
static timeout(milliseconds: number): AbortSignal;
static any(signals: Iterable): AbortSignal;
}
```
These are the DOM Standard's classes with one difference: there is no tree, so an event is dispatched to its target only. Capture listeners run first, then the others, as the DOM does at the target; `bubbles` has no effect. The classes can be subclassed, which makes `EventTarget` a small event emitter:
```ts
class Channel extends EventTarget {
send(message: string) {
this.dispatchEvent(new CustomEvent("message", { detail: message }));
}
}
const channel = new Channel();
channel.addEventListener("message", (e) => console.log((e as CustomEvent).detail));
channel.send("hello");
```
These events are unrelated to the events of elements (`onPress`, `onChangeText`, ...), which are props; see [Touch and gestures](https://nsh.dev/docs/touch-and-gestures.md).
`AbortSignal` cancels `fetch` requests and stream pipes, and removes listeners added with `{ signal }`:
```ts
const controller = new AbortController();
const signal = AbortSignal.any([controller.signal, AbortSignal.timeout(10_000)]);
fetch("https://api.example.com/search?q=nsh", { signal }).catch((e) => {
if (e.name === "AbortError" || e.name === "TimeoutError") return;
throw e;
});
// later, for example when the screen closes:
controller.abort();
```
## URLs
```ts
class URL {
constructor(url: string | URL, base?: string | URL);
static canParse(url: string | URL, base?: string | URL): boolean;
static parse(url: string | URL, base?: string | URL): URL | null;
/** A `blob:` URL `fetch` reads `blob` from, until revoked. */
static createObjectURL(blob: Blob): string;
static revokeObjectURL(url: string): void;
href: string;
readonly origin: string;
protocol: string;
username: string;
password: string;
host: string;
hostname: string;
port: string;
pathname: string;
search: string;
hash: string;
readonly searchParams: URLSearchParams;
toString(): string;
toJSON(): string;
}
class URLSearchParams {
constructor(init?: string | Record | Iterable<[string, string]> | URLSearchParams);
readonly size: number;
append(name: string, value: string): void;
delete(name: string, value?: string): void;
get(name: string): string | null;
getAll(name: string): string[];
has(name: string, value?: string): boolean;
set(name: string, value: string): void;
sort(): void;
forEach(callback: (value: string, key: string, parent: URLSearchParams) => void, thisArg?: unknown): void;
entries(): IterableIterator<[string, string]>;
keys(): IterableIterator;
values(): IterableIterator;
[Symbol.iterator](): IterableIterator<[string, string]>;
toString(): string;
}
```
URLs are parsed per the URL Standard. A URL's `searchParams` is live in both directions: changing the parameters rewrites the query, and setting `search` or `href` parses it again.
```ts
const url = new URL("/v1/items", "https://api.example.com");
url.searchParams.set("page", "2");
url.searchParams.append("tag", "new");
url.href; // "https://api.example.com/v1/items?page=2&tag=new"
```
The iterators of `URLSearchParams` (and of `Headers` and `FormData`) iterate over a snapshot taken when the iterator was created, and have the iterator helpers (`map`, `filter`, `toArray`, ...).
`URL.createObjectURL(blob)` returns a `blob:` URL that `fetch` (with `GET`) can read until `revokeObjectURL` is called. Such URLs are understood by `fetch` only, not by `` or other elements.
## Text encoding
```ts
class TextEncoder {
readonly encoding: "utf-8";
encode(input?: string): Uint8Array;
encodeInto(source: string, destination: Uint8Array): { read: number; written: number };
}
/** UTF-8, UTF-16LE / BE and windows-1252 (with the labels of latin1 and ASCII). */
class TextDecoder {
constructor(label?: string, options?: { fatal?: boolean; ignoreBOM?: boolean });
readonly encoding: string;
readonly fatal: boolean;
readonly ignoreBOM: boolean;
decode(input?: BufferSource, options?: { stream?: boolean }): string;
}
```
`TextDecoder` knows fewer encodings than a browser: UTF-8, UTF-16LE, UTF-16BE and windows-1252 (which the labels `latin1`, `iso-8859-1`, `ascii` and the like also name, as the Encoding Standard specifies). Other labels throw a `RangeError`. With `{ stream: true }`, a multi-byte sequence cut between two chunks is kept for the next call.
## Blobs and form data
```ts
class Blob {
constructor(blobParts?: Iterable, options?: { type?: string; endings?: "transparent" | "native" });
readonly size: number;
readonly type: string;
slice(start?: number, end?: number, contentType?: string): Blob;
arrayBuffer(): Promise;
bytes(): Promise;
text(): Promise;
stream(): ReadableStream;
}
class File extends Blob {
constructor(fileBits: Iterable, fileName: string,
options?: { type?: string; endings?: "transparent" | "native"; lastModified?: number });
readonly name: string;
readonly lastModified: number;
readonly webkitRelativePath: string;
}
class FormData {
constructor();
append(name: string, value: string | Blob, fileName?: string): void;
delete(name: string): void;
get(name: string): File | string | null;
getAll(name: string): (File | string)[];
has(name: string): boolean;
set(name: string, value: string | Blob, fileName?: string): void;
forEach(callback: (value: File | string, key: string, parent: FormData) => void, thisArg?: unknown): void;
entries(): IterableIterator<[string, File | string]>;
keys(): IterableIterator;
values(): IterableIterator;
[Symbol.iterator](): IterableIterator<[string, File | string]>;
}
type BlobPart = ArrayBuffer | ArrayBufferView | Blob | string;
```
A blob holds its bytes in memory, and `slice` shares them instead of copying. Blobs are in-memory values only: there is no file system access behind `File`, and `FormData` has no constructor from a form element.
A `FormData` body is sent as `multipart/form-data`, and `Response.formData()` / `Request.formData()` parse both `multipart/form-data` and `application/x-www-form-urlencoded`.
```ts
const form = new FormData();
form.append("title", "Holiday");
form.append("photo", new Blob([bytes], { type: "image/jpeg" }), "photo.jpg");
await fetch("https://api.example.com/upload", { method: "POST", body: form });
```
## Fetch
```ts
/**
* HTTP through the platform's network stack (NSURLSession,
* HttpURLConnection), streaming both ways; `data:` and `blob:` URLs are
* answered locally.
*/
function fetch(input: string | URL | Request, init?: RequestInit): Promise;
interface RequestInit {
method?: string;
headers?: HeadersInit;
/** A ReadableStream is sent as it is read (chunked), with `duplex: "half"`. */
body?: BodyInit | null;
duplex?: "half";
signal?: AbortSignal | null;
/** "manual" gives the 3xx response itself. */
redirect?: "follow" | "manual" | "error";
credentials?: "omit" | "same-origin" | "include";
cache?: "default" | "no-store" | "reload" | "no-cache" | "force-cache" | "only-if-cached";
mode?: "same-origin" | "no-cors" | "cors";
referrer?: string;
referrerPolicy?: string;
integrity?: string;
keepalive?: boolean;
priority?: "high" | "low" | "auto";
}
type HeadersInit = Headers | Record | Iterable<[string, string]>;
type BodyInit = ReadableStream | Blob | BufferSource | FormData | URLSearchParams | string;
```
`fetch` sends HTTP and HTTPS requests through the platform's own network stack: `NSURLSession` on iOS and Mac Catalyst, `HttpURLConnection` on Android. So the system's proxy settings, TLS configuration and certificate trust apply. `data:` URLs and `blob:` URLs (from `URL.createObjectURL`) are answered locally without touching the network; any other scheme rejects.
```ts
type Todo = { id: number; title: string; done: boolean };
async function loadTodos(): Promise {
const res = await fetch("https://api.example.com/todos", {
headers: { Authorization: `Bearer ${token}` },
});
if (!res.ok) throw new Error(`HTTP ${res.status}`);
return res.json();
}
```
### Headers, Request and Response
```ts
class Headers {
constructor(init?: HeadersInit);
append(name: string, value: string): void;
delete(name: string): void;
get(name: string): string | null;
getSetCookie(): string[];
has(name: string): boolean;
set(name: string, value: string): void;
forEach(callback: (value: string, key: string, parent: Headers) => void, thisArg?: unknown): void;
entries(): IterableIterator<[string, string]>;
keys(): IterableIterator;
values(): IterableIterator;
[Symbol.iterator](): IterableIterator<[string, string]>;
}
interface Body {
/** The body as it arrives (a byte stream), or null. */
readonly body: ReadableStream | null;
readonly bodyUsed: boolean;
arrayBuffer(): Promise;
blob(): Promise;
bytes(): Promise;
formData(): Promise;
json(): Promise;
text(): Promise;
}
class Request implements Body {
constructor(input: string | URL | Request, init?: RequestInit);
readonly method: string;
readonly url: string;
readonly headers: Headers;
readonly destination: string;
readonly referrer: string;
readonly referrerPolicy: string;
readonly mode: string;
readonly credentials: string;
readonly cache: string;
readonly redirect: "follow" | "manual" | "error";
readonly integrity: string;
readonly keepalive: boolean;
readonly isReloadNavigation: boolean;
readonly isHistoryNavigation: boolean;
readonly signal: AbortSignal;
readonly duplex: "half";
clone(): Request;
// ...and the Body members
}
class Response implements Body {
constructor(body?: BodyInit | null, init?: { status?: number; statusText?: string; headers?: HeadersInit });
static json(data: unknown, init?: ResponseInit): Response;
static error(): Response;
static redirect(url: string | URL, status?: number): Response;
readonly type: "basic" | "default" | "error";
readonly url: string;
readonly redirected: boolean;
readonly status: number;
readonly ok: boolean;
readonly statusText: string;
readonly headers: Headers;
clone(): Response;
// ...and the Body members
}
```
### Streaming
A response's `body` is a byte stream filled as the network delivers data, so large or long-lived responses (downloads, server-sent events, streamed model output) can be processed while they arrive. When more than 1 MiB is waiting unread, the request is paused until the app reads again.
```ts
const res = await fetch("https://api.example.com/events");
const lines = res.body!.pipeThrough(new TextDecoderStream());
for await (const chunk of lines) {
console.log(chunk);
}
```
A `ReadableStream` request body is uploaded chunk by chunk as the network asks for it; pass `duplex: "half"` as the Fetch Standard requires:
```ts
await fetch("https://api.example.com/upload", {
method: "PUT",
body: blob.stream(),
duplex: "half",
});
```
### Differences from browsers
- **No CORS and no origin.** An app is not a web page: it may talk to any host its [capabilities](https://nsh.dev/docs/sandbox-and-capabilities.md) allow. `mode` is accepted and ignored.
- **No forbidden headers.** An app may set `Cookie`, `User-Agent`, `Origin` and any other header. When no `Accept` header is given, `Accept: */*` is sent.
- **`redirect: "manual"` delivers the 3xx response itself**, with its status and `Location` header, instead of an opaque filtered response. `"follow"` follows redirects (each hop is checked against the capabilities); `"error"` rejects when the response is a redirect.
- **A `FormData` body always gets `Content-Type: multipart/form-data; boundary=…`**, even when a `Content-Type` header was set. HTTP libraries often set a default content type and clear it only where they detect a browser; without the boundary the server cannot parse the body.
- **`credentials`, `cache`, `referrer`, `referrerPolicy`, `integrity`, `keepalive` and `priority`** are validated and kept on the `Request` object, but are not passed to the network stack: caching and cookie handling are those of the platform's stack.
- **Capabilities.** When the app declares `network` capabilities in `nsh.json` (or the embedding app grants fewer), a request to a host that is not allowed never reaches the network and `fetch` rejects with a `TypeError`. See [Sandbox and capabilities](https://nsh.dev/docs/sandbox-and-capabilities.md).
- **Errors.** Network failures reject with a `TypeError` whose message starts with `Network request failed:`; aborts reject with the signal's `reason`.
See [Networking](https://nsh.dev/docs/networking.md) for a guide to loading data in components.
## Streams
The full WHATWG Streams Standard: readable streams (default and byte streams, BYOB readers, `tee`, `pipeTo` / `pipeThrough`, `ReadableStream.from`, async iteration), writable streams, transform streams, the two queuing strategies and the text encoding streams.
```ts
class ReadableStream {
constructor(underlyingSource: UnderlyingByteSource, strategy?: { highWaterMark?: number });
constructor(underlyingSource?: UnderlyingDefaultSource, strategy?: QueuingStrategy);
static from(iterable: Iterable> | AsyncIterable): ReadableStream;
readonly locked: boolean;
cancel(reason?: any): Promise;
getReader(options: { mode: "byob" }): ReadableStreamBYOBReader;
getReader(): ReadableStreamDefaultReader;
pipeThrough(transform: { writable: WritableStream; readable: ReadableStream },
options?: StreamPipeOptions): ReadableStream;
pipeTo(destination: WritableStream, options?: StreamPipeOptions): Promise;
tee(): [ReadableStream, ReadableStream];
values(options?: { preventCancel?: boolean }): AsyncIterableIterator;
[Symbol.asyncIterator](options?: { preventCancel?: boolean }): AsyncIterableIterator;
}
class ReadableStreamDefaultReader {
constructor(stream: ReadableStream);
readonly closed: Promise;
read(): Promise<{ done: false; value: R } | { done: true; value?: R }>;
releaseLock(): void;
cancel(reason?: any): Promise;
}
class ReadableStreamBYOBReader {
constructor(stream: ReadableStream);
readonly closed: Promise;
read(view: T, options?: { min?: number }): Promise>;
releaseLock(): void;
cancel(reason?: any): Promise;
}
class ReadableStreamDefaultController {
readonly desiredSize: number | null;
close(): void;
enqueue(chunk?: R): void;
error(e?: any): void;
}
class ReadableByteStreamController {
readonly byobRequest: ReadableStreamBYOBRequest | null;
readonly desiredSize: number | null;
close(): void;
enqueue(chunk: ArrayBufferView): void;
error(e?: any): void;
}
class ReadableStreamBYOBRequest {
readonly view: ArrayBufferView | null;
respond(bytesWritten: number): void;
respondWithNewView(view: ArrayBufferView): void;
}
class WritableStream {
constructor(underlyingSink?: UnderlyingSink, strategy?: QueuingStrategy);
readonly locked: boolean;
abort(reason?: any): Promise;
close(): Promise;
getWriter(): WritableStreamDefaultWriter;
}
class WritableStreamDefaultWriter {
constructor(stream: WritableStream);
readonly closed: Promise;
readonly desiredSize: number | null;
readonly ready: Promise;
abort(reason?: any): Promise;
close(): Promise;
releaseLock(): void;
write(chunk?: W): Promise;
}
class WritableStreamDefaultController {
readonly signal: AbortSignal;
error(e?: any): void;
}
class TransformStream {
constructor(transformer?: Transformer, writableStrategy?: QueuingStrategy,
readableStrategy?: QueuingStrategy);
readonly readable: ReadableStream;
readonly writable: WritableStream;
}
class TransformStreamDefaultController {
readonly desiredSize: number | null;
enqueue(chunk?: O): void;
error(reason?: any): void;
terminate(): void;
}
class ByteLengthQueuingStrategy {
constructor(init: { highWaterMark: number });
readonly highWaterMark: number;
readonly size: (chunk: ArrayBufferView) => number;
}
class CountQueuingStrategy {
constructor(init: { highWaterMark: number });
readonly highWaterMark: number;
readonly size: (chunk: any) => 1;
}
class TextEncoderStream {
readonly encoding: "utf-8";
readonly readable: ReadableStream;
readonly writable: WritableStream;
}
class TextDecoderStream {
constructor(label?: string, options?: { fatal?: boolean; ignoreBOM?: boolean });
readonly encoding: string;
readonly fatal: boolean;
readonly ignoreBOM: boolean;
readonly readable: ReadableStream;
readonly writable: WritableStream;
}
```
The underlying source, sink and transformer objects (`UnderlyingDefaultSource`, `UnderlyingByteSource`, `UnderlyingSink`, `Transformer`) and `StreamPipeOptions` (`preventAbort`, `preventCancel`, `preventClose`, `signal`) have the standard shapes; their full declarations are in `globals.d.ts`.
`TextEncoderStream` keeps a surrogate pair cut between two string chunks together, and `TextDecoderStream` a multi-byte sequence cut between two byte chunks.
A transform stream that splits text into lines:
```ts
function lines(): TransformStream {
let rest = "";
return new TransformStream({
transform(chunk, controller) {
const parts = (rest + chunk).split("\n");
rest = parts.pop()!;
for (const line of parts) controller.enqueue(line);
},
flush(controller) {
if (rest) controller.enqueue(rest);
},
});
}
const res = await fetch("https://api.example.com/log");
for await (const line of res.body!.pipeThrough(new TextDecoderStream()).pipeThrough(lines())) {
console.log(line);
}
```
## Number and date formatting
QuickJS has no `Intl` of its own; NSH adds one, formatting in Rust: `Intl.NumberFormat`, `Intl.DateTimeFormat` and `Intl.getCanonicalLocales`, and the `toLocaleString` of numbers and BigInts and `toLocaleString` / `toLocaleDateString` / `toLocaleTimeString` of dates, which take the same `locales` and `options`. Their declarations are TypeScript's own (the `ES2023` library), so they are not in `globals.d.ts`.
```ts
(1234567.891).toLocaleString(); // "1,234,567.891" (en-US)
(1234.5).toLocaleString("de-DE", { style: "currency", currency: "EUR" }); // "1.234,50 €"
new Intl.NumberFormat("en", { notation: "compact" }).format(1_500_000); // "1.5M"
new Intl.NumberFormat("en", { style: "percent", signDisplay: "exceptZero" }).format(0.042); // "+4%"
new Date().toLocaleTimeString([], { hour: "2-digit", minute: "2-digit", hour12: false }); // "09:05"
new Intl.DateTimeFormat("en-US", { dateStyle: "medium", timeStyle: "short" }).format(date); // "Oct 16, 2026, 3:04 PM"
```
Without `locales`, the device's locale is used: its region format (iOS `NSLocale.currentLocale`, Android `Locale.getDefault()`), `en-US` in `nsh trace` and on the web.
- **Numbers**: the `decimal`, `percent`, `currency` (`currencyDisplay` `symbol`, `narrowSymbol`, `code`) and `unit` (common units: lengths, masses, durations, data sizes, temperatures, speeds) styles; `minimumIntegerDigits`, `minimum` / `maximumFractionDigits`, `minimum` / `maximumSignificantDigits`, `roundingPriority`, `trailingZeroDisplay`, `useGrouping`, `signDisplay`; `standard`, `scientific`, `engineering` and `compact` notation (`compactDisplay` `short` / `long`). Rounding is half away from zero, on the number as written (`1.005` rounds to `1.01`, as in browsers). `format` also takes BigInts and decimal strings, exactly. `formatToParts` and `resolvedOptions` work as in browsers.
- **Dates**: `weekday`, `year`, `month` (`numeric`, `2-digit`, `short`, `long`, `narrow`), `day`, `hour`, `minute`, `second`, `fractionalSecondDigits`, `hour12` / `hourCycle`, `timeZoneName` and `dateStyle` / `timeStyle`, in the Gregorian calendar. `timeZone` takes the device's zone, `UTC` or an offset (`"+08:00"`); other zone names throw a `RangeError` (there is no time zone database).
- **Locales**: the locale data is a table of common languages (English, German, French, Spanish, Italian, Portuguese, Dutch, Russian, Swedish, Polish, Turkish, Indonesian, Vietnamese, Thai, Hindi, Chinese, Japanese, Korean) with their regional variants (`en-GB`, `en-IN`, `de-CH`, `es-MX`, `pt-PT`, `zh-TW`, ...), not the whole of CLDR, so details can differ from a browser's output. A locale without data falls back to the next one asked for, then the device's, then `en-US`; Russian, Swedish, Polish, Turkish, Indonesian, Vietnamese, Thai and Hindi dates have no month names (their months are written as numbers, their weekdays in English). Digits are always Latin.
- **Not there**: `Intl.Collator` (`String.prototype.localeCompare` is QuickJS's, which ignores the locale), `PluralRules`, `RelativeTimeFormat`, `ListFormat`, `DisplayNames`, `Segmenter` and `Intl.Locale`; `currencySign: "accounting"` and `currencyDisplay: "name"` format as `standard` and `code`.
## Performance
All of these classes are native: parsing URLs, encoding and decoding text, building multipart bodies and moving stream chunks happen in Rust, not in QuickJS. Still, every call from JS crosses into the runtime, so prefer whole-buffer operations (`res.json()`, `res.text()`, `encoder.encode(s)`) over byte-by-byte loops in JS. See [Performance](https://nsh.dev/docs/performance.md).
Source: https://nsh.dev/modules/globals
---
# Style reference
This page lists every key of the `style` prop. The keys and their types come from the generated declarations in `.nsh/types/nsh/types.d.ts`, so your editor checks them as you type. Layout is flexbox with a column default, numbers are points, and the CSS features are there too: grid, gradients, box shadows, filters, blend modes, outlines, transitions and keyframe animations.
For how to apply styles, combine them with classes and inherit text styles, see [Styling](https://nsh.dev/docs/styling.md). For the class syntax, see the [Tailwind reference](https://nsh.dev/modules/tailwind-reference.md).
## The four interfaces
Each interface extends the one before it:
| Interface | Extends | Used by |
|---|---|---|
| `LayoutStyle` | | Layout of every element (flexbox, grid, sizes, spacing, position) |
| `ViewStyle` | `LayoutStyle` | ``, ``, ``, controls and every other element |
| `TextStyle` | `ViewStyle` | `` and `` (and views, whose text styles pass to the text inside them) |
| `ImageStyle` | `ViewStyle` | `` |
```tsx
import type { ViewStyle, TextStyle } from "nsh";
import { Text, View } from "nsh";
const card: ViewStyle = {
padding: 16,
borderRadius: 12,
backgroundColor: "#fff",
boxShadow: "0 4px 12px rgb(0 0 0 / 0.08)",
};
const title: TextStyle = { fontSize: 17, fontWeight: "600", color: "#111" };
Hello;
```
`style` also takes an array; entries merge left to right and falsy entries are skipped, so conditional styles need no spreading:
```tsx
```
> [!NOTE]
> The inline `style` always wins over `class`. Setting a key to `null` or `undefined` resets it to its default.
## Value types
| Type | Accepts |
|---|---|
| `number` | Points (density-independent). `1` is one point on iOS and one dp on Android. |
| `DimensionValue` | `number` (points), `` `${number}%` `` (of the parent), or `"auto"`. |
| `LengthValue` | `number` (points) or `` `${number}%` ``. No `"auto"`. |
| `ColorValue` | A CSS color string: `#rgb`, `#rgba`, `#rrggbb`, `#rrggbbaa`, `rgb()`, `rgba()`, `hsl()`, `hsla()`, `oklch()`, `oklab()`, or a named CSS color (`black`, `white`, `transparent`, `rebeccapurple`, ...). |
| `NativeNode` | A native animated value or a node derived from it (see [nsh/animated](https://nsh.dev/modules/nsh-animated.md)). Accepted by `backgroundColor`, `opacity` and the steps of `transform`; the value then animates on the UI thread without JS. |
Strings with CSS syntax (`boxShadow`, `backgroundImage`, `filter`, `gridTemplateColumns`, ...) take lengths in `px` and bare numbers (both points), `pt`, or `rem` (16 points).
## LayoutStyle
Layout is computed in Rust (flexbox and CSS grid). The defaults differ from CSS's: `display: "flex"`, `flexDirection: "column"`, `flexShrink: 0`, `alignContent: "flex-start"`, `alignItems: "stretch"`, and no automatic minimum size. See [Layout](https://nsh.dev/docs/layout.md).
### Display and position
| Key | Type | Description |
|---|---|---|
| `display` | `"flex" \| "grid" \| "block" \| "none"` | The layout of the children. `"none"` removes the element from layout and hides it. `"block"` stacks children as CSS block layout does. Default `"flex"`. |
| `position` | `"relative" \| "absolute"` | `"absolute"` takes the element out of the flow and places it by `top` / `right` / `bottom` / `left` relative to its parent. Default `"relative"`. |
| `top` | `DimensionValue` | Offset from the parent's top edge (absolute) or from the element's own position (relative). |
| `right` | `DimensionValue` | Offset from the right edge. In right-to-left layouts it means the end edge. |
| `bottom` | `DimensionValue` | Offset from the bottom edge. |
| `left` | `DimensionValue` | Offset from the left edge. In right-to-left layouts it means the start edge. |
| `start` | `DimensionValue` | Left in left-to-right layouts, right in right-to-left ones. |
| `end` | `DimensionValue` | Right in left-to-right layouts, left in right-to-left ones. |
| `zIndex` | `number` | Order among siblings (stable for equal values); also the native stacking order. |
| `direction` | `"inherit" \| "ltr" \| "rtl"` | Layout direction, inherited by default. Right to left, `left` and `right` properties mean start and end. |
### Flexbox
| Key | Type | Description |
|---|---|---|
| `flexDirection` | `"row" \| "column" \| "row-reverse" \| "column-reverse"` | The main axis. Default `"column"`. |
| `flexWrap` | `"wrap" \| "nowrap" \| "wrap-reverse"` | Whether children wrap onto several lines. Default `"nowrap"`. |
| `flex` | `number` | One number, not the CSS shorthand: a positive number sets `flexGrow` to it, `flexShrink: 1` and `flexBasis: 0`; `0` makes the element inflexible; a negative number shrinks it down to its minimum size. |
| `flexGrow` | `number` | How much of the free space the element takes. Default `0`. |
| `flexShrink` | `number` | How much the element shrinks when space runs out. Default `0`, not `1` as in CSS. |
| `flexBasis` | `DimensionValue` | The main-axis size before free space is distributed. |
| `justifyContent` | `"flex-start" \| "flex-end" \| "center" \| "space-between" \| "space-around" \| "space-evenly" \| "stretch"` | Distribution of children along the main axis. Default `"flex-start"`. |
| `alignItems` | `"flex-start" \| "flex-end" \| "center" \| "stretch" \| "baseline"` | Alignment of children on the cross axis. Default `"stretch"`. |
| `alignSelf` | `"auto" \| "flex-start" \| "flex-end" \| "center" \| "stretch" \| "baseline"` | Overrides the parent's `alignItems` for this element. |
| `alignContent` | `"flex-start" \| "flex-end" \| "center" \| "stretch" \| "space-between" \| "space-around" \| "space-evenly"` | Distribution of wrapped lines (and grid tracks) on the cross axis. Default `"flex-start"`. |
| `gap` | `LengthValue` | Space between rows and columns of children. |
| `rowGap` | `LengthValue` | Space between rows. |
| `columnGap` | `LengthValue` | Space between columns. |
### Grid
Set `display: "grid"` to lay out children in a CSS grid. Track lists use CSS syntax.
| Key | Type | Description |
|---|---|---|
| `gridTemplateColumns` | `string` | Column tracks: `"repeat(3, 1fr)"`, `"200px 1fr"`, `minmax()`, `auto-fill` / `auto-fit`, line names. |
| `gridTemplateRows` | `string` | Row tracks, same syntax. |
| `gridAutoColumns` | `string` | Size of implicitly created columns. |
| `gridAutoRows` | `string` | Size of implicitly created rows. |
| `gridAutoFlow` | `"row" \| "column" \| "dense" \| "row dense" \| "column dense"` | How items without a placement fill the grid. |
| `gridColumn` | `number \| string` | Column placement: `"1 / 3"`, `"span 2"`, a line number or a line name. |
| `gridColumnStart` | `number \| string` | Start column line. |
| `gridColumnEnd` | `number \| string` | End column line. |
| `gridRow` | `number \| string` | Row placement, same syntax as `gridColumn`. |
| `gridRowStart` | `number \| string` | Start row line. |
| `gridRowEnd` | `number \| string` | End row line. |
| `justifyItems` | `"flex-start" \| "flex-end" \| "center" \| "stretch" \| "baseline"` | How grid items sit in their areas along the row. |
| `justifySelf` | `"auto" \| "flex-start" \| "flex-end" \| "center" \| "stretch" \| "baseline"` | Overrides `justifyItems` for this item. |
```tsx
```
### Size
| Key | Type | Description |
|---|---|---|
| `width` | `DimensionValue` | Width. |
| `height` | `DimensionValue` | Height. |
| `minWidth` | `LengthValue` | Minimum width. |
| `minHeight` | `LengthValue` | Minimum height. |
| `maxWidth` | `LengthValue` | Maximum width. |
| `maxHeight` | `LengthValue` | Maximum height. |
| `aspectRatio` | `number` | Width divided by height; the missing dimension is computed from the other one. |
### Margin
Margins accept `"auto"` (absorbs free space, as in flexbox) and negative values.
| Key | Type | Description |
|---|---|---|
| `margin` | `DimensionValue` | All four sides. |
| `marginHorizontal` | `DimensionValue` | Left and right. |
| `marginVertical` | `DimensionValue` | Top and bottom. |
| `marginTop` | `DimensionValue` | Top. |
| `marginRight` | `DimensionValue` | Right. |
| `marginBottom` | `DimensionValue` | Bottom. |
| `marginLeft` | `DimensionValue` | Left. |
| `marginStart` | `DimensionValue` | Left in left-to-right layouts, right in right-to-left ones. |
| `marginEnd` | `DimensionValue` | The opposite of `marginStart`. |
### Padding
| Key | Type | Description |
|---|---|---|
| `padding` | `LengthValue` | All four sides. |
| `paddingHorizontal` | `LengthValue` | Left and right. |
| `paddingVertical` | `LengthValue` | Top and bottom. |
| `paddingTop` | `LengthValue` | Top. |
| `paddingRight` | `LengthValue` | Right. |
| `paddingBottom` | `LengthValue` | Bottom. |
| `paddingLeft` | `LengthValue` | Left. |
| `paddingStart` | `LengthValue` | Left in left-to-right layouts, right in right-to-left ones. |
| `paddingEnd` | `LengthValue` | The opposite of `paddingStart`. |
## ViewStyle
Everything drawn by a view. All keys of `LayoutStyle` apply too.
### Background
| Key | Type | Description |
|---|---|---|
| `backgroundColor` | `ColorValue \| NativeNode` | The fill color. Takes a native animated color. |
| `backgroundImage` | `string` | CSS gradients drawn over the background color: `linear-gradient(...)`, `radial-gradient(...)`, `conic-gradient(...)`, several separated by commas. |
| `experimental_backgroundImage` | `string` | Alias of `backgroundImage`. |
```tsx
```
### Borders
| Key | Type | Description |
|---|---|---|
| `borderWidth` | `number` | Width of all four sides. |
| `borderTopWidth` | `number` | Top side. |
| `borderRightWidth` | `number` | Right side. |
| `borderBottomWidth` | `number` | Bottom side. |
| `borderLeftWidth` | `number` | Left side. |
| `borderStartWidth` | `number` | Start side (left in left-to-right layouts). |
| `borderEndWidth` | `number` | End side. |
| `borderColor` | `ColorValue` | Color of all four sides. |
| `borderTopColor` | `ColorValue` | Top side. |
| `borderRightColor` | `ColorValue` | Right side. |
| `borderBottomColor` | `ColorValue` | Bottom side. |
| `borderLeftColor` | `ColorValue` | Left side. |
| `borderStartColor` | `ColorValue` | Start side. |
| `borderEndColor` | `ColorValue` | End side. |
| `borderStyle` | `"solid" \| "dashed" \| "dotted"` | Line style. Default `"solid"`. |
### Corners
| Key | Type | Description |
|---|---|---|
| `borderRadius` | `number` | Radius of all four corners. |
| `borderTopLeftRadius` | `number` | Top-left corner. |
| `borderTopRightRadius` | `number` | Top-right corner. |
| `borderBottomRightRadius` | `number` | Bottom-right corner. |
| `borderBottomLeftRadius` | `number` | Bottom-left corner. |
| `borderTopStartRadius` | `number` | Top corner on the start side. |
| `borderTopEndRadius` | `number` | Top corner on the end side. |
| `borderBottomStartRadius` | `number` | Bottom corner on the start side. |
| `borderBottomEndRadius` | `number` | Bottom corner on the end side. |
| `borderCurve` | `"circular" \| "continuous"` | `"continuous"`: corners whose curvature eases into the edges, like iOS app icons. Where the box is too small the curve falls back to circular, so pills stay pills. Works on both platforms. |
### Outline
An outline is drawn outside the border and takes no space in layout.
| Key | Type | Description |
|---|---|---|
| `outlineWidth` | `number` | Line width. |
| `outlineColor` | `ColorValue` | Line color. |
| `outlineStyle` | `"solid" \| "double" \| "dashed" \| "dotted" \| "none" \| "hidden"` | Line style. |
| `outlineOffset` | `number` | Distance from the border edge; negative values move it inside. |
### Shadows
Two ways to cast a shadow: the CSS `boxShadow` (several layers, spread, inset) or the four `shadow*` keys.
| Key | Type | Description |
|---|---|---|
| `boxShadow` | `string` | CSS `box-shadow`: offsets, blur, spread and color, several layers separated by commas, `inset` for inner shadows. |
| `shadowColor` | `ColorValue` | Shadow color. |
| `shadowOffset` | `{ width: number; height: number }` | Offset. Default `{ width: 0, height: -3 }`. |
| `shadowOpacity` | `number` | Opacity, 0 to 1. Default `0` (no shadow). |
| `shadowRadius` | `number` | Blur radius. Default `3`. |
```tsx
```
> [!NOTE]
> On Android, shadows need a background: a view with a transparent background (text on nothing, for example) casts no shadow. For Android's material shadow use the `android={{ elevation }}` prop instead of a style.
### Opacity, visibility and overflow
| Key | Type | Description |
|---|---|---|
| `opacity` | `number \| NativeNode` | 0 (invisible) to 1. Takes a native animated value. |
| `visibility` | `"visible" \| "hidden" \| "collapse"` | Hidden views keep their space in layout (unlike `display: "none"`). |
| `overflow` | `"visible" \| "hidden"` | `"hidden"` clips children to the view's bounds (and corners). |
| `backfaceVisibility` | `"visible" \| "hidden"` | `"hidden"`: invisible while a 3D rotation turns it away. |
### Transforms
| Key | Type | Description |
|---|---|---|
| `transform` | `TransformStep[]` | A list of steps, applied right to left like CSS. |
| `transformOrigin` | `string \| (number \| string)[]` | The point `transform` acts around: keywords, percentages and lengths (`"top left"`, `"50% 0"`, `[10, 20]`). Default the center. |
A `TransformStep` is an object with one key:
| Step | Value | Native animated |
|---|---|---|
| `translateX`, `translateY` | `number` (points) | yes |
| `scale`, `scaleX`, `scaleY` | `number` (factor) | yes |
| `rotate`, `rotateZ`, `rotateX`, `rotateY` | `number` (degrees) or `string` (`"45deg"`, `"0.5rad"`) | yes |
| `skewX`, `skewY` | `number` (degrees) or `string` | no |
| `perspective` | `number` | no |
```tsx
```
> [!NOTE]
> Android has no skew: `skewX` and `skewY` are ignored there.
### Filters and blending
| Key | Type | Description |
|---|---|---|
| `filter` | `string` | CSS filters of the view and its content: `brightness()`, `contrast()`, `grayscale()`, `hue-rotate()`, `invert()`, `saturate()`, `sepia()`, `opacity()`, `blur()`, `drop-shadow()`. |
| `backdropFilter` | `string` | CSS filters of what is behind the view, such as `"blur(10px)"` for frosted glass. |
| `mixBlendMode` | `"normal" \| "multiply" \| "screen" \| "overlay" \| "darken" \| "lighten" \| "color-dodge" \| "color-burn" \| "hard-light" \| "soft-light" \| "difference" \| "exclusion" \| "hue" \| "saturation" \| "color" \| "luminosity" \| "plus-darker" \| "plus-lighter"` | How the view blends with what is behind it. |
> [!WARNING]
> Platform limits:
> - iOS applies only `opacity()`, `brightness()` up to 1 and `drop-shadow()` to views. `` takes every color filter and `blur()`, applied while the image is decoded.
> - iOS `backdropFilter` blur uses a system material at a fraction, an approximation of the exact radius.
> - Android needs API 31 for `blur()`, `drop-shadow()` and `backdropFilter`; below that only color filters apply. Blend modes need API 29.
### Transitions
When a transitioned property changes (from a new style, a class or a variant such as `active:`), the platform animates it natively. No JS runs during the animation.
| Key | Type | Description |
|---|---|---|
| `transitionProperty` | `string` | Comma-separated properties that animate: `"all"`, `"colors"` (`color`, `backgroundColor`, `borderColor`, ...), `"opacity"`, `"transform"`, `"boxShadow"`, `"borderRadius"`, `"borderWidth"`, or `"none"`. |
| `transitionDuration` | `number \| "${number}ms" \| "${number}s"` | Milliseconds as a number, or `"150ms"` / `"0.2s"`. |
| `transitionDelay` | `number \| "${number}ms" \| "${number}s"` | Delay before the transition starts. |
| `transitionTimingFunction` | `"linear" \| "ease" \| "ease-in" \| "ease-out" \| "ease-in-out" \| "cubic-bezier(...)"` | The easing curve. |
```tsx
open.set(!open())}
style={{ height: open() ? 160 : 56, opacity: open() ? 1 : 0.8,
transitionProperty: "opacity, transform", transitionDuration: 200 }}
/>
```
### Keyframe animations
Built-in keyframe animations, run natively (Tailwind's `animate-*`).
| Key | Type | Description |
|---|---|---|
| `animationName` | `"none" \| "spin" \| "ping" \| "pulse" \| "bounce"` | The animation. |
| `animationDuration` | `number \| "${number}ms" \| "${number}s"` | Length of one iteration. |
| `animationDelay` | `number \| "${number}ms" \| "${number}s"` | Delay before the first iteration. |
| `animationTimingFunction` | `"linear" \| "ease" \| "ease-in" \| "ease-out" \| "ease-in-out" \| "cubic-bezier(...)"` | The easing curve. |
| `animationIterationCount` | `number \| "infinite"` | How many times it runs. |
For animations driven by values, gestures or springs, use [nsh/animated](https://nsh.dev/modules/nsh-animated.md) instead.
### Pointer
| Key | Type | Description |
|---|---|---|
| `cursor` | `"auto" \| "default" \| "pointer" \| "text" \| "grab" \| "grabbing" \| "not-allowed" \| ...` (every CSS cursor keyword, and `"none"`) | The pointer shape over the view, for pointers (iPad trackpads, Mac Catalyst, desktops). |
## TextStyle
Text styles apply to `` and ``. They are also inherited, as in CSS: text properties set on a view pass to the texts and inputs inside it, and nested `` spans inherit from their parent text. All keys of `ViewStyle` apply too. See [Text](https://nsh.dev/docs/text.md).
### Font
| Key | Type | Description |
|---|---|---|
| `color` | `ColorValue` | Text color. |
| `fontSize` | `number` | Size in points. |
| `fontWeight` | `"normal" \| "bold" \| number \| "${number}"` | `"normal"` (400), `"bold"` (700) or 100–900. |
| `fontFamily` | `string` | A system family, or the name an imported font evaluates to (`import Inter from "./Inter.ttf"`). See [Fonts](https://nsh.dev/docs/fonts.md). |
| `fontStyle` | `"normal" \| "italic"` | Italic. |
| `fontVariant` | `("small-caps" \| "oldstyle-nums" \| "lining-nums" \| "tabular-nums" \| "proportional-nums")[]` | OpenType number and letter forms. `tabular-nums` keeps digits the same width, useful for counters and timers. |
### Spacing and alignment
| Key | Type | Description |
|---|---|---|
| `lineHeight` | `number` | Line height in points. |
| `letterSpacing` | `number` | Extra space between characters, in points. |
| `textAlign` | `"auto" \| "left" \| "center" \| "right" \| "justify"` | Horizontal alignment. |
| `textAlignVertical` | `"auto" \| "top" \| "center" \| "bottom"` | Where text shorter than its box sits (a `` with a height). |
| `writingDirection` | `"auto" \| "ltr" \| "rtl"` | Base direction of the text; `"auto"` takes it from the content. |
| `textTransform` | `"none" \| "uppercase" \| "lowercase" \| "capitalize"` | Case transformation. |
> [!TIP]
> Tailwind's `leading-*` and `tracking-*` classes set line heights as multiples of the font size and letter spacing in `em`, so they scale with `text-*` sizes.
### Decoration
| Key | Type | Description |
|---|---|---|
| `textDecorationLine` | `"none" \| "underline" \| "line-through" \| "underline line-through"` | Lines drawn with the text. |
| `textDecorationColor` | `ColorValue` | Color of the lines (default the text color). |
| `textDecorationStyle` | `"solid" \| "double" \| "dotted" \| "dashed" \| "wavy"` | Line style. |
| `textDecorationThickness` | `number \| "auto" \| "from-font"` | Line thickness. |
| `textUnderlineOffset` | `number \| "auto"` | Distance of the underline from the baseline. |
### Text shadow
| Key | Type | Description |
|---|---|---|
| `textShadowColor` | `ColorValue` | Shadow color. |
| `textShadowOffset` | `{ width: number; height: number }` | Shadow offset. |
| `textShadowRadius` | `number` | Blur radius. |
> [!NOTE]
> iOS draws no gradients (`backgroundImage`) or inset box shadows on ``.
## ImageStyle
All keys of `ViewStyle`, plus:
| Key | Type | Description |
|---|---|---|
| `tintColor` | `ColorValue` | Recolors a template image (every opaque pixel takes this color), for monochrome icons. |
How an image fills its box is the `resizeMode` prop of ``, not a style; see [image](https://nsh.dev/elements/image.md).
## Props that look like styles
Some settings are props in NSH, not style keys. Their Tailwind classes exist, but an explicit prop wins over the class:
| Setting | Prop | Class |
|---|---|---|
| Line limit | `numberOfLines` on `` | `line-clamp-*`, `truncate` |
| Ellipsis position | `ellipsizeMode` on `` | `text-ellipsis`, `text-clip` |
| Touch pass-through | `pointerEvents` | `pointer-events-none`, `pointer-events-auto` |
| Image fit | `resizeMode` on `` | `object-cover`, `object-contain`, ... |
| Selectable text | `selectable` on `` | `select-text`, `select-none` |
| Placeholder color | `placeholderTextColor` on `` | `placeholder:text-*` |
| Material shadow (Android) | `android={{ elevation }}` | |
## Related
- [Styling](https://nsh.dev/docs/styling.md): `style`, `class`, inheritance and precedence.
- [Tailwind reference](https://nsh.dev/modules/tailwind-reference.md): every supported utility and variant.
- [Layout](https://nsh.dev/docs/layout.md): flexbox and grid in practice.
- [view](https://nsh.dev/elements/view.md), [text](https://nsh.dev/elements/text.md), [image](https://nsh.dev/elements/image.md): the elements these styles apply to.
Source: https://nsh.dev/modules/style-reference
---
# Tailwind reference
Every element takes Tailwind CSS classes in its `class` prop (and conditional ones in `classList`). NSH resolves them in Rust against Tailwind 4's default theme: JS passes only the string, each distinct string is parsed once and cached, and variants such as `dark:` or `active:` are re-evaluated natively when the appearance, window size or press state changes, without running any JS.
This page lists what is supported. For an introduction see [Tailwind](https://nsh.dev/docs/tailwind.md); for the style keys the utilities set, see the [Style reference](https://nsh.dev/modules/style-reference.md).
```tsx
Inbox 0, "text-zinc-400": unread() === 0 }}>{unread()}
```
## How classes combine
- **Later wins.** Among utilities of the same variant level, the one later in the string wins: `"p-4 p-2"` is `p-2`. A component can take a `class` prop and append it to its own classes without `tailwind-merge`.
- **Variants apply after plain utilities**, in Tailwind's variant order: position (`first:` ...), `group-*:`, `focus-within:`, `hover:`, `focus:`, `active:`, `enabled:`, `disabled:`, motion, `max-*:` breakpoints, then breakpoints ascending, orientation, direction, `dark:`, then platform. So `"active:bg-red-500 bg-blue-500"` is red while pressed.
- **`style` wins over classes.** The inline `style` is applied on top of the resolved classes. An important utility (`!p-4` or `p-4!`) wins over the inline style too.
- **Text styles inherit.** Text utilities on a view (`text-white`, `font-bold`, `text-center`, ...) pass to the `` and `` elements inside it, as in CSS.
- **Display is CSS's.** `flex` and `inline-flex` make a row unless a direction utility says otherwise, wherever it is in the string (`flex-col flex` is a column). Views without `flex` keep NSH's column default.
## Units and the theme
Lengths are points (`1` = one point on iOS, one dp on Android). Tailwind's `1rem` is 16 points, so the theme matches the web at the same density: one spacing step is 4 points (`p-4` = 16 points), `text-base` is 16 points.
### Spacing values
Utilities that take a spacing value (padding, margin, gap, inset, width, height, `basis`, `translate`, `leading`) accept:
| Value | Example | Result |
|---|---|---|
| Steps (quarter steps allowed) | `p-4`, `gap-1.5`, `m-0.25` | step × 4 points |
| `px` | `p-px`, `-mt-px` | 1 point |
| `0` | `m-0` | 0 |
| Fraction | `w-1/2`, `left-1/3` | percentage of the parent |
| `full` | `w-full`, `inset-full` | 100% |
| `auto` (margin, inset, width, height, basis) | `mx-auto`, `w-auto` | `"auto"` |
| Arbitrary | `p-[13px]`, `w-[42%]`, `mt-[1.5rem]` | the given length |
Margins, insets, `translate`, `rotate`, `skew`, `scale`, `z`, `hue-rotate`, `tracking`, `underline-offset`, `outline-offset`, grid lines and gradient angles take a leading `-` for negative values: `-mt-2`, `-translate-x-1/2`, `-rotate-6`.
### Colors
The full Tailwind 4 palette is built in: `red`, `orange`, `amber`, `yellow`, `lime`, `green`, `emerald`, `teal`, `cyan`, `sky`, `blue`, `indigo`, `violet`, `purple`, `fuchsia`, `pink`, `rose`, `slate`, `gray`, `zinc`, `neutral`, `stone`, each in shades `50`, `100`–`900`, `950`; plus `black`, `white` and `transparent`. Your own colors come from `nsh.json` (see [Theme customization](#theme-customization)).
Every color utility takes:
| Form | Example |
|---|---|
| A theme color | `bg-sky-500`, `text-brand` |
| An opacity modifier (percent) | `bg-black/50`, `ring-black/5` |
| An arbitrary opacity | `text-black/[0.35]` |
| An arbitrary color | `bg-[#0ea5e9]`, `border-[rgb(0_0_0_/_0.1)]`, `text-[oklch(70%_0.2_50)]` |
| An arbitrary color with a modifier | `bg-[rgb(0_0_255)]/25` |
`currentColor` and `inherit` are not color values; rings default to the element's text color when no ring color is given.
## Arbitrary values
Square brackets pass a value the theme does not have: `w-[37px]`, `grid-cols-[200px_1fr]`, `bg-[linear-gradient(45deg,red,blue)]`.
- `_` stands for a space (`grid-cols-[1fr_2fr]`); write `\_` for a literal underscore.
- Lengths: a bare number or `px` (points), `pt`, `rem` (16 points), and `%` where percentages make sense.
- Angles (`rotate-[...]`, `hue-rotate-[...]`, `bg-linear-[...]`): `deg`, `rad`, `grad`, `turn`.
- Times (`duration-[...]`, `delay-[...]`): `ms` or `s`; a bare number is milliseconds.
- Type hints settle ambiguous values: `text-[length:22px]` is a size, `text-[color:#333]` a color; `border-[length:3px]` likewise.
Arbitrary CSS properties (`[mask-type:luminance]`) are not supported.
## Layout
| Class | Sets |
|---|---|
| `flex`, `inline-flex` | `display: "flex"`, and a row unless a direction utility is present |
| `grid`, `inline-grid` | `display: "grid"` |
| `block`, `flow-root` | `display: "block"` (block layout) |
| `inline`, `inline-block` | `display: "flex"` (there is no inline formatting context: they lay out as views) |
| `hidden` | `display: "none"` |
| `relative`, `static`, `sticky` | `position: "relative"` (`sticky` does not stick) |
| `absolute`, `fixed` | `position: "absolute"` (`fixed` is relative to the parent, not the screen) |
| `inset-`, `inset-x-`, `inset-y-` | `top` / `right` / `bottom` / `left` |
| `top-`, `right-`, `bottom-`, `left-` | the matching offset |
| `start-`, `end-` | `start` / `end` (direction-aware) |
| `z-`, `z-auto`, `z-[]` | `zIndex` (`z-auto` is 0) |
| `visible` | `visibility: "visible"` |
| `invisible`, `collapse` | `visibility: "hidden"` (keeps its space) |
| `overflow-hidden`, `overflow-clip`, `overflow-x-hidden`, `overflow-y-hidden` | `overflow: "hidden"` |
| `overflow-visible` | `overflow: "visible"` |
| `aspect-square`, `aspect-video`, `aspect-/`, `aspect-[/]`, `aspect-auto` | `aspectRatio` (1, 16/9, a/b, none) |
Scrolling is an element, not a class: `overflow-auto` and `overflow-scroll` are not supported; use [scroll](https://nsh.dev/elements/scroll.md).
## Flexbox and grid
| Class | Sets |
|---|---|
| `flex-row`, `flex-row-reverse`, `flex-col`, `flex-col-reverse` | `flexDirection` |
| `flex-wrap`, `flex-wrap-reverse`, `flex-nowrap` | `flexWrap` |
| `flex-1` | grow 1, shrink 1, basis 0 |
| `flex-` | grow n, shrink 1, basis 0 |
| `flex-auto` | grow 1, shrink 1, basis auto |
| `flex-initial` | grow 0, shrink 1, basis auto |
| `flex-none` | grow 0, shrink 0, basis auto |
| `flex-[]`, `flex-[__]` | the CSS shorthand: `flex-[2]`, `flex-[3_0_50%]` |
| `grow`, `grow-`, `grow-[]` | `flexGrow` (bare: 1) |
| `shrink`, `shrink-`, `shrink-[]` | `flexShrink` (bare: 1) |
| `basis-` | `flexBasis` (spacing, fraction, `full`, `auto`, arbitrary) |
| `justify-start`, `justify-end`, `justify-center`, `justify-between`, `justify-around`, `justify-evenly`, `justify-stretch`, `justify-normal` | `justifyContent` |
| `items-start`, `items-end`, `items-center`, `items-baseline`, `items-stretch` | `alignItems` |
| `self-auto`, `self-start`, `self-end`, `self-center`, `self-stretch`, `self-baseline` | `alignSelf` |
| `content-start`, `content-end`, `content-center`, `content-between`, `content-around`, `content-evenly`, `content-stretch`, `content-normal` | `alignContent` |
| `justify-items-start`, `-end`, `-center`, `-stretch`, `-normal` | `justifyItems` |
| `justify-self-auto`, `-start`, `-end`, `-center`, `-stretch` | `justifySelf` |
| `place-content-center`, `-start`, `-end`, `-between`, `-stretch` | `alignContent` and `justifyContent` |
| `place-items-center`, `-start`, `-end`, `-stretch` | `alignItems` and `justifyItems` |
| `place-self-auto`, `-center`, `-start`, `-end`, `-stretch` | `alignSelf` and `justifySelf` |
| `gap-`, `gap-x-`, `gap-y-` | `gap`, `columnGap`, `rowGap` |
| `space-x-`, `space-y-` | `columnGap`, `rowGap` (a gap, not margins on the children) |
| `grid-cols-` | `gridTemplateColumns: "repeat(n, minmax(0, 1fr))"` |
| `grid-cols-[]`, `grid-cols-none` | `gridTemplateColumns` (`grid-cols-[200px_1fr]`) |
| `grid-rows-`, `grid-rows-[]`, `grid-rows-none` | `gridTemplateRows` |
| `col-span-`, `col-span-full`, `col-auto`, `col-`, `col-[]` | `gridColumn` |
| `col-start-`, `col-end-` (and `-auto`) | `gridColumnStart`, `gridColumnEnd` |
| `row-span-`, `row-span-full`, `row-auto`, `row-`, `row-[]` | `gridRow` |
| `row-start-`, `row-end-` (and `-auto`) | `gridRowStart`, `gridRowEnd` |
| `grid-flow-row`, `grid-flow-col`, `grid-flow-dense`, `grid-flow-row-dense`, `grid-flow-col-dense` | `gridAutoFlow` |
| `auto-cols-auto`, `-min`, `-max`, `-fr`, `auto-cols-[