Documentation
Premiers pas
Ce guide crée une application NSH, l’exécute sur les plateformes ciblées et la publie. Les applications NSH s’écrivent en Rust ou en TSX, sur le même moteur : Rust compile votre application en code natif sans machine virtuelle JavaScript, et TSX s’exécute sur QuickJS avec des mises à jour à chaud, sous forme de modules qu’une application native peut charger à l’exécution. Le guide suit d’abord une application Rust, puis la même application en TSX. Il suppose que nsh est installé et présent dans votre PATH (Installation) ; dans un checkout, utilisez ./target/debug/nsh à la place.
import { mount, signal, Text, View } from "nsh";
import { button, buttonText, desktop, focusRing, hoverFill, ui } from "./palette";
// The app `nsh init` creates, with a reset button and a short history.
function App() {
const count = signal(0);
const history = signal<number[]>([]);
const change = (delta: number) => {
const next = count() + delta;
count.set(next);
history.update((h) => [next, ...h].slice(0, 4));
};
return (
<View style={{ flex: 1, backgroundColor: ui().page, padding: 20, paddingTop: 24, alignItems: "center", justifyContent: "center", gap: 16 }}>
<Text style={{ fontSize: 15, color: ui().muted }}>{desktop ? "You clicked" : "You tapped"}</Text>
<Text style={{ fontSize: 56, fontWeight: "700", color: count() < 0 ? ui().down : ui().text }}>{count()}</Text>
<View style={{ flexDirection: "row", gap: 12 }}>
<View onPress={() => change(-1)} role="button" {...button()}>
<Text style={buttonText()}>−1</Text>
</View>
<View onPress={() => change(1)} role="button" {...button("accent")}>
<Text style={buttonText("accent")}>{desktop ? "+1" : "Tap"}</Text>
</View>
</View>
<If when={history().length > 0}>
<Text style={{ fontSize: 13, color: ui().muted }}>Recent: {history().join(", ")}</Text>
</If>
<View onPress={() => (count.set(0), history.set([]))} hitSlop={8} hoverStyle={hoverFill()} focusStyle={focusRing()} activeStyle={{ opacity: 0.7 }}>
<Text style={{ fontSize: 15, color: ui().accent }}>Reset</Text>
</View>
</View>
);
}
mount(App);#Créer une application
Dans un terminal, nsh init demande dans quel langage l’application est écrite : rust, ts ou mixed (TSX avec un crate Rust dédié). --template choisit sans demander.
#En Rust
nsh init my-app --template rust
nsh init crée le répertoire et écrit un projet Cargo :
| Fichier | Rôle |
|---|---|
src/lib.rs | L’application : son composant racine, son état et ses gestionnaires |
src/main.rs | Monte la même racine sur macOS, Windows ou Linux pour cargo run |
Cargo.toml | Le crate, qui dépend de nsh-core et nsh-base à la version de la CLI |
nsh.json | Le nom, l’id, la version et le numéro de build de l’application, son crate ("rust": ".") et son composant racine ("root": "App") (Structure du projet) |
App.test.tsx | Le test de la racine, en TypeScript (nsh test), avec tsconfig.json et .nsh/types pour votre éditeur |
README.md | Comment lancer, tester et packager l’application, et comment ajouter plus tard une entrée TSX |
.gitignore | Ignore .nsh/ et target/ |
Le développement Rust nécessite une toolchain Rust (rustup) ; les commandes nsh compilent les runners de l’application à partir des crates de NSH sur crates.io, à la version de la CLI.
#En TSX
nsh init my-app --template ts
Une application TSX est constituée d’un ou plusieurs fichiers .tsx avec une entrée : ce que nsh run exécute, et ce qu’une application hôte charge comme module.
| Fichier | Rôle |
|---|---|
App.tsx | Le module d’entrée : rend l’application avec mount |
App.test.tsx | Son test (nsh test) |
nsh.json | Le nom, l’id, la version et le numéro de build de l’application (Structure du projet) |
tsconfig.json | Étend .nsh/tsconfig.json, les réglages TypeScript de NSH, qui résolvent nsh vers les déclarations de .nsh/types |
.gitignore | Ignore .nsh/ et target/ |
.nsh/types/ | Déclarations générées de chaque module NSH, pour votre éditeur |
Les fichiers déjà existants sont conservés, vous pouvez donc l’exécuter dans un répertoire qui en contient certains ; une application Rust est créée dans un nouveau répertoire.
Dans les deux cas, le nom affiché est par défaut celui du répertoire ; passez --name "My App" pour en choisir un autre. L’id devient com.example.<name> : modifiez-le dans nsh.json avant de publier. nsh init my-app --template mixed crée une application TSX qui est aussi un crate Rust (Cargo.toml et src/lib.rs à côté de App.tsx), dont le composant, le module et la classe App.tsx importe (Développer avec Rust).
#Le composant racine
Les deux modèles produisent la même application, un compteur :
// src/lib.rs
use nsh_base::theme;
use nsh_core::prelude::*;
/// The crate's UI set: what a TSX entry may import from `my-app/native`.
#[native_module("my-app/native")]
fn native(registry: &mut Registry) {
registry.register::<App>();
}
/// The application's root component.
#[component]
pub fn App() -> impl IntoElement {
let count = signal(0_u32);
view()
.flex_1()
.bg(move || theme().background)
.text_color(move || theme().foreground)
.child(
view()
.flex_1()
.p_safe_area()
.child(
view()
.flex_1()
.items_center()
.justify_center()
.gap_4()
.child(text(move || format!("Count: {}", count.get())).text_size(px(28.0)))
.child(
view()
.px_5()
.py_3()
.rounded(px(10.0))
.bg(move || theme().primary)
.active(|s| s.opacity(0.6))
.on_press(move || count.update(|value| *value += 1))
.child(
text("Tap")
.text_size(px(17.0))
.text_color(move || theme().primary_foreground),
),
),
),
)
}// App.tsx
import { mount, signal, Text, View } from "nsh";
function App() {
const count = signal(0);
return (
<View class="flex-1 items-center justify-center gap-4 bg-background pt-[env(safe-area-inset-top)] pb-[env(safe-area-inset-bottom)]">
<Text class="text-3xl font-semibold text-foreground">{count()}</Text>
<View
onPress={() => count.update((c) => c + 1)}
role="button"
class="rounded-(--radius) bg-primary px-5 py-3 active:bg-primary/90"
>
<Text class="text-lg text-primary-foreground">Tap</Text>
</View>
</View>
);
}
mount(App);En Rust :
use nsh_core::prelude::*apporte les primitives (view(),text(),scroll(),input(),image(), …), les signaux et les méthodes de style typées (.p_safe_area()ajoute à une vue un padding égal à la zone de sécurité) ;nsh_basecontient les composants de base sans style (button(), le clavier, les boîtes de dialogue) ettheme(), les couleurs et les rayons avec lesquels les composants de l’application dessinent (Thème).#[component] pub fn App()est un composant : une fonction qui s’exécute une seule fois et renvoie ses éléments. Le"root": "App"densh.jsonle nomme, etnsh runle monte.signal(0_u32)crée une valeur qui change au fil du temps.text(move || format!(…, count.get()))lie le texte au signal : quandcountchange, NSH réexécute cette closure et met à jour ce seul texte, rien d’autre. Les styles acceptent des closures de la même façon :.bg(move || theme().primary)suit le thème en mode sombre..on_press(..)fait répondre la vue aux appuis ;.active(|s| s.opacity(0.6))est son style pendant l’appui, appliqué nativement. Le bouton appartient à l’application : NSH fournit les primitives et les capacités, et l’application dessine son UI à partir de celles-ci..px_5(),.rounded(px(10.0)),.gap_4()sont des méthodes de style typées nommées comme les utilitaires de Tailwind, en points (px). La mise en page repose sur flexbox, et les enfants s’empilent en colonne par défaut (Mise en page).#[native_module("my-app/native")]nomme ce qu’une entrée TSX peut importer depuis le crate, pour le moment où vous ajoutez des écrans TSX par-dessus (Développer avec Rust).
En TSX :
import { mount, signal, Text, View } from "nsh":nshest le module cœur : signaux,mount, les autres primitives réactives et les éléments hôtes. Il est servi par le runtime natif, et non inclus dans le bundle.nsh/basecontient les composants de base, commensh_baseen Rust.function App()est un composant qui s’exécute une seule fois.{count()}lit le signal dans le JSX : le compilateur voit un appel et fait suivre le signal à ce texte.<View onPress>est une vue native qui réagit aux appuis ; les classesactive:sont son style pendant l’appui.View,Text,Image,Scrollet les autres éléments hôtes sont importés depuisnsh(Inputdepuisnsh/base) ; seulsIfetEachn’ont pas besoin d’import.classaccepte des classes Tailwind ;style={{ … }}accepte un objet de propriétés de style en camelCase, avec des nombres en points.bg-background,bg-primary,text-primary-foregroundetrounded-(--radius)désignent les tokens du thème de l’application (Tokens de thème) : ils suivent le mode sombre d’eux-mêmes, et"theme"dansnsh.jsonles restyle.mount(App)rend le composant dans la surface de l’application.
#Développer
#Lancer l’application
nsh run compile l’application, la lance et recompile pendant que vous éditez ; les logs de l’application apparaissent dans le terminal. Donnez-lui le répertoire de l’application : il exécute la racine d’une application Rust, ou le App.tsx d’une application TSX (nsh run my-app/App.tsx désigne une autre entrée).
Pour une application Rust, nsh run compile le runner propre à l’application pour la cible, avec votre crate et la racine montée et sans QuickJS, puis le démarre ; une modification du code Rust le recompile et redémarre l’application. cargo run dans le répertoire de l’application démarre la même racine directement sur le backend desktop. Pour une application TSX, nsh run démarre un serveur de dev et un hôte de développement qui charge l’application depuis celui-ci, et patche l’application en cours d’exécution à chaque sauvegarde.
Sans --target, nsh run utilise la machine sur laquelle vous êtes : une fenêtre AppKit sur macOS, une fenêtre Win32 sur Windows et une fenêtre GTK sur Linux. Choisissez une autre plateforme avec --target :
| Cible | Commande | S’exécute depuis |
|---|---|---|
| Simulateur iOS | nsh run my-app --target ios | macOS avec Xcode |
| Android | nsh run my-app --target android | macOS, Windows, Linux |
| macOS (AppKit) | nsh run my-app | macOS (par défaut) |
| Windows | nsh run my-app | Windows (par défaut) |
| Linux (GTK 4) | nsh run my-app | Linux (par défaut) |
| Web | nsh run my-app --target web | macOS, Windows, Linux |
| Mac Catalyst | nsh run my-app --target catalyst | macOS |
#Simulateur iOS
nsh run my-app --target ios
nsh run my-app --target ios --device "iPhone 16 Pro"
nsh utilise un simulateur déjà démarré, ou démarre l’iPhone le plus récent si aucun n’est en cours, et y installe l’application (le runner d’une application Rust, ou l’hôte de développement). --device choisit un simulateur par nom ou par UDID.
#Android
nsh run my-app --target android
Démarrez d’abord un émulateur ou connectez un appareil (adb devices les liste ; NSH utilise le premier). nsh y installe l’application : le libnsh.so propre à une application Rust, ou un APK d’hôte de développement (recompilé uniquement quand le runtime change) dont le port du serveur de dev est redirigé vers l’appareil avec adb reverse, ce qui fonctionne par USB sans réseau partagé.
#macOS (AppKit)
nsh run my-app --target macos
L’application s’ouvre comme une application Mac de bureau (AppKit, dans une fenêtre redimensionnable), avec la souris, le trackpad et le clavier comme entrées à la place du toucher. Elle nécessite macOS 14 ou ultérieur ; voir macOS.
#Windows (par défaut sur Windows)
nsh run my-app
L’application s’ouvre dans une fenêtre de bureau (1200×800 points), avec la souris, le pavé tactile, le tactile et le clavier comme entrées. Elle nécessite Windows 10 1809 ou ultérieur ; voir Windows.
#Linux (par défaut sur Linux)
nsh run my-app
L’application s’ouvre dans une fenêtre GTK (390×844 points). Elle nécessite GTK 4.14 ou ultérieur ; voir Linux.
#Web
nsh run my-app --target web
nsh affiche l’adresse de la page ; ouvrez-la dans un navigateur. Une application Rust s’exécute en WebAssembly avec votre crate, et la page ouverte charge d’elle-même le nouveau build après une modification ; voir Web.
#Mac Catalyst
nsh run my-app --target catalyst
L’application s’ouvre dans une fenêtre de la taille d’un iPhone (390×844 points) sur votre Mac. C’est le moyen le plus rapide de voir une application comme l’affiche un téléphone : aucun simulateur à démarrer. Elle utilise UIKit comme iOS, donc ce que vous voyez est ce qu’affiche un iPhone, hormis les comportements propres au Mac comme le défilement à la molette. Pour l’application Mac elle-même, utilisez --target macos.
#La modifier
Laissez nsh run en cours d’exécution et changez le libellé du bouton :
text("Add one")
.text_size(px(17.0))
.text_color(move || theme().primary_foreground)<Text style={{ color: "white", fontSize: 17 }}>Add one</Text>Enregistrez. Une application Rust est recompilée et redémarre avec le nouveau libellé ; le compteur repart de zéro. Une application TSX reçoit une mise à jour à chaud : la CLI recompile et envoie un patch, et l’hôte réexécute le module modifié et ne re-rend que les composants dont le code a changé. Ici, c’est App, qui possède le signal count, donc le compteur repart lui aussi de zéro.
Des morceaux d’UI peuvent passer dans des composants à part, ou dans de simples fonctions qui renvoient des éléments :
/// The app's button: a view that answers presses.
fn tap_button(on_press: impl Fn() + 'static) -> impl IntoElement {
view()
.p_3()
.rounded(px(10.0))
.bg(move || theme().primary)
.active(|s| s.opacity(0.6))
.on_press(on_press)
.child(text("Tap").text_size(px(17.0)).text_color(move || theme().primary_foreground))
}
// In App: .child(tap_button(move || count.update(|value| *value += 1)))function TapButton(props: { onPress: () => void }) {
return (
<View onPress={props.onPress} activeStyle={{ opacity: 0.6 }} style={{ padding: 12, borderRadius: 10, backgroundColor: "#0969da" }}>
<Text style={{ color: "white", fontSize: 17 }}>Tap</Text>
</View>
);
}
// In App: <TapButton onPress={() => count.update((c) => c + 1)} />En TSX, l’état survit quand vous modifiez un composant qui ne le possède pas : appuyez plusieurs fois, puis changez la couleur de TapButton, et le bouton se met à jour tandis que le compteur reste. Le remplacement à chaud fonctionne par composant de premier niveau : une fonction dont le nom commence par une majuscule et dont le corps contient du JSX. Un composant est re-rendu quand son propre code change, ou quand le code du module situé en dehors des composants change (une constante qu’il utilise, un helper). Si une modification ne peut pas être appliquée en place (par exemple, elle importe un module NSH que l’application en cours n’avait pas encore importé, ou elle change les capacités de nsh.json), l’hôte recharge toute l’application.
#Erreurs
Une erreur de compilation Rust est affichée dans le terminal avec la sortie de cargo, et nsh run recompile à la sauvegarde suivante. Une erreur de build TSX est affichée dans le terminal et montrée sur l’appareil sous forme d’overlay rouge indiquant le fichier, la ligne et la colonne ; l’application continue d’exécuter le dernier build valide. L’overlay disparaît au build réussi suivant (touchez-le pour le fermer plus tôt). Une exception levée à l’exécution est montrée de la même façon, avec sa pile ramenée à votre source .tsx.
#L’héberger dans votre application
Une application native affiche NSH dans une vue qui lui est propre : NshAppView (un UIView) sur iOS, NshView (un View Android) sur Android, AppView sur macOS, Windows, Linux et dans les applications GPUI. Placez-la comme n’importe quelle autre vue, sur n’importe quel écran, autant de fois que nécessaire.
Une racine Rust fait partie du build de l’application : l’application lie votre crate et monte la racine avec AppSource::Rust sur le AppView de macOS, Windows, Linux, GPUI et iOS (Développer avec Rust). Une application TSX est un module que la vue charge : pendant le développement depuis le serveur de dev, en release depuis un bundle compilé ou un package signé téléchargé. Les sections ci-dessous chargent des modules TSX.
#iOS
Liez NativeShellDev.xcframework en Debug et NativeShell.xcframework en Release (Intégration dans les applications iOS), puis :
import NativeShell
let app = NshAppView()
app.onError = { message in print("nsh error: \(message)") }
view.addSubview(app) // and constrain it like any view
#if DEBUG
try app.connectDevServer(URL(string: "ws://127.0.0.1:8790/ws")!) // `nsh dev my-app` on your Mac
#else
try app.loadBundle(at: Bundle.main.url(forResource: "bundle", withExtension: "nshc", subdirectory: "nsh")!)
#endif
nsh dev my-app sert le module à l’application et pousse chaque sauvegarde, comme le fait nsh run avec son propre hôte. Pour le build de release, nsh build my-app --release --out MyApp/nsh écrit le bundle dans un dossier que vous ajoutez aux ressources de l’application.
#Android
Ajoutez nsh-dev.aar pour les builds debug et nsh.aar pour les builds release (Intégration dans les applications Android), puis :
val nsh = NshView(this)
nsh.setErrorListener { message -> Log.e("nsh", message) }
setContentView(nsh) // or add it to any layout
if (BuildConfig.DEBUG) {
nsh.connectDevServer("ws://127.0.0.1:8790/ws") // with `adb reverse tcp:8790 tcp:8790`
} else {
nsh.loadAssetBundle("nsh") // assets/nsh, from `nsh build my-app --release --out app/src/main/assets/nsh`
}
#macOS, Windows et Linux
Les applications de bureau écrites en Rust intègrent NSH avec AppView, depuis les crates nsh-platform-macos, nsh-platform-windows, nsh-platform-linux et nsh-platform-gpui : une NSView, une fenêtre enfant, un widget GTK ou une vue GPUI qui monte une racine Rust ou charge un bundle compilé. Voir macOS, Windows, Linux et GPUI, ainsi que Plateformes pour tous les hôtes côte à côte.
#Ce que le module peut faire
Sur iOS et Android, l’hôte décide avant le chargement de ce qu’un module peut faire : grantCapabilities définit les hôtes réseau et les autres capacités auxquels il peut accéder, et des limites plafonnent sa mémoire, son temps d’appel et son nombre de nœuds (Sandbox et capacités). Pour donner à vos modules vos propres vues natives (une carte, un graphique, un composant de design system), enregistrez-les comme composants natifs.
#Publier des mises à jour sous forme de packages signés
Un module TSX que l’application télécharge après la publication prend la forme d’un package signé. Créez une clé racine une fois et gardez-la hors ligne, certifiez une clé de release avec elle, puis publiez :
nsh keygen keys/root && nsh keygen keys/release
nsh certify --root keys/root.secret --key keys/release.pub --apps 'com.example.*' -o keys/release.cert
nsh publish my-app --key keys/release.secret --cert keys/release.cert --build 2
L’application fait confiance à la clé publique de la racine et charge ce qu’elle télécharge ; NSH vérifie la signature, le certificat et la liste de révocation avant d’exécuter quoi que ce soit :
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 suit le même flux avec NshTrust et NshPackage. Hébergement, distribution et révocation en détail : Packages signés.
#Ou le livrer comme application autonome
nsh package construit une application installable, en Rust ou en TSX, sans projet Xcode ni build Gradle à maintenir de votre côté :
nsh package my-app --target ios-simulator --run
nsh package my-app --target android
nsh package my-app --target macos
nsh package my-app --target linux
nsh package my-app --target windows
nsh package my-app --target web
Une application Rust est empaquetée avec son propre runner, construit à partir de votre crate et des crates de NSH, avec sa racine montée ; une application TSX l’est avec le runtime précompilé. Options, signature et icônes : Packaging.
#Essayer dans le terminal
nsh test exécute une application en mode headless, sans appareil ni fenêtre. À partir du nsh.json d’une application Rust ou du point d’entrée d’une application TSX, il ouvre l’application, la laisse s’exécuter et affiche ce qu’elle monte ; avec --trace, il affiche aussi les opérations de vues natives et la durée de chaque commit (Testing) :
nsh test my-app/nsh.json --trace # a Rust app's root
nsh test my-app/App.tsx --trace # a TSX app's entry
À partir d’un répertoire, il exécute les tests de l’application : le App.test.tsx de chaque modèle ouvre l’application, appuie sur son bouton et vérifie ce qu’elle affiche (nsh test my-app). Celui d’une application Rust ouvre sa racine (open(".")) ; elle peut aussi être testée dans cargo test avec nsh_testing::Headless (Building with Rust).
#Étapes suivantes
- Building with Rust : les applications Rust, leur thème, et TSX par-dessus un crate Rust.
- JSX and components et Reactivity : composants et signaux, en Rust et en TSX.
- Platforms : ce que prend en charge chaque plateforme, et sa propre page (iOS, Android, macOS, Windows, Linux).
- Signed packages et Sandbox and capabilities : la distribution et ce que les modules sont autorisés à faire.
- Project structure : ce qui va dans
nsh.json, les assets, les fichiers.