Notepad demo app showing the Courvux reactive UI framework running inside Tauri 2, styled with Tailwind 4, and shipping with strict CSP (script-src 'self', no unsafe-eval) thanks to the courvux-precompiler Rust → WASM build-time expression compiler.
A complete Markdown notepad with two top-level modes:
- Library mode — flat notes folder owned by the app, one
.mdper note with YAML frontmatter. - Project mode — open any folder anywhere on disk and edit
.mdfiles in place. Native folder picker, file tree, image preview, inline image rendering viaasset://.
Plus: native menu bar (File / Edit), .md file associations, single-instance handling, PDF export with real link annotations, sidebar search, save state machine, atomic writes.
Strict CSP (script-src 'self', no unsafe-eval) thanks to the courvux-precompile Vite plugin.
See FEATURES.md for the complete feature list.
pnpm install
pnpm tauri:devThe first build compiles all of Tauri + the platform's webview bindings (WebKit GTK on Linux, WebView2 on Windows, WKWebView on macOS) and takes a few minutes; subsequent runs are seconds. Hot module reload works for both src/main.js and src/style.css; Rust changes inside src-tauri/ trigger a re-link, not a full rebuild.
Common to every platform:
- Node 18+ (
node --version) - pnpm (
pnpm --version) —npm install -g pnpmif missing - Rust toolchain —
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
sudo dnf install -y \
webkit2gtk4.1-devel \
openssl-devel \
curl wget file \
libappindicator-gtk3-devel \
librsvg2-devel \
patchelf \
rpm-buildsudo apt install -y \
libwebkit2gtk-4.1-dev \
build-essential curl wget file \
libxdo-dev libssl-dev libayatana-appindicator3-dev \
librsvg2-dev patchelfxcode-select --installThat ships everything Tauri needs (Clang, the macOS SDK, codesign for ad-hoc signatures during local builds).
- Install Microsoft C++ Build Tools — pick the "Desktop development with C++" workload.
- WebView2 is preinstalled on Windows 11 and recent Windows 10 updates. If your build target is older, install the Evergreen runtime.
- Optional bundling tools (only needed for installer artifacts): the build downloads NSIS / WiX automatically on first run.
pnpm tauri:buildOutputs land in src-tauri/target/release/bundle/:
| Platform | Bundle types Tauri produces |
|---|---|
| Linux | appimage/*.AppImage, deb/*.deb, rpm/*.rpm |
| macOS | dmg/*.dmg, macos/*.app |
| Windows | msi/*.msi, nsis/*-setup.exe |
The bundle.targets array in src-tauri/tauri.conf.json controls which formats to build. Currently set to ["appimage", "rpm", "deb"] for the Linux Fedora-targeted host that produced this repo's first release; on macOS / Windows the CLI ignores Linux-only targets and emits the platform-native ones automatically.
To register the .md file association on Linux, install the produced package:
sudo dnf upgrade src-tauri/target/release/bundle/rpm/*.rpm
xdg-mime default dev.vanjex.courvux-tauri-notepad.desktop text/markdownlinuxdeploy ships an old strip binary inside its AppImage that doesn't recognize the .relr.dyn section type emitted by glibc on Fedora 40+. The build crashes mid-bundle with unknown type [0x13] section .relr.dyn. Skip the strip step (the system libraries are already stripped):
NO_STRIP=true pnpm tauri:buildSame workaround applies to other rolling-release distros that ship a recent glibc (Arch, openSUSE Tumbleweed). Debian / Ubuntu typically ship an older glibc and don't hit this.
A Tauri build always produces artifacts for the host platform. To ship for Linux + macOS + Windows you need three machines (or three GitHub Actions runners) — there's no cross-compile story for the webview side. The Rust side cross-compiles fine, but the bundled webview can't.
If you want releases for all three from a single push, the standard pattern is a release.yml workflow with three matrix jobs (ubuntu-latest, macos-latest, windows-latest), each running pnpm tauri:build and uploading its artifact.
courvux-tauri-example/
├── package.json # Vite + frontend deps; Tauri CLI as devDep
├── vite.config.js # tailwindcss + courvuxPrecompile plugins
├── index.html # CSP meta + #app mount point
├── src/
│ ├── main.js # Courvux app — entire UI lives here
│ ├── pdf-export.js # jsPDF DOM walker + PdfBuilder
│ ├── markdown.js # marked + Prism + DOMPurify pipeline
│ ├── icons.js # Lucide → static SVG strings
│ ├── style.css # @import "tailwindcss" + print + markdown body
│ ├── tauri.js # invoke wrappers for every Rust command
│ └── assets/ # logo, etc.
├── src-tauri/
│ ├── Cargo.toml
│ ├── build.rs
│ ├── tauri.conf.json # productName, window, CSP, fileAssociations, bundle
│ ├── capabilities/default.json # core + dialog + opener scope
│ ├── icons/ # generated via `cargo tauri icon`
│ └── src/
│ ├── main.rs # `windows_subsystem` guard + delegates to lib
│ └── lib.rs # tauri commands + atomic file IO + menu + Builder::run
├── README.md
└── LICENSE
vite.config.js registers courvux/plugin/precompile ahead of any other transform. The plugin walks every .js module looking for template: properties whose value is a static string or template literal with no ${} interpolations. For each one, it extracts every Courvux template expression — {{ ... }}, :attr="...", @event="...", cv-X="..." — and rewrites them through the courvux-precompiler WASM module into JS arrow functions. The compiled functions are inserted as a sibling exprs: property on the same component config; the runtime checks this map before falling back to new Function.
For this app, the build report reads:
[courvux-precompile] processed 1 file(s), 134 expression(s) precompiled, 0 template(s) fell back to runtime new Function.
Zero fallbacks → the runtime never has to call new Function, and the strict script-src 'self' CSP holds.
Library notes (<app-data>/courvux-tauri-notepad/notes/<id>-<slug>.md):
$XDG_DATA_HOME/dev.vanjex.courvux-tauri-notepad/notes/
# typically:
~/.local/share/dev.vanjex.courvux-tauri-notepad/notes/
The footer of the sidebar shows the resolved storage path. Each note is one Markdown file; the slug suffix is derived from the title for human readability when you open the folder in another editor.
App config — same parent directory, config.json:
{ "notesDir": null, "autoSave": true, "recentProjects": [...] }notesDir overrides the default notes location; recentProjects is the most-recently-opened-folders list shown on the welcome screen.
Window state — <app-data>/courvux-tauri-notepad/window-state.json, managed by tauri-plugin-window-state.
Project mode files — wherever the user opened them. The app never copies project files to its own data dir; they stay in their original location.
app_data_dir() respects the bundle identifier from tauri.conf.json, so two installs of different bundles never share state.
MIT — see LICENSE.
