Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
259 changes: 259 additions & 0 deletions .planning/codebase/ARCHITECTURE.md

Large diffs are not rendered by default.

178 changes: 178 additions & 0 deletions .planning/codebase/CONCERNS.md

Large diffs are not rendered by default.

223 changes: 223 additions & 0 deletions .planning/codebase/CONVENTIONS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,223 @@
# Coding Conventions

**Analysis Date:** 2026-05-20

## Naming Patterns

**Files:**
- Svelte components: PascalCase with `.svelte` extension — `Editor.svelte`, `TitleBar.svelte`, `FindBar.svelte`
- Svelte 5 rune stores: camelCase with `.svelte.ts` double extension — `tabs.svelte.ts`, `settings.svelte.ts`, `update.svelte.ts`
- Utility modules: camelCase with `.ts` extension — `markdown.ts`, `export.ts`, `theme.ts`, `i18n.ts`
- Routes: SvelteKit convention — `+page.svelte`, `+layout.ts`
- Rust files: snake_case — `lib.rs`, `main.rs`, `setup.rs`

**Functions:**
- TypeScript/Svelte: camelCase — `resolvePath`, `isYoutubeLink`, `processMarkdownHtml`, `exportAsHtml`
- Svelte event handlers in `$props`: lowercase with `on` prefix — `onsave`, `onnew`, `onopen`, `ontoggleEdit`, `onscrollsync`
- Rust: snake_case — `atomic_write`, `convert_markdown`, `process_wikilinks`, `process_internal_embeds`
- Tauri commands (Rust): snake_case — `read_file_content`, `save_file_content`, `open_markdown`

**Variables:**
- TypeScript: camelCase — `tabManager`, `updateStore`, `markdownBody`, `renderDebounceMs`
- Rust: snake_case — `watcher_lock`, `path_to_watch`, `app_handle`
- Constants: SCREAMING_SNAKE_CASE in both languages — `FIND_MARK_CLASS`, `MAX_MATCHES`, `DEBOUNCE_MS`, `APP_NAME`, `EXE_NAME`

**Types and Interfaces:**
- TypeScript interfaces: PascalCase — `Tab`, `ExportContext`, `Translation`, `DefaultFonts`
- TypeScript type aliases: PascalCase — `OSType`, `LanguageCode`, `UpdatePhase`, `ErrorSource`
- Rust structs: PascalCase — `WatcherState`, `AppState`, `InstallStatus`
- Union string literal types used for discriminated state — `UpdatePhase`, `ErrorSource`

**Classes (Svelte Stores):**
- Store classes are named with `Store` or `Manager` suffix — `TabManager`, `SettingsStore`, `UpdateStore`
- Exported as singleton `const` instances — `export const tabManager = new TabManager()`, `export const settings = new SettingsStore()`

## Code Style

**Formatting:**
- No `.prettierrc` or `biome.json` detected — no enforced formatter configured
- Indentation: tabs in `.svelte` files; the codebase mixes tabs and spaces in TypeScript (tabs are dominant)
- Svelte components use TypeScript strictly: `<script lang="ts">` on every component

**TypeScript strictness:**
- `strict: true` in `tsconfig.json` — all strict type checks enabled
- `allowJs: true` with `checkJs: true` — JavaScript files are also type-checked
- `skipLibCheck: true` — library declaration files are not checked
- `moduleResolution: "bundler"` — SvelteKit/Vite bundler resolution

**Linting:**
- No ESLint or Biome config detected — no enforced lint rules
- `svelte-check` is the only static analysis tool (runs TypeScript + Svelte checks)
- Run via: `npm run check` or `npm run check:watch`

## Import Organization

No enforced order, but observed pattern across files:

**Svelte components (`<script lang="ts">`):**
1. Svelte core imports — `onMount`, `onDestroy`, `tick`, `untrack`
2. Tauri API imports — `@tauri-apps/api/core`, `@tauri-apps/api/event`, plugins
3. Third-party library imports — `monaco-editor`, `dompurify`, `highlight.js`
4. Internal store imports — `../stores/tabs.svelte.js`, `../stores/settings.svelte.js`
5. Internal component imports — `./ContextMenu.svelte`, `../components/Editor.svelte`
6. Internal utility imports — `../utils/i18n.js`, `../utils/markdown`
7. CSS imports at end — `import 'highlight.js/styles/github-dark.css'`

Note: `MarkdownViewer.svelte` has inconsistent import ordering (some imports scattered mid-block), indicating this is not enforced.

**Path Aliases:**
- `$lib` alias maps to `src/lib/` (SvelteKit default)
- Relative imports use `.js` extension even for TypeScript sources (SvelteKit ESM requirement) — `import { t } from '../utils/i18n.js'`

## Svelte 5 Rune Patterns

The entire frontend uses **Svelte 5 runes** exclusively. No legacy `writable`/`readable` stores.

**Reactive state:**
```typescript
// In class-based stores (.svelte.ts files)
class TabManager {
tabs = $state<Tab[]>([]);
activeTabId = $state<string | null>(null);
}

// In Svelte components
let query = $state('');
let caseSensitive = $state(false);
```

**Side effects:**
```typescript
$effect(() => {
localStorage.setItem('editor.minimap', String(this.minimap));
// ... persist all settings on any reactive change
});

// Scoped effects in class constructors use $effect.root
$effect.root(() => {
$effect(() => { /* ... */ });
});
```

**Component props:**
```typescript
let {
value = $bindable(),
language = 'markdown',
onsave,
onnew,
} = $props<{
value: string;
language?: string;
onsave?: () => void;
onnew?: () => void;
}>();
```

**Bindable props:**
- `$bindable()` used for two-way binding: `value = $bindable()`, `zoomLevel = $bindable()`

## Tauri IPC Pattern

**Frontend calling Rust:**
```typescript
// Typed invoke with explicit cast
const result = await invoke<string>('get_os_type');

// With payload object (snake_case keys match Rust param names)
await invoke('save_file_content', { path: selected, content: fullHtml });

// Fire-and-forget with error suppression
invoke('watch_file', { path: filePath }).catch(console.error);
```

**Rust command declaration:**
```rust
#[tauri::command]
fn save_file_content(path: String, content: String) -> Result<(), String> {
atomic_write(Path::new(&path), content.as_bytes()).map_err(|e| e.to_string())
}
```

All Tauri commands return `Result<T, String>` — errors are always stringified with `.map_err(|e| e.to_string())`.

## Error Handling

**Rust:**
- Tauri commands consistently return `Result<T, String>` with `.map_err(|e| e.to_string())`
- Internal helper functions use `std::io::Result<T>`
- `unwrap()` used on static regex patterns (intentional panic on bad literal regex)
- `unwrap_or` and `unwrap_or_else` used for optional defaults, not panics
- Platform-specific no-op stubs for non-Windows commands return `Ok(())` directly

**TypeScript/Svelte:**
- Async functions wrapped in `try/catch` (49 try blocks across the codebase)
- Errors logged via `console.error(...)` — no structured error reporting
- Non-critical failures use `.catch(console.error)` chained inline
- `invoke()` calls in event handlers typically use `.catch(console.error)` pattern
- Store operations with critical data use full try/catch blocks

## Logging

**Rust:**
- `println!()` used directly throughout `setup.rs` for install progress — no structured logger (`env_logger` is a dependency but not actively used in code paths)
- Debug output uses `println!("Setup Args: {:?}", args)` format

**Frontend:**
- `console.error()` used exclusively — no `console.log()` or `console.warn()` in production paths
- Errors always include a descriptive message: `console.error('Failed to load vscode themes:', e)`

## Comments

**When to comment:**
- Complex algorithmic logic gets block comments — `atomic_write` has an extensive header explaining all correctness guarantees
- Non-obvious `#[cfg(...)]` branches are explained inline
- Workarounds and edge cases documented at point of implementation

**JSDoc/TSDoc:**
- Not used — no `@param`, `@returns`, or `@type` annotations in TypeScript code

**Rust doc comments:**
- Block-level `///` doc comments used on `atomic_write` and complex private functions
- Inline `//` comments used for step-by-step logic within functions

## Function Design

**Size:**
- Svelte components handle UI logic inline (Editor.svelte is 1,228 lines, TitleBar.svelte 1,392 lines)
- Utility functions in `src/lib/utils/` are focused and small
- Rust Tauri command handlers are thin wrappers delegating to helpers

**Parameters:**
- Rust commands accept primitive types (`String`, `bool`, `Vec<u8>`) that serialize over IPC
- TypeScript context objects used for multi-param utility functions: `ExportContext` in `export.ts`

**Return Values:**
- Rust: `Result<T, String>` for fallible commands; primitive types for infallible ones
- TypeScript: `Promise<void>` for side-effect functions; typed return for data functions

## Module Design

**Exports:**
- Named exports throughout — no default exports except SvelteKit config files
- Singleton instances exported as `const`: `export const tabManager`, `export const settings`, `export const updateStore`
- Types/interfaces exported alongside their owning module (no separate `types.ts`)

**Barrel Files:**
- None — no `index.ts` files; every import uses a direct path

## Platform-Specific Code

**Rust:**
- `#[cfg(target_os = "windows")]` / `#[cfg(not(target_os = "windows"))]` used extensively for Windows-only install logic — 23 occurrences
- Non-Windows stubs always return `Ok(())` immediately
- `#[cfg(unix)]` used in `atomic_write` for directory fsync after rename
- `#[cfg(target_os = "macos")]` for macOS window decoration and menu setup

**TypeScript:**
- `OSType` union (`'macos' | 'windows' | 'linux' | 'unknown'`) from `settings.svelte.ts` used for platform branching
- Per-platform font defaults in `DEFAULT_FONTS` constant

---

*Convention analysis: 2026-05-20*
166 changes: 166 additions & 0 deletions .planning/codebase/INTEGRATIONS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
# External Integrations

**Analysis Date:** 2026-05-20

## APIs & External Services

**VS Code Marketplace (VSIX download):**
- Purpose: Fetch VS Code color themes by URL from vscodethemes.com, download and unzip `.vsix` packages
- Endpoint: `https://{publisher}.gallery.vsassets.io/_apis/public/gallery/publisher/{publisher}/extension/{extension}/latest/assetbyname/Microsoft.VisualStudio.Services.VSIXPackage`
- Rust command: `fetch_vscode_theme` in `src-tauri/src/lib.rs:433`
- HTTP client: `reqwest 0.12` (async GET, no auth)
- Parsing: `zip` crate extracts `extension/package.json` and theme JSON from `.vsix`

**GitHub Releases (auto-updater):**
- Purpose: In-app update checks and downloads
- Endpoint: `https://github.com/alecdotdev/Markpad/releases/latest/download/latest.json`
- Configured in: `src-tauri/tauri.conf.json` under `plugins.updater.endpoints`
- Plugin: `tauri-plugin-updater ^2` (JS: `@tauri-apps/plugin-updater`)
- Auth: Update packages verified by minisign public key (`pubkey` in `tauri.conf.json`)
- Update feed (`latest.json`) is generated and uploaded to GitHub Releases by `.github/workflows/build.yml`

**Google Fonts:**
- Purpose: Font loading in preview/editor CSS
- CSP: `style-src` allows `https://fonts.googleapis.com`, `font-src` allows `https://fonts.gstatic.com`
- Implementation: CSS `@import` in stylesheets — no JS API calls

**YouTube:**
- Purpose: Auto-embed YouTube links found in Markdown image/anchor tags
- URLs matched: `youtube.com/watch`, `youtu.be/`
- Rendered as: `<iframe src="https://www.youtube.com/embed/{id}">` via `src/lib/utils/markdown.ts:replaceWithYoutubeEmbed`
- CSP `frame-src` permits: `https://www.youtube.com https://www.youtube-nocookie.com`

## Data Storage

**Databases:**
- None. No SQL or NoSQL database.

**File Storage — Markdown/Text Files:**
- All document content stored as files on the local filesystem
- Read via Tauri command `read_file_content` (`src-tauri/src/lib.rs:313`)
- Written atomically via `save_file_content` / `save_file_binary` using `atomic_write()` (`src-tauri/src/lib.rs:28`) — write-to-temp-then-rename with `fsync`
- File rename: `rename_file` command (`src-tauri/src/lib.rs:333`)
- File delete: `delete_file` command (`src-tauri/src/lib.rs:787`)
- File copy: `copy_file` / `copy_file_to_img` commands (`src-tauri/src/lib.rs:796`, `749`)
- Directory listing: `list_directory_contents` command (`src-tauri/src/lib.rs:816`)
- Image saving: `save_image` decodes base64 and writes to a sibling image directory (`src-tauri/src/lib.rs:717`)
- Image directory cleanup: `cleanup_empty_img_dir` (`src-tauri/src/lib.rs:801`)

**App Configuration (Tauri config dir):**
- Theme preference: `{app_config_dir}/theme.txt` — plain text (`"light"`, `"dark"`, `"system"`)
- Downloaded VS Code themes: `{app_config_dir}/themes/{name}.json`
- Config dir resolved via `app.path().app_config_dir()` in `src-tauri/src/lib.rs`
- Platform locations:
- Windows: `%APPDATA%\com.alecdotdev.markpad\`
- macOS: `~/Library/Application Support/com.alecdotdev.markpad/`
- Linux: `~/.config/com.alecdotdev.markpad/`

**Editor Settings (WebView localStorage):**
- Stored in WebView's `localStorage` (key prefix `editor.*`)
- Persisted keys include: `editor.minimap`, `editor.wordWrap`, `editor.lineNumbers`, `editor.vimMode`, `editor.statusBar`, `editor.wordCount`, `editor.renderLineHighlight`, `editor.showTabs`, `editor.restoreStateOnReopen`, `editor.zenMode`, `editor.highlightColor`, `editor.splitScrollSync`, and others
- Managed by: `src/lib/stores/settings.svelte.ts` and `src/lib/stores/tabs.svelte.ts`

**Window State:**
- Persisted automatically by `tauri-plugin-window-state` (size, position, maximized, visible, fullscreen)
- Stored in Tauri's app data directory by the plugin

**Caching:**
- None

## Authentication & Identity

**Auth Provider:**
- None. No user authentication. Fully offline-capable app.

## OS / Native Integrations

**Clipboard:**
- Text read/write: `arboard 3` crate via commands `clipboard_read_text`, `clipboard_write_text` (`src-tauri/src/lib.rs:622-631`)
- Image read: `clipboard_read_image` encodes clipboard image as base64 PNG; macOS Retina images are downscaled by 2x using Lanczos3 (`src-tauri/src/lib.rs:634`)
- Also available via Tauri plugin: `@tauri-apps/plugin-clipboard-manager` (JS side)

**Filesystem Watcher:**
- Library: `notify 6` (`RecommendedWatcher`)
- Used to detect external file changes while a file is open in the editor
- Commands: `watch_file` / `unwatch_file` (`src-tauri/src/lib.rs:338`, `370`)
- Emits Tauri event `"file-changed"` to the webview on any filesystem event for the watched path

**File Open/Reveal:**
- `opener::reveal(path)` opens the containing folder in the OS file manager (`open_file_folder`, `src-tauri/src/lib.rs:328`)
- `@tauri-apps/plugin-opener` opens URLs in the default browser and files with their default OS handler

**Native File Dialogs:**
- `@tauri-apps/plugin-dialog` (`save`, `open`) — used in `src/lib/utils/export.ts` for HTML/PDF export save dialogs

**System Fonts:**
- `font-kit 0.14` enumerates all OS font families
- Command `get_system_fonts` returns a sorted deduplicated list (`src-tauri/src/lib.rs:591`)
- Used in Settings UI for the editor font picker

**OS Detection:**
- `get_os_type` returns `"macos"` | `"windows"` | `"linux"` via `#[cfg]` attributes (`src-tauri/src/lib.rs:601`)
- `is_win11` reads Windows Registry `CurrentBuild` value to check build >= 22000 (`src-tauri/src/lib.rs:570`)

**Window Management:**
- Tauri-managed frameless/native window; macOS uses `TitleBarStyle::Overlay` with hidden title
- Window background color set from `theme.txt` preference before first paint (avoids white flash)
- macOS native menu bar built in `setup()` with File, Edit, Window, App submenus; menu events forwarded as Tauri events to the webview

**Single Instance:**
- `tauri-plugin-single-instance` ensures only one app process runs at a time
- If a second instance is launched with a file argument, the path is forwarded to the running window via `"file-path"` event and the window is focused

**macOS URL handler:**
- `RunEvent::Opened { urls }` handles macOS "Open With" / drag-to-dock events
- Emits `"file-path"` event to the webview with the resolved path (`src-tauri/src/lib.rs:1159`)

## Windows-Specific OS Integrations

**Installer / Uninstaller (self-contained — no external installer framework required):**
- Copies executable to `%LOCALAPPDATA%\Markpad\` (per-user) or `%ProgramFiles%\Markpad\` (all-users) — `src-tauri/src/setup.rs:159`
- Creates `.lnk` shortcuts on Desktop and Start Menu via `mslnk 0.1`
- Writes uninstall registry keys under `HKCU/HKLM\Software\Microsoft\Windows\CurrentVersion\Uninstall\Markpad`
- Registers `.md` and `.markdown` file associations in Registry (`Software\Classes\.md`, `Software\Classes\Markpad.File`) — `src-tauri/src/setup.rs:428`
- Self-uninstall via generated `.bat` + VBScript (`wscript`) to allow deleting the running EXE (`src-tauri/src/setup.rs:370`)

**NSIS Installer:**
- Also provided as a standard NSIS `.exe` installer (generated by Tauri's bundle system); `src-tauri/tauri.conf.json` references `hooks.nsi`

## Monitoring & Observability

**Error Tracking:**
- None (no Sentry, Bugsnag, etc.)

**Logs:**
- Rust: `env_logger 0.11.8` / `log 0.4.29` crates; `println!` used for debug output in setup/install flows
- Frontend: `console.error` for rendering failures (Mermaid, KaTeX, image resolution)

## CI/CD & Deployment

**Hosting:**
- GitHub Releases — primary distribution for all platform binaries
- Chocolatey (`push.chocolatey.org`) — Windows package registry (published in CI)
- Snap Store — Linux snap package (published in CI via `snapcraft`)

**CI Pipeline:**
- GitHub Actions — `.github/workflows/build.yml` (manual trigger, builds all platforms in matrix)
- Test workflow — `.github/workflows/test.yml` (runs on PR: `svelte-check` + `cargo test`)
- Update feed workflow — generates `latest.json` from `.sig` artifacts and uploads to the GitHub Release

**Required CI Secrets:**
- `TAURI_SIGNING_PRIVATE_KEY` — minisign private key for signing update artifacts
- `TAURI_SIGNING_PRIVATE_KEY_PASSWORD` — password for the signing key
- `CHOCO_API_KEY` — Chocolatey push API key
- `SNAPCRAFT_STORE_CREDENTIALS` — Snap Store credentials

## Webhooks & Callbacks

**Incoming:**
- None

**Outgoing:**
- Tauri updater polls `https://github.com/alecdotdev/Markpad/releases/latest/download/latest.json` on user-initiated update check

---

*Integration audit: 2026-05-20*
Loading