# Getting started

This page takes you from a clone of the repository to a running, tested app.

> **Example: Hello.** Source in [examples/hello](https://github.com/FormalSnake/NativeDesktop/blob/main/examples/hello). Live on the web and captured on macOS, Linux and Windows in the docs app.


## Install

You need a Rust toolchain, `cargo-nextest` (`nd test` runs through it) and, because the terminal
widget builds `libghostty-vt`, Zig 0.16. The repository's dev shell has all of it:

```sh
nix develop
```

With direnv, the checked-in `.envrc` (`use flake`) does that on `cd`. Without Nix, install
[rustup](https://rustup.rs); `rust-toolchain.toml` pins 1.95.0 with the `wasm32-unknown-unknown`
target and rustup fetches both on the first `cargo` call. Then:

```sh
cargo install cargo-nextest --locked
```

Build the CLI from the repository. It installs a binary named `nd`:

```sh
cargo install --path crates/nd-cli
```

To skip the install, run it in place with `cargo run -p nd-cli -- <args>`; every `nd` command on
this page works that way.

## Create an app

```sh
nd new demo --nd-path /path/to/nativedesktop
```

The crates are not on crates.io yet (`publish = false`), so the project has to point at your
checkout. `--nd-path` does that, and so does the `ND_PATH` environment variable. Add
`--standalone` when the new directory sits inside another Cargo workspace. The name must start
with a lowercase letter and may contain lowercase letters, digits, `-` and `_`. `--id` sets the
reverse-DNS app id; it defaults to `com.example.demo`.

`nd new` writes these files and refuses a directory that is not empty:

| 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, 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` is `cargo run`. On macOS the window is AppKit. On Linux it is the GPUI backend. Windows
code exists but has not been run, so treat a Windows build as untested. On a target with 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, then the appearance audit.
`--no-audit` skips the audit and `--target web` adds 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. The [testing page](https://nativedesktop.formalsnake.dev/tooling/testing.md) covers the tiers.

## 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 do not exist in a browser; `nd check --target web` flags them, and the
[web guide](https://nativedesktop.formalsnake.dev/guides/web.md) covers the rest.

## The hello app

[Hello](https://github.com/FormalSnake/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/FormalSnake/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. The click handler is
wrapped by `cx.listener` so it receives `&mut Counter`. `cx.notify()` marks this view dirty, and
the next frame runs `render` again. `cx.theme()` hands out colour tokens, so the secondary text
colour is not a literal.

Opening the window is two functions. `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/FormalSnake/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/FormalSnake/NativeDesktop/blob/main/examples/hello/tests/tier1.rs), line 7.</sub>


`#[nd::test]` runs natively and, compiled for `wasm32-unknown-unknown`, in headless Chromium.
Next: [the programming model](https://nativedesktop.formalsnake.dev/concepts/programming-model.md).
