# Getting started

This page takes you from installing the `nd` CLI to a running, tested app.

> **Example: Hello.** Source in [examples/hello](https://github.com/FormalStudio/NativeDesktop/blob/main/examples/hello). Its page on the docs site shows it captured on macOS, Linux and Windows.


## Install

nd releases are git tags on [github.com/FormalStudio/NativeDesktop](https://github.com/FormalStudio/NativeDesktop),
not crates.io versions. nd builds on a patched GPUI, and crates.io doesn't accept git or patched
dependencies.

1. Install [rustup](https://rustup.rs) and git.
2. On macOS, install Xcode or the Command Line Tools (`xcode-select --install`). The AppKit
   backend links Swift.
3. If you use the terminal widget (`nd::terminal`), put Zig 0.16 on `PATH`.
4. Install `cargo-nextest`, which `nd test` runs through, and then the CLI from the release tag.
   The CLI installs a binary named `nd`.

```sh
cargo install cargo-nextest --locked
cargo install --git https://github.com/FormalStudio/NativeDesktop --tag v0.6.1 --locked nd-cli
```

`--locked` builds with the versions that nd was tested with. Without it, cargo takes the newest
releases, and some of those have since been yanked. In a clone of the repository, `nix develop`
gives you all of these tools, and `cargo install --path crates/nd-cli` installs the CLI from the
checkout.

### Windows

Run these commands in PowerShell:

```powershell
winget install --id Microsoft.VisualStudio.2022.BuildTools --override "--quiet --wait --add Microsoft.VisualStudio.Workload.VCTools --includeRecommended"
winget install --id Rustlang.Rustup
winget install --id Git.Git
```

The first command installs the MSVC compiler, the linker, and the Windows SDK that the
`x86_64-pc-windows-msvc` toolchain links with. Then:

1. Open a new terminal so that `cargo` and `git` are on `PATH`.
2. Run the two `cargo install` commands from the previous section.

`nd package windows` also needs the .NET SDK (`winget install --id Microsoft.DotNet.SDK.8`), which
it uses to fetch Velopack's `vpk`.

A debug build of an app and its tests fills several gigabytes under `target`. If `C:` is low on
space, set `CARGO_TARGET_DIR` to a folder on another drive.

## Create an app

```sh
nd new demo
```

The new project depends on nd at the CLI's own release tag (`tag = "v0.6.1"` for CLI 0.6.1), and its
`[patch.crates-io]` block points the GPUI crates at the fork vendored in the same tag. `nd new`
also writes nd's `Cargo.lock`, so the first build resolves to the versions nd was released with.
Add `--standalone` if the new directory is inside another Cargo workspace. The name must start
with a lowercase letter and can contain lowercase letters, digits, `-`, and `_`. `--id` sets the
reverse-DNS app id, which defaults to `com.example.demo`. To work against a local checkout of nd
instead, pass `--nd-path /path/to/nativedesktop/crates/nd` (or set `ND_PATH`).

`nd new` refuses a directory that isn't empty. It writes these files:

| File | What it is |
| --- | --- |
| `src/app.rs` | The `Counter` view. This is the file you edit first. |
| `src/lib.rs`, `src/main.rs` | `app()`, `open()` and `run()`; `main` calls `run`. |
| `src/demo.rs` | Fixed state when `<NAME>_DEMO` is set (`DEMO_DEMO=1` here), for tests and screenshots. |
| `tests/app.rs` | A tier 1 test that clicks the button on the test backend. |
| `tests/appearance.rs` | The tier 2 appearance audit: one `.window(..)` per window you add. |
| `nativedesktop.toml` | App id, display name, and update feed. |
| `build.rs` | One call, `nd::build::app()`, which links the app's Swift on macOS. |
| `justfile` | `dev`, `check`, and `test` recipes. |
| `CLAUDE.md`, `README.md` | The rules an agent follows in this project, and a README with `nd:` placeholders. |
| `rustfmt.toml`, `clippy.toml`, `.config/nextest.toml` | Format, lint, and test-tier settings. |

## Run it

```sh
cd demo
just dev
```

`just dev` runs `cargo run`. On macOS, the window uses AppKit. On Linux and Windows, it uses the
GPUI backend. On a target that has no backend, `run` returns `no platform backend is linked for this
target`.

## Check and test

```sh
nd check
nd test
```

`nd check` runs `cargo fmt --check`, clippy with the framework's deny table (`unwrap_used`,
`expect_used`, `dbg_macro`, `todo`, and warnings), the framework lints, and then the appearance
audit. Add `--no-audit` to skip the audit, or `--target web` to add the web lints. `nd test` runs
tiers 1 and 2 through nextest with no window server. `nd test --tier real` runs tier 3, which opens
real windows one at a time. For details on each tier, see the [testing page](https://native.formalsnake.dev/tooling/testing.md).

## Keep nd up to date

```sh
nd upgrade
```

This moves the `nd` dependencies and the GPUI patch block to the newest release tag, updates
`Cargo.lock`, and prints the [changelog](https://github.com/FormalStudio/NativeDesktop/blob/main/CHANGELOG.md)
entries between the two versions. To pick a tag, add `--to v0.7.0`. Then install the matching CLI
with the same `cargo install --git ... --tag <tag> --locked nd-cli` command.

## Run it in a browser

```sh
nd serve --target web
```

This compiles the app to WebAssembly, runs `wasm-bindgen` and serves the bundle at
`http://127.0.0.1:8080/` (`--port` changes it). The first run installs a `wasm-bindgen-cli` that
matches your lockfile. `nd build --target web` writes the bundle without serving it. Some
framework calls don't exist in a browser. `nd check --target web` flags them, and the
[web guide](https://native.formalsnake.dev/guides/web.md) covers the rest.

## The hello app

[Hello](https://github.com/FormalStudio/NativeDesktop/blob/main/examples/hello/src/lib.rs) is the scaffold's counter with a toolbar button. The
view is a struct and an `impl Render`:

```rust
pub struct Counter {
    count: u32,
}

impl Render for Counter {
    fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
        let theme = cx.theme();
        scaffold()
            .toolbar(toolbar().item(button("add", "Add one").on_click(cx.listener(
                |this, _: &ClickEvent, _window, cx| {
                    this.count += 1;
                    cx.notify();
                },
            ))))
            .content(
                v_flex()
                    .size_full()
                    .items_center()
                    .justify_center()
                    .gap_2()
                    .child(window_controls().at(point(px(16.), px(10.))))
                    .child(text(self.count.to_string()).text_size(px(48.)).tabular_nums())
                    .child(text("clicks").text_color(theme.text.secondary)),
            )
    }
}
```

<sub>From [examples/hello/src/lib.rs](https://github.com/FormalStudio/NativeDesktop/blob/main/examples/hello/src/lib.rs), line 4.</sub>


`scaffold()` is the window structure: a toolbar and a content area. `button("add", "Add one")` is
a toolbar item; the string `"add"` is its id, which tests use to find it. `cx.listener` wraps
the click handler so that it receives `&mut Counter`. `cx.notify()` marks this view dirty, and
the next frame runs `render` again. `cx.theme()` provides color tokens, so the secondary text
color isn't a literal.

Two functions open the window. `Application::new` takes the manifest that `build.rs` filled
from `nativedesktop.toml`, and `open` builds the root view as an entity:

```rust
pub fn app() -> Application {
    Application::new(nd::manifest!())
}

pub fn open(cx: &mut App) -> WindowHandle<Counter> {
    cx.open_window(WindowOptions::main("main").title("Hello"), |_window, cx| cx.new(|_cx| Counter { count: 0 }))
}
```

<sub>From [examples/hello/src/lib.rs](https://github.com/FormalStudio/NativeDesktop/blob/main/examples/hello/src/lib.rs), line 35.</sub>


`src/main.rs` is `hello::app().run(|cx| { hello::open(cx); })`. The test drives the same `open`:

```rust
#[nd::test]
fn starts_at_zero(cx: &mut TestApp) {
    let window = hello::open(cx).id();
    assert!(text(cx, window).contains("0 clicks"), "{}", text(cx, window));
}

#[nd::test]
fn add_one_counts_each_click(cx: &mut TestApp) {
    let window = hello::open(cx).id();
    click(cx, window, "add");
    click(cx, window, "add");
    assert!(text(cx, window).contains("2 clicks"), "{}", text(cx, window));
}
```

<sub>From [examples/hello/tests/tier1.rs](https://github.com/FormalStudio/NativeDesktop/blob/main/examples/hello/tests/tier1.rs), line 7.</sub>


A `#[nd::test]` test runs natively, and in headless Chromium when you compile it for
`wasm32-unknown-unknown`.
Next: [the programming model](https://native.formalsnake.dev/concepts/programming-model.md).
