A native markdown viewer CLI built with Rust (wry/tao) and Svelte 5.
Use the Taskfile dev command for frontend/native UI development:
task devBy default, task dev auto-selects a free port (equivalent to DEV_PORT=auto).
This command starts Vite in web/, waits for it to be ready, then runs Rust with ATTN_DEV_SERVER_URL so the wry webview uses the Vite dev server with HMR.
Useful overrides:
task dev ATTN_PATH=tests/fixtures/basic.md
task dev DEV_PORT=5174
task dev DEV_PORT=auto
task dev DEV_HOST=0.0.0.0scripts/build.sh # debug (default) — automation + devtools enabled
scripts/build.sh release # release — automation + devtools stripped
scripts/build.sh prod # alias for releaseOr directly with cargo:
cargo build # debug (automation, devtools, screenshots, dev server)
cargo build --release # release (stripped — clean for distribution)Debug builds (debug_assertions on) include automation CLI flags (--screenshot, --eval, --click, --wait-for, --query, --fill), devtools, and dev server support. Release builds strip all of these automatically — no feature flags needed.
# Generate temporary placeholder icon (replace before final release)
scripts/generate-placeholder-icon.sh
# Build app bundle
scripts/macos-build-bundle.sh prod aarch64-apple-darwin
# Sign app bundle (requires APPLE_SIGNING_IDENTITY)
scripts/macos-sign-app.sh target/aarch64-apple-darwin/release/bundle/osx/attn.app
# Create signed DMG (if APPLE_SIGNING_IDENTITY is set)
scripts/macos-create-dmg.sh target/aarch64-apple-darwin/release/bundle/osx/attn.app
# Notarize + staple (requires APPLE_ID / APPLE_APP_SPECIFIC_PASSWORD / APPLE_TEAM_ID)
scripts/macos-notarize-dmg.sh target/aarch64-apple-darwin/release/bundle/osx/attn.dmgGitHub Action setup is documented in .github/RELEASE_SETUP.md.
To bump the version (e.g., to 0.3.6):
- Update
versioninCargo.tomlandpackage.json(root) - Run
cargo checkto updateCargo.lock - Commit:
git commit -m "Bump version to 0.3.6" - Tag:
git tag v0.3.6 - Push:
git push && git push origin v0.3.6
src/main.rs— CLI entry, daemon event loop, webview setupsrc/daemon.rs— Unix socket IPC, fork, single-instance protocolsrc/watcher.rs— File change detection via notifysrc/markdown.rs— Markdown rendering (comrak + syntect)src/ipc.rs— Webview IPC message handlingsrc/screenshot.rs— Native WKWebView screenshot (debug builds, macOS)web/— Svelte 5 frontend, built by Vite intoweb/dist/index.html(embedded at compile time). In dev,task devserves Vite directly for HMR.build.rs— Runs Vite build, recursively watchesweb/src/andweb/styles/for changesscripts/build.sh— Unified build script (web + Rust)scripts/test-e2e.sh— Automated E2E test runner
attn runs as a single-instance daemon. The first invocation forks to background and opens a window. Subsequent invocations connect via unix socket at ~/.attn/attn.sock.
Use task dev during development to keep the daemon in the foreground with HMR enabled:
task dev ATTN_PATH=path/to/file.mdIf you only need Rust-side iteration (no frontend HMR), you can still run:
cargo run -- --no-fork path/to/file.md# Structured interaction commands (preferred for E2E tests)
attn --click 'text=Submit' # click by text content
attn --click '.my-button' # click by CSS selector
attn --wait-for 'h1' # wait for element to appear (default 5s)
attn --wait-for 'h1' --timeout 10000 # custom timeout in ms
attn --query 'h1' # JSON: {status, count, elements[{tag, text, visible, attributes}]}
attn --query '[data-sidebar]' | jq '.count'
attn --fill 'input.search' 'hello' # fill a form field
# Evaluate JavaScript in the webview and print the result (escape hatch)
attn --eval "document.title"
attn --eval "document.querySelector('h1')?.textContent"
attn --eval "window.__attn__" # access the Svelte app bridge
# Get daemon info (binary path, PID, window ID)
attn --info
# Take a screenshot (macOS, debug builds only)
attn --screenshotSelectors support CSS selectors and a text= prefix for matching by element text content (like Playwright locators). Exit code 0 on success, 1 on not_found/timeout.
Run the automated E2E test suite:
scripts/test-e2e.shThis builds attn, launches it with test fixtures, asserts DOM state via --eval, and captures screenshots to /tmp/attn-e2e-screenshots/.
Test fixtures are in tests/fixtures/:
basic.md— headings, checkboxes, code block, table, blockquotetypography.md— all heading levels, nested lists, text formattingnested/child.md— subdirectory file for tree/breadcrumb testing
- Start the daemon with HMR:
task dev ATTN_PATH=some/file.md - In another terminal, use
--evalto inspect/interact with the webview:- Query DOM state:
cargo run -- --eval "document.querySelector('.task-list').children.length" - Trigger actions:
cargo run -- --eval "document.querySelector('input[type=checkbox]').click()" - Read app state:
cargo run -- --eval "JSON.stringify(window.__attn_init__)"
- Query DOM state:
- Use
--infoto get PID/window ID for external tooling - Use
--screenshotto capture visual state for comparison