Skip to main content

frus-cli

The command-line tool of frus, a cross-platform UI framework in Rust. It installs a program called frus, which starts new applications, runs them on any device, and builds what you ship.

cargo install frus-cli
frus create my_app
cd my_app
frus run # on this computer
frus run -d android # on the connected phone
frus run -d web # in a browser
commandwhat it does
frus create <dir>writes a new application
frus deviceslists what the application can run on now
frus run [-d <device>]builds it, starts it there, and shows its log in the terminal
frus build apk|web|desktopbuilds what you ship, in release

frus create writes an application that runs on Windows, macOS, Linux, Android and the web from the same code: a counter built from a component, with its tests, the Android launcher icons and launch theme, the web page, a CI workflow and a release workflow that publishes every platform's build when you push a tag. Its README.md says how to run and ship it on each platform.

frus create <directory> [options]

--name <name> the package's name (default: the directory's name)
--org <org> the reverse domain the Android package starts with
(default: com.example; the package is <org>.<name>)
--frus-path <dir> build against a checkout of frus, not the published crates
--no-git do not make the directory a git repository

The directory is made if it is not there, and frus create never writes over a file. In a directory with files already, only the template's missing files are written: frus create . in an application brings back what was deleted (web/, the icons, the desktop binary, the workflows), keeps its Cargo.toml and its code, adds no sample code beside them, and takes the application's name from its manifest. The name is checked before anything is written. It must work as a package, a crate, a program, a .wasm file and part of an Android package, so it is lowercase letters, digits, _ and -, starting with a letter, and neither a Rust keyword nor the name of a crate the application builds with.

The template is built into the program, so frus create needs no network. Its files are in template/, which is also a cargo generate template.

Running: frus run and frus devices​

frus devices
frus run [-d <device>] [--release] [--hot] [--port <port>] [-- <args for the application>]

--hot (desktop, web and Android): true hot reload. The application keeps running, and each change you save is compiled into a patch and applied to it in place, with every state where it was, in a few seconds. R still starts it again afresh. The first run installs the engine (a few minutes, once, into ~/.frus/tools) and builds the application for patching (into target/frus-hot/, without changing its manifest). A patch cannot change the shape of a state that is alive (a field added): R for that.

On the web (frus run --hot -d web), the page is served by frus run as without --hot, its module built by the engine; each patch is linked into the running page, which does not reload. The first patch of a run takes longer (half a minute on a debug build), the next ones a few seconds. A page that is reloaded gets the latest patch again as it starts.

On Android (frus run --hot -d <device>), the engine builds the application into an Android project of its own (a plain native activity, so nothing about the application changes), installs it and starts it; each patch is pushed to the device and loaded into the running application. The first build is long — the whole application and the Android project, a quarter of an hour on a modest machine — and its package is large, for it keeps every symbol a patch may need. Patches then take ten to twenty-five seconds. R builds again, installs and starts afresh (a minute or two). The package installed by frus run without --hot is removed first: Android will not replace it with the engine's (its data on the device goes with it).

On Linux, building the engine needs OpenSSL's headers (sudo apt install pkg-config libssl-dev); frus doctor says whether they are there.

A change that does not compile is said with its place in your own files (src/x.rs:40:34: error[E0425]: …), and the application goes on running the code it had. FRUS_HOT_TRACE=1 shows every line the engine prints, for when something looks wrong.

frus devices lists this computer, the Android devices adb sees (USB debugging on) and the emulators that can be started, the iOS simulators on a Mac, the browsers installed here, and a bare web server. -d takes a device's id or name, or android, web, desktop; without it the application runs on this computer.

Whatever the device, the application's log follows in the terminal: the desktop program's output, the phone's logcat (the application's own tag, and Rust panics), and the browser's console — the page is served with a small script that copies each console message back to the terminal. The run lasts as long as the application: closing the window, the app or the browser ends it.

While it runs, keys drive it:

key
rreload: build again, stop the application and start the new build where the old one was — the same page, on a desktop the window in the same place and size — with the state of every component that keeps one (State::save)
Rrestart: the same, but every state starts afresh
hthe list of keys
ddetach: frus run ends and the application goes on running (desktop, Android)
cclear the terminal
qquit: the application is stopped
sscreenshot, saved beside the application as frus_01.png, frus_02.png… (desktop: the application's frame; Android: the device's screen; web: the page's canvas)
wprint the widget tree, every widget with its box
fprint the focus tree: the focusable widgets in tab order, the focused one marked
Sprint the semantics tree: what a screen reader is told, in reading order
ithe inspector: outlines, and a card for the widget under the pointer (F12 in the window)
pdebug paint: outlines around every widget
oswitch the platform the widgets behave as: Android, iOS, Windows, macOS, Linux, Fuchsia (debug builds)
bswitch the brightness: light or dark, for an application with a theme for each
Pthe performance panel: what the last frame cost, building, laying out and painting, rendering, on the GPU, and two graphs of the last 90 frames — the interface and the drawing — against a 60 Hz frame's budget, late frames in red; the frame statistics in the log

The keys that look into the application reach it through the channel it reports on, and it carries them out on its next frame: a socket on a desktop, a file in its own storage on Android (written with run-as, so a debug build only), and the page's own script on the web. A key pressed while the application is still starting waits for it.

A build that fails leaves the running application as it was. The restart is the same on every platform as far as the developer sees, and done three ways underneath:

  • desktop: the application runs from a copy of the program, so the new build can be written while it runs; it reports where it is and where its window sits to frus run over a local socket, and the next one is started with both;
  • Android: the same report reaches frus run through the application's log; the new APK is installed — unless the build wrote the same package again, which is only started again — and started with the location as the link it opens;
  • web: the page asks the server which build it serves, and reloads itself when there is a new one — its address, which is the application's location, survives the reload.

iOS simulators are listed, but frus cannot run on iOS yet: its shell does not exist (issue #19).

For editors: frus daemon and frus run --machine​

An editor drives the same commands through JSON lines: one object a line, commands on standard input, answers and events on standard output. Nothing else is written to standard output; the tools frus runs (cargo, cargo-apk, wasm-bindgen) write to standard error.

frus daemon runs as long as the editor does, and knows the devices:

→ {"id":1,"method":"device.getDevices"}
← {"id":1,"result":[{"id":"windows","name":"Windows","platform":"windows","category":"desktop","detail":"windows-x64","emulator":false,"available":true,"unavailableReason":null}, …]}
→ {"id":2,"method":"device.enable"}
← {"id":2,"result":true}
← {"event":"device.added","params":{"id":"XMJNW19B23011768","name":"STK L21","platform":"android", …}}
methodanswer
daemon.versionthe tool's version
daemon.shutdowntrue, and the daemon ends
device.getDevicesthe devices, as frus devices lists them
device.enable / device.disabletrue; device.added and device.removed events while enabled
device.takeScreenshotthe screen of an Android device (deviceId), a PNG in base64
daemon.getSupportedPlatformsfor the application in projectRoot: platforms, and in platformTypes each platform with isSupported and, if not, reasons (reasonText, fixText)
emulator.getEmulatorsthe emulators that can be started: id, name, category, platformType
emulator.launchstarts emulatorId (coldBoot: without its saved state); it arrives as device.added once up
emulator.createmakes an Android emulator named name (or frus_emulator), installing a system image if none is there: success, emulatorName, error

On the way, the daemon says what it does as daemon.logMessage events (level, message), and what the developer should see now as daemon.showMessage (level, title, message).

frus run --machine -d <device> is one run, with every event carrying its appId:

eventwhen
app.startthe run begins: deviceId, deviceName, directory
app.progresswhat the tool says: message, and error: true for a failure that did not end the run
app.startedthe application is up
app.loga line of the application's own log: log, error
app.restarteda reload or restart is done: fullRestart, seconds
app.stopthe run ended: error when it failed

and takes {"method":"app.restart","params":{"fullRestart":false}} (r; true is R), {"method":"app.stop"}, {"method":"app.detach"}, and any other key by its letter: {"method":"app.key","params":{"key":"p"}}.

Building: frus build​

frus build apk # the Android package
frus build web # web/, with the application compiled into web/pkg/
frus build desktop # the program for this computer

Release by default; --debug for a debug build.

On Windows, frus build desktop puts dxcompiler.dll and dxil.dll beside the program: DXC, DirectX 12's shader compiler, with which the program starts about half a second sooner than with Windows' own (fetched once into ~/.frus/tools, about 32 MB). Ship them with the program; it runs without them, starting slower. frus run puts them beside the program it starts too.

Emulators: frus emulators​

frus emulators the emulators that can be started
frus emulators --launch <id> [--cold] start one (--cold: without its saved state)
frus emulators --create [--name <name>] make an Android emulator

--create installs what it needs first: the emulator, and a system image if none is installed (the newest installed one is used otherwise). A started emulator shows in frus devices once it is up, and frus run -d <its id> runs on it.

Starting over: frus clean​

frus clean deletes the application's builds. When its build folder is its own (target/), all of it; when the folder is shared with other projects (CARGO_TARGET_DIR, a workspace), only this application's builds, for each platform, and the other projects' are left alone. The web build's generated web/pkg and frus run --hot's shadow package go too.

Tests and lints: frus test, frus analyze​

frus test [<filter>] [-v]
frus analyze [--fix]

frus test runs cargo test and says it briefly: the counts, then each failed test with the place it panicked and its message.

✗ Tests: 3 passed, 1 failed.

✗ counter::tests::adds
src/counter.rs:40:9
assertion `left == right` failed
left: 1
right: 2

A build that does not compile is said the same way, each error at its place; -v shows cargo's own output instead.

frus analyze runs clippy on every target and rustfmt's check, and lists what to fix — each lint at its place, then the files that are not formatted. --fix formats them and applies clippy's own suggestions first. Both commands exit with a failure while anything is left, for a CI step or a pre-commit hook; clippy and rustfmt are installed if the toolchain lacks them.

Checking the machine: frus doctor​

frus doctor [-v] [--fix] [--machine]

One line for each part of the toolchain — [✓] ready, [!] usable but incomplete, [✗] missing — and under it what is wrong, with what to do:

[✓] frus (0.5.0)
[✓] Rust (1.96.1, x86_64-pc-windows-msvc)
[✓] Desktop (windows)
[!] Android toolchain (android-36.1)
✗ cargo-apk is not installed: `frus doctor --fix`
[✓] Web (Edge)
[✓] Hot reload (frus run --hot)
[!] iOS
✗ frus cannot run on iOS yet: its shell does not exist (ROADMAP, issue #19)
[✓] VS Code (1.142.0)
[✓] Connected devices (3 available)

It looks at Rust and its targets, Visual Studio's C++ tools on Windows, the Android SDK (its platform, build-tools, NDK, Java, the Rust target, cargo-apk, the emulator), the web build (the wasm target, wasm-bindgen, the browsers), the hot-reload engine, iOS, VS Code and its frus extension, and the devices. It changes nothing: -v shows everything it found, --fix installs what is missing (never what it found fine), --machine answers in JSON.

What is missing is installed​

A command does not stop at "install X first": what it needs and does not find, it installs, says so, and carries on.

  • Rust targets (wasm32-unknown-unknown, the Android ones the manifest lists): rustup target add.
  • wasm-bindgen, at exactly the version in the application's Cargo.lock. A different version on the PATH is left alone — other projects may need it — and the pinned one goes in ~/.frus/tools/.
  • cargo-apk: cargo install cargo-apk.
  • The Android SDK and NDK: the SDK ANDROID_HOME names, or Android Studio's, is completed with the packages it lacks (sdkmanager, licences accepted); with no SDK at all, one is installed in ~/.frus/android-sdk, with a JDK in ~/.frus/jdk if there is no Java. Downloads use curl and the system's archive tool.

FRUS_HOME moves ~/.frus; FRUS_ANDROID_SDK names the SDK to use (and to install into).

Everything the tool does can still be done by hand with cargo, cargo-apk, adb and wasm-bindgen: it adds no build system of its own.

License​

MIT or Apache-2.0, at your option.