Menu da documentação

Documentação

Your first app

Building a native TDE app from scratch.

Nesta página

What a TDE app actually is

A TDE app is an ordinary ANSI terminal program. It is its own Rust binary, it runs as its own OS process, and TDE hosts it behind a PTY in a desktop window. The same binary runs identically in a plain terminal — that is the test that it is a real app and not a plugin.

What TDE owns: windows, focus, composition, the PTY and the app-host lifecycle. What your app never receives: Desktop, Wm, or any global TDE state object. The boundary is a process boundary, and it exists so that an app crash — or an app's dependency tree — stays out of the desktop process.

Apps build their interface from tde-app-sdk, which re-exports the shared component kit. That is how a third-party app looks native, follows the user's theme and works in Web Share without knowing any of that.

Quick start

Install the author CLI, scaffold, test, build and register:

bash
cargo install --git https://github.com/tde-sh/tde --package tde-app-sdk --bin tde-app

tde-app new ./my-app dev.example.my-app
cd my-app

tde-app validate tde-app.toml
cargo fmt --check
cargo test
cargo run

cargo build --release
tde-app register

cargo run starts the binary in your current terminal — no desktop needed. tde-app register writes a Launcher command extension pointing at the release binary; restart TDE and search for the app's title in the Launcher.

The scaffold is deliberately small: Cargo.toml, tde-app.toml, src/lib.rs with a working App, src/main.rs, a headless test in tests/, and a TDE_APP.md written for coding agents. The id argument is optional — omit it and tde-app derives dev.tde.<dirname>.

CommandWhat it does
tde-app new <dir> [id]Scaffold a project. Refuses a non-empty directory.
tde-app validate [tde-app.toml]Check schema, id, entrypoint and capability names.
tde-app register [tde-app.toml] [--commands-dir <dir>]Write the Launcher entry. Fails if the release binary is missing.

The manifest

tde-app.toml is the app's portable declaration.

tde-app.toml
schema = 1

[app]
id = "dev.example.task-board"
title = "Task Board"
version = "0.1.0"
entrypoint = "target/release/task-board"
min_tde = "0.3.18"
capabilities = []
FieldRules
schemaMust be 1. manifest_version is accepted as a synonym; if both are present they must agree.
app.idLowercase reverse-DNS, path-safe, stable. Must match AppMetadata.
app.titleVisible text, no control characters.
app.versionStarts with a digit; digits and . - + * ^ only. Must match AppMetadata.
app.entrypointA safe relative path. No absolute paths, no .., no backslashes, no whitespace or shell metacharacters.
app.min_tdeSame version grammar. Raise it only when your code needs a newer host.
capabilitiesAny of storage, network, clipboard, process, secrets. Unknown names are a validation error.

Unknown keys are rejected outright, so a typo fails tde-app validate instead of being silently ignored.

Registration writes ~/.config/tde/commands/<app-id>.toml containing a single Open command that changes into the project directory and executes the release binary. It is the same command extension mechanism you could write by hand — registration is not a Store publication.

The lifecycle

The v0.1 trait is intentionally three methods:

src/lib.rs
pub trait App {
  fn metadata(&self) -> AppMetadata;
  fn update(&mut self, event: Event) -> AppAction;
  fn render(&mut self, frame: &mut Frame<'_>);
}
TypeYour responsibility
AppMetadataBuilt with AppMetadata::new(id, title, version, icon); it validates all four.
EventKey, Mouse, Resize, Tick, FocusGained, FocusLost, CloseRequested.
Framearea(), theme() and paint() — the bounded surface you are allowed to draw on.
AppActionReturn Continue, Quit, or Notify(Notice). Never reach into host state.
AppHarnessDrives events and inspects the canvas with no terminal attached.

Import surface:

src/lib.rs
use tde_app_sdk::{App, AppAction, AppMetadata, Event, Frame};
use tde_app_sdk::ui::prelude::*;

Direct imports from tde, tde-wm, tde-term or tde-app-host are not public API and will break. Pin the SDK to a tag or revision and set a matching min_tde; the desktop is free to change its compositor and PTY host underneath you as long as you stay on the published boundary.

UI rules

Four rules carry most of the weight.

Theme-native styling

Use the semantic styles a Theme exposes — base, heading, muted, field, selected, accent_style — and the component tokens. No RGB literals, no private app palette, no if theme_is_dark ladder. The host can switch to e-Ink or any of the other palettes without your app being rewritten.

Rect clipping is mandatory

Canvas::draw_str and Canvas::fill know only the outer canvas edge. They do not know about your window, and they will happily paint over the desktop.

  • Use frame.paint() for every piece of app-specific text, fill and ellipsis. It exposes only bounded operations.
  • Pass frame.canvas() only into a shared component's render method.
  • Give the same area rectangle to layout, paint, placement and hit-testing.

This is not advisory. Every TDE app crate ships an AST scanner in tests/ui2_regression.rs that parses its own sources and fails the build on a direct Canvas mutator — resolving aliases, reborrows and shadowing rather than grepping for a method name.

Responsive, not a second app

Observe frame.area() at the top of render and check the Breakpoint. At compact width, swap a permanent rail and split panes for AppBar plus NavDrawer, ellipsize long labels, and choose a compact table representation. The supported minimum viewport is 30 × 24 cells. Do not build a separate mobile app: same task, different presentation.

Mouse first, keyboard complete

Every interactive target starts as a visible mouse or tap target, and every one of them also needs visible focus and a keyboard route:

IntentRequired binding
Move between controlsArrow keys
Activate, toggle, submitEnter and Space
Close, cancel, go backEsc, before the host sees a close request
PointerThe same action and geometry as the keyboard focus

Tab / Shift+Tab may assist form traversal but cannot be the only path. Single-letter accelerators are fine as a shortcut, never as the only route. Keep one source of geometry shared by render, hit-testing and focus.

Headless tests

AppHarness is the public test seam. Test the state transition first, then render it at a fixed size:

tests/starter.rs
let mut app = AppHarness::new(MyApp::default(), Size::new(44, 10), Theme::tde());
app.dispatch(Event::Key(KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE)));
app.render();
assert_eq!(app.app().runs(), 1);

The harness offers new, dispatch, render, resize, app, canvas and notification inspection — no terminal, no screenshots, no flake. Cover at least:

  • a normal width and a compact width;
  • the important state transition;
  • a click inside the visible control rect;
  • the Enter / Space equivalent of that click;
  • Esc cancel or close;
  • an empty data state;
  • a long Unicode label that must ellipsize.

Do not mock a component's result. Exercise real app state and the actual canvas.

Before you share it

  1. tde-app validate tde-app.toml passes.
  2. cargo fmt --check and cargo test pass.
  3. Headless tests exist for the important transitions and the compact layout.
  4. tde-app.toml declares the fewest capabilities possible and matches AppMetadata.
  5. No private desktop crate, no raw host protocol, no hard-coded colour, no unclipped write.
  6. cargo build --release && tde-app register succeeds, TDE restarts, and the app opens from the Launcher.