# Agent Guide: marstemedia This document is for AI agents working on this codebase. Read it before touching anything. --- ## What is this project? **marstemedia** is a cross-platform desktop (+ Android) news reader app built with: - **Tauri v2** as the native shell - **Yew** (Rust → WebAssembly) for the UI - **Rust** for all backend logic (networking, file I/O, cryptography) It has three features: 1. **Nostr feed** — anonymous browsing of decentralized social posts, filterable by hashtag 2. **RSS reader** — fetches and displays articles from a user-configurable list of RSS/Atom feeds 3. **AI news** — one-click tech news summary from Groq (Llama 3) or OpenRouter --- ## Repository Layout ``` marstemedia/ ├── src/ # Frontend (Yew/Wasm) │ ├── main.rs # Wasm bootstrap │ ├── app.rs # Router + top-level layout │ ├── navbar/ │ │ ├── mod.rs │ │ └── navbar.rs # Bottom navigation dock │ └── pages/ │ ├── mod.rs │ ├── home.rs # Nostr feed page │ ├── news.rs # RSS + AI news page │ └── greet.rs # Placeholder/demo page │ ├── src-tauri/ # Backend (native Rust) │ ├── Cargo.toml │ ├── tauri.conf.json # App config (window size, build commands) │ ├── capabilities/ │ │ └── default.json # Tauri capability permissions │ └── src/ │ ├── main.rs # Desktop executable entry │ ├── lib.rs # Tauri setup, command registration │ ├── home.rs # Nostr: fetch_nostr_posts command │ └── news.rs # RSS + AI: all news commands + NewsState │ └── easy-nostr/ # Local path-dependency (nostr-sdk wrapper) │ ├── Cargo.toml # Frontend workspace root ├── styles.css # All CSS for the app ├── index.html # Trunk entry point ├── Trunk.toml # Trunk (Wasm bundler) config ├── justfile # Dev shortcuts └── .gitea/workflows/ └── android.yaml # CI: builds Android APK ``` --- ## How to Add a New Page 1. Create `src/pages/yourpage.rs` as a Yew `#[function_component]`. 2. Export it in `src/pages/mod.rs`: `pub mod yourpage;` 3. Add a variant to the `Route` enum in `src/app.rs`: ```rust #[at("/yourpage")] YourPage, ``` 4. Add a match arm in the `switch` function in `src/app.rs`: ```rust Route::YourPage => html! { }, ``` 5. Add a `>` in `src/navbar/navbar.rs`. --- ## How to Add a New Backend Command 1. Write your async function in the appropriate module (`src-tauri/src/home.rs`, `news.rs`, or a new file). 2. Annotate it with `#[tauri::command]`. 3. If you need shared state, add a field to `NewsState` in `src-tauri/src/news.rs`, or create a new state struct and call `.manage()` in `lib.rs`. 4. Register the command in `src-tauri/src/lib.rs` inside `tauri::generate_handler![...]`. 5. In the frontend, call it with `invoke("your_command_name", args)`. --- ## How to Call a Backend Command from the Frontend Every page that needs backend calls declares this extern at the top: ```rust #[wasm_bindgen] extern "C" { #[wasm_bindgen(js_namespace = ["window", "__TAURI__", "core"], catch)] async fn invoke(cmd: &str, args: JsValue) -> Result; } ``` Serialize args with `serde_wasm_bindgen::to_value(...)`, deserialize results with `serde_wasm_bindgen::from_value(...)`. Use `spawn_local` to run async calls inside Yew callbacks. --- ## Key Patterns to Know ### State management (frontend) Yew uses `use_state` hooks. There is no global store — state lives inside components. If two pages need to share data, consider lifting state to `app.rs` or using Tauri's managed state on the backend. ### State management (backend) `NewsState` in `src-tauri/src/news.rs` is registered with `.manage()` and injected into commands via `State<'_, NewsState>`. All fields are `Mutex` — always lock, read/write, and drop the lock quickly. **API keys are in-memory only** (not saved to disk). ### Config persistence RSS feeds are saved to `/rss.ron` using the `ron` crate. Access the config path via `app.path().app_config_dir()`. Pattern is: read on load, write on save, keep in-memory state in sync. ### Markdown rendering The `News` page renders article content as Markdown using `pulldown-cmark`. The output is injected with `Html::from_html_unchecked`. Only use this for content you control or have sanitized. ### Privacy pattern (Nostr) A new random keypair is generated on every `fetch_nostr_posts` call. This is intentional — do not change this to a persistent key without understanding the privacy implications. --- ## CSS `styles.css` in the project root is the single entry point — it only contains `@import` statements. All actual styles are split into files under `assets/`: | File | What's inside | |---|---| | `assets/variables.css` | CSS custom properties (colors, fonts), reset, `body`/`html` | | `assets/layout.css` | `.feed-container`, responsive media queries | | `assets/navbar.css` | `.navbar-dock`, `.nav-link` | | `assets/header.css` | `.feed-header`, `.mode-toggle`, `.toggle-btn`, `.action-btn`, `.provider-switch` | | `assets/cards.css` | `.post-card`, `.markdown-body`, `.post-category-badge` | | `assets/config.css` | `.config-panel`, RSS list, inputs, `.add-btn`, `.delete-btn`, `.save-master-btn` | | `assets/filters.css` | Hashtag chips (Home), `.category-bar`, `.cat-btn` (News) | | `assets/animations.css` | `@keyframes float`, `slideIn`, `pulse-subtle` | | `assets/utils.css` | `.reload-btn-large`, `.error-box`, `.loading-spinner`, `.status-msg` | | `assets/greet.css` | `.robot-container`, `.robot-icon`, `.bubble` | When adding styles for a new page, create a new file in `assets/` and add an `@import` line to `styles.css`. There is no CSS framework — vanilla CSS with custom classes. --- ## Build & Dev ```bash # Start dev server (hot reload) cargo tauri dev # Build for desktop release cargo tauri build # Build for Android (requires Android SDK + NDK) cargo tauri android build # Frontend only (no Tauri shell) trunk serve ``` The `justfile` may contain additional shortcuts — check it first. --- ## Dependencies Worth Knowing | Crate | Used for | |---|---| | `yew` | UI components | | `yew-router` | Client-side routing | | `wasm-bindgen` / `wasm-bindgen-futures` | Wasm ↔ JS bridge | | `serde-wasm-bindgen` | Serialize/deserialize across the Tauri IPC | | `pulldown-cmark` | Markdown → HTML in the frontend | | `nostr-sdk` | Nostr protocol (via `easy-nostr` wrapper) | | `reqwest` | HTTP client (rustls, no OpenSSL) | | `feed-rs` | RSS/Atom feed parsing | | `ron` | Config file serialization | | `tauri-plugin-opener` | Open URLs in system browser | --- ## What to Watch Out For - **`easy-nostr`** is a local path dependency at `src-tauri/easy-nostr/`. It is also a private Git repo cloned by CI. If you need to update it, clone it manually into that path. - **No OpenSSL** — both `reqwest` usages (frontend and backend) use `rustls-tls` with `default-features = false`. Keep it that way to avoid cross-compilation pain on Android/CI. - **`wasm32-unknown-unknown`** target is required for the frontend. `aarch64-linux-android` and friends are required for Android CI. - The `Greet` page (`src/pages/greet.rs`) appears to be a leftover demo page from the Tauri template. It is still linked in the navbar — decide whether to keep or remove it. - API keys entered by the user are held in `Mutex` fields in `NewsState`. They are lost when the app restarts. There is no persistence for keys by design.