diff --git a/.gitignore b/.gitignore index 66fb657..8e9699f 100644 --- a/.gitignore +++ b/.gitignore @@ -41,5 +41,8 @@ data/projects/*/ # === Stray root lock file (frontend/package-lock.json is kept for CI) === /package-lock.json +# === Local build output === +release/ + # === Generated images (article assets, not source) === /assets/ diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..0f5d022 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,39 @@ +# Changelog + +All notable changes to Ironpad are documented here. + +## [0.2.0] - 2026-02-16 + +### Added +- **Task comments** -- date-stamped comment entries per task, stored as YAML in frontmatter. Last comment shown as summary in task list and dashboard cards. Add/delete via API and UI. +- **Recurring tasks on calendar** -- tasks with daily/weekly/monthly/yearly recurrence now appear on the calendar even without an explicit `due_date`. Occurrences are computed from the anchor date (`due_date` or `created`) and `recurrence_interval`. Recurring entries show with a dashed border and recurrence icon to distinguish from regular due-date tasks. +- **System tray mode** -- production binary runs in the system tray instead of a console window. Tray menu with "Open in Browser" and "Quit". No CMD window on Windows in release builds (`windows_subsystem = "windows"`). Server runs on a background thread with the tray event loop on the main thread for cross-platform safety. +- **App icon and branding** -- Ironpad logo embedded in the Windows executable (Explorer icon + tray icon) via `winresource`. Favicon and logo added to the web UI (browser tab + top bar header). +- **Local build script** -- `build-local.ps1` for building a testable release package locally. + +### Changed +- Backend `main.rs` restructured for dual-mode operation: development mode runs the server directly (no tray), production mode runs server on background thread with tray on main thread. +- Calendar view refactored to use `CalendarEntry` interface that merges regular due-date tasks with computed recurring occurrences. + +### Dependencies +- Added `tray-item = "0.10"` for cross-platform system tray support. +- Added `windows-sys = "0.52"` (Windows only) for loading the embedded icon resource. +- Added `winresource = "0.1"` (Windows build dependency) for embedding the icon in the .exe. + +--- + +## [0.1.0] - 2025-12-01 + +### Added +- Initial release of Ironpad -- local-first, file-based project and knowledge management. +- **Backend**: Rust/Axum API server with dynamic port (3000-3010), WebSocket real-time sync, file watcher, Git auto-commit (60s batching), ripgrep search. +- **Frontend**: Vue 3 SPA with Milkdown WYSIWYG editor, dark/light theme, Pinia state management. +- **File-based tasks**: each task stored as a markdown file with YAML frontmatter (title, completed, section, priority, due_date, tags, subtasks, recurrence). +- **Split-panel task view**: task list with active/backlog/completed sections, detail editor with markdown, due date picker, tag system, subtasks, recurrence picker. +- **Calendar view**: month grid showing tasks by due date with color-coded urgency and daily note indicators. +- **Dashboard**: cross-project home page with active task summaries per project. +- **Daily notes**: date-based notes with templates. +- **Git panel**: commit history with diffs, working directory changes, push/fetch with ahead/behind indicators. +- **Project notes**: split-panel notes view per project. +- **Search**: Ctrl+K search panel with ripgrep-powered full-text search. +- Cross-platform builds (Windows, macOS, Linux) via GitHub Actions. diff --git a/README.md b/README.md index b0f8565..c09143d 100644 --- a/README.md +++ b/README.md @@ -8,13 +8,13 @@ ![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg) ![Platform](https://img.shields.io/badge/platform-Windows%20%7C%20macOS%20%7C%20Linux-lightgrey) ![Rust](https://img.shields.io/badge/rust-1.70%2B-orange) -![Version](https://img.shields.io/badge/version-0.1.0-green) +![Version](https://img.shields.io/badge/version-0.2.0-green) Ironpad stores all your notes, projects, and tasks as plain Markdown files. No cloud services, no vendor lock-in -- your data stays on your machine in a format you can read and edit with any text editor. Every change is automatically versioned with Git. ![Ironpad Screenshot](docs/screenshot.jpg) -> **v0.1.0 -- Early Release.** This is the first public release. It's functional and we use it daily, but expect rough edges. Bug reports and feature requests are welcome via [Issues](https://github.com/OlaProeis/ironPad/issues). +> **v0.2.0** -- Task comments, recurring tasks on calendar, system tray mode, and app branding. See [CHANGELOG.md](CHANGELOG.md) for details. --- @@ -24,14 +24,15 @@ Ironpad stores all your notes, projects, and tasks as plain Markdown files. No c - **Local-first** -- Works fully offline, no internet required - **Git integration** -- Automatic version control with 60-second commit batching, full diff viewer, push/fetch - **WYSIWYG editing** -- Milkdown editor with real-time markdown rendering and formatting toolbar -- **Project management** -- Organize tasks and notes by project with due dates, tags, subtasks, and recurrence -- **Calendar view** -- Month grid showing tasks by due date with color-coded urgency +- **Project management** -- Organize tasks and notes by project with due dates, tags, subtasks, recurrence, and comments +- **Calendar view** -- Month grid showing tasks by due date with color-coded urgency; recurring tasks automatically expanded across the month - **Dashboard** -- Cross-project overview with active task summaries - **Daily notes** -- Quick capture with templates for daily journaling - **Real-time sync** -- WebSocket-based live updates; edit in VS Code, see changes in the browser instantly - **External editing** -- Full support for VS Code, Obsidian, Vim, or any text editor - **Search** -- ripgrep-powered full-text search across all files (Ctrl+K) - **Dark theme** -- Beautiful dark UI by default with light mode toggle +- **System tray** -- Runs quietly in the system tray (Windows, macOS, Linux); no console window in release builds - **Tiny footprint** -- 5 MB binary, ~20 MB RAM, sub-second startup ## Quick Start @@ -86,6 +87,9 @@ Open http://localhost:5173 in your browser. Ironpad is under active development. Here's what's planned: +- [x] Task comments and activity summary +- [x] Recurring tasks on calendar (daily/weekly/monthly/yearly expansion) +- [x] System tray mode (Windows, macOS, Linux) - [ ] UI polish and animations - [ ] Tag extraction and filtering across projects - [ ] Backlinks between notes @@ -93,7 +97,6 @@ Ironpad is under active development. Here's what's planned: - [ ] Export to PDF / HTML - [ ] Custom themes - [ ] Global hotkey (Ctrl+Shift+Space) -- [ ] System tray mode - [ ] Kanban board view for tasks See [CHECKLIST.md](docs/ai-workflow/CHECKLIST.md) for detailed implementation status. diff --git a/ROADMAP.md b/ROADMAP.md index 95b2569..12ee624 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -1,28 +1,30 @@ # Ironpad Roadmap -## Release 0.2.0 (Next) +## Release 0.2.0 (Current) -### Planned Features +### Features #### 1. Task comments & activity summary - **Comment section** per task with date-stamped entries -- Store comments (e.g. in task file as YAML sequence or dedicated section) -- **Last comment as summary** — show the most recent comment/activity in the main task list and dashboard to indicate last action or status -- Enables quick status updates without editing the full description +- Store comments as YAML sequence in task frontmatter +- **Last comment as summary** -- most recent comment shown in task list and dashboard cards +- Add/delete comments via API and UI, newest-first display with relative timestamps #### 2. Recurring tasks on the calendar -- **Bug/feature gap**: Tasks with daily/weekly recurrence but no explicit `due_date` currently do not appear on the calendar (calendar only shows tasks with `task.due_date`) -- **Change**: Expand recurring tasks into the calendar for the visible month: - - **Daily** — show on every day in the month (or cap at reasonable limit) - - **Weekly** — show on the matching weekday(s) in the month - - **Monthly** — show on the day-of-month if set, else treat as “floating” -- Requires frontend logic to compute occurrences from `recurrence`, `recurrence_interval`, and optionally `due_date` / `created` +- Tasks with daily/weekly recurrence now appear on the calendar (previously required explicit `due_date`) +- Recurring tasks expanded into the visible month grid (daily/weekly/monthly/yearly) +- Anchor date: `due_date` if set, otherwise `created`; respects `recurrence_interval` +- Recurring occurrences shown with dashed border and recurrence icon #### 3. System tray mode -- **Replace CMD window** with a system tray icon (Windows, macOS, Linux) +- System tray icon replaces CMD window (Windows, macOS, Linux) - Tray menu: **Open in Browser** | **Quit** - No console window on Windows in release builds -- Implementation doc: [docs/system-tray-implementation.md](docs/system-tray-implementation.md) +- Server runs on background thread; tray event loop on main thread (cross-platform safe) + +#### 4. App branding +- Ironpad logo as system tray icon and Windows exe icon +- Favicon and logo in the web UI (browser tab + header) --- @@ -31,18 +33,18 @@ Ideas that fit the current architecture and local-first design: ### High fit (0.3.x) -- **Calendar drag-and-drop** — reschedule tasks by dragging onto a new date (already listed in ai-context) -- **Week / day calendar views** — alternative to month view for denser task planning -- **Sort task list by due date / priority** — alongside current created-date sorting -- **Overdue indicator** — clearer overdue badge or count in sidebar and dashboard +- **Calendar drag-and-drop** -- reschedule tasks by dragging onto a new date +- **Week / day calendar views** -- alternative to month view for denser task planning +- **Sort task list by due date / priority** -- alongside current created-date sorting +- **Overdue indicator** -- clearer overdue badge or count in sidebar and dashboard ### Medium fit (0.4.x) -- **Quick-add task** — global or dashboard shortcut to create a task without opening a project -- **Bulk actions** — complete multiple tasks, move section, add/remove tags in one go -- **Task templates** — create tasks from predefined templates (e.g. “Meeting prep”, “Review”) -- **Tag extraction and cross-project filtering** — surface and filter by tags across all projects +- **Quick-add task** -- global or dashboard shortcut to create a task without opening a project +- **Bulk actions** -- complete multiple tasks, move section, add/remove tags in one go +- **Task templates** -- create tasks from predefined templates (e.g. "Meeting prep", "Review") +- **Tag extraction and cross-project filtering** -- surface and filter by tags across all projects -### Longer term (Phase 6+) +### Longer term - UI polish and subtle animations - Responsive sidebar / mobile-friendly layout - Global hotkey (e.g. Ctrl+Shift+Space) @@ -56,7 +58,7 @@ Ideas that fit the current architecture and local-first design: ## Version history -| Version | Status | Notes | -|---------|---------|----------------------------------------------------| -| 0.1.0 | Current | First public release, core features in place | -| 0.2.0 | Planned | Comments, recurring tasks on calendar, system tray | +| Version | Status | Date | Notes | +|---------|----------|------------|----------------------------------------------------------| +| 0.1.0 | Released | 2025-12-01 | First public release, core features in place | +| 0.2.0 | Current | 2026-02-16 | Comments, recurring calendar, system tray, app branding | diff --git a/ai-context.md b/ai-context.md index 21418ac..935eb8e 100644 --- a/ai-context.md +++ b/ai-context.md @@ -67,7 +67,7 @@ ironpad/ ## Implemented Features ### Backend -- API-only server (no frontend serving, no browser auto-open) +- **Dual-mode server**: API-only in development; frontend-serving + system tray in production - Dynamic port (3000-3010) - Notes CRUD with atomic writes - Frontmatter auto-management @@ -79,12 +79,13 @@ ironpad/ - Git remote info (ahead/behind tracking), fetch support - Projects API with notes management - **File-based Tasks API** — each task is a markdown file with frontmatter - - Fields: id, title, completed, section, priority, due_date, is_active, tags, parent_id, recurrence, recurrence_interval + - Fields: id, title, completed, section, priority, due_date, is_active, tags, parent_id, recurrence, recurrence_interval, comments - Rich text descriptions with markdown support - Sorted by created date (stable ordering) - **Subtasks** — tasks with `parent_id` link to a parent task - **Tags** — YAML sequence in frontmatter, per-task labels for filtering - **Recurring tasks** — when completing a recurring task, auto-creates next instance with advanced due date + - **Comments** — date-stamped comment entries stored as YAML sequence in frontmatter; last comment shown as summary in list/dashboard - Daily notes API (`/api/daily`, `/api/daily/today`, `/api/daily/:date`) - Assets API (upload + serve) @@ -113,9 +114,14 @@ ironpad/ - **Tag filter bar** — click tags to filter task list - **Subtasks** — expandable subtasks under parent tasks, add subtask inline - **Recurrence picker** — set daily/weekly/monthly/yearly recurrence + - **Task comments** — date-stamped comments section in task detail, newest first, add/delete + - **Last comment summary** — most recent comment shown in task list and dashboard cards - Inline title editing (double-click) -- **Calendar view** — month grid showing tasks by due date +- **Calendar view** — month grid showing tasks by due date + recurring task expansion - Tasks with due dates plotted on calendar cells + - **Recurring tasks expanded** — daily/weekly/monthly/yearly tasks shown on computed occurrences + - Recurring occurrences use anchor date (`due_date` or `created`), respect `recurrence_interval` + - Recurring entries shown with dashed border and ↻ icon to distinguish from regular tasks - Daily notes shown as blue dots - Color-coded urgency (overdue, today, soon) - Month navigation + Today button @@ -150,6 +156,8 @@ GET/POST /api/projects/:id/tasks GET/PUT/DEL /api/projects/:id/tasks/:task_id PUT /api/projects/:id/tasks/:task_id/toggle PUT /api/projects/:id/tasks/:task_id/meta +POST /api/projects/:id/tasks/:task_id/comments +DELETE /api/projects/:id/tasks/:task_id/comments/:index GET /api/tasks # All tasks across projects GET /api/daily @@ -224,12 +232,25 @@ WS /ws --- -## Not Yet Implemented (Phase 6+) +## Implemented in Phase 6 + +- **Recurring tasks on calendar** — frontend expands daily/weekly/monthly/yearly tasks into the visible month grid + - Anchor date: `due_date` if set, otherwise `created`; respects `recurrence_interval` + - Deduplicates against regular due-date entries; visual indicator (dashed border, ↻ icon) +- **System tray mode** — production binary runs in system tray (Windows, macOS, Linux) + - `#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]` hides console on Windows + - Server on background thread, tray event loop on main thread (cross-platform safe) + - Tray menu: "Open in Browser" / "Quit" + - Uses `tray-item` crate with platform-specific icon loading (`windows-sys` on Windows) + - Development mode unchanged (no tray, API-only) + +--- + +## Not Yet Implemented (Phase 7+) - UI polish and animations - Responsive sidebar - Global hotkey (Ctrl+Shift+Space) -- System tray mode - Backlinks between notes - Graph view - Export (PDF / HTML) @@ -262,12 +283,14 @@ WS /ws | Project note ID | `{slug}-index` format | | Task storage | Individual .md files in `tasks/` folder | | List sorting | By created date (stable, not affected by edits) | -| Backend mode | API-only (no frontend serving) | +| Backend mode | API-only (dev); frontend-serving + system tray (production) | | Theme | Dark by default, toggle to light, persists to localStorage | | Tags | YAML sequence in frontmatter, project-scoped filtering | | Subtasks | Separate task files with `parent_id` field linking to parent | | Recurring tasks | On completion, backend auto-creates next instance with advanced due date | -| Calendar | Pure frontend month grid, tasks filtered by `due_date` presence | +| Calendar | Pure frontend month grid, tasks by `due_date` + recurring expansion | +| System tray | `tray-item` crate; main thread tray, background thread server; `windows-sys` for icon on Windows | +| Task comments | YAML sequence in frontmatter, date-stamped, last comment as list/dashboard summary | | Dashboard | Home route `/`, loads all projects + all tasks for cross-project summary | --- diff --git a/backend/Cargo.lock b/backend/Cargo.lock index dd243d1..75e0a71 100644 --- a/backend/Cargo.lock +++ b/backend/Cargo.lock @@ -20,6 +20,12 @@ dependencies = [ "libc", ] +[[package]] +name = "anyhow" +version = "1.0.101" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f0e0fee31ef5ed1ba1316088939cea399010ed7731dba877ed44aeb407a75ea" + [[package]] name = "atomic-waker" version = "1.1.2" @@ -102,9 +108,15 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" -version = "2.10.0" +version = "2.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3" +checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af" + +[[package]] +name = "block" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d8c1fef690941d3e7788d328517591fecc684c084084702d6ff1641e993699a" [[package]] name = "block-buffer" @@ -134,15 +146,15 @@ checksum = "5dd9dc738b7a8311c7ade152424974d8115f2cdad61e8dab8dac9f2362298510" [[package]] name = "bytes" -version = "1.11.0" +version = "1.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b35204fbdc0b3f4446b89fc1ac2cf84a8a68971995d0bf2e925ec7cd960f9cb3" +checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" [[package]] name = "cc" -version = "1.2.55" +version = "1.2.56" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "47b26a0954ae34af09b50f0de26458fa95369a0d478d8236d3f93082b219bd29" +checksum = "aebf35691d1bfb0ac386a69bac2fde4dd276fb618cf8bf4f5318fe285e821bb2" dependencies = [ "find-msvc-tools", "jobserver", @@ -176,6 +188,36 @@ dependencies = [ "windows-link", ] +[[package]] +name = "cocoa" +version = "0.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6140449f97a6e97f9511815c5632d84c8aacf8ac271ad77c559218161a1373c" +dependencies = [ + "bitflags 1.3.2", + "block", + "cocoa-foundation", + "core-foundation 0.9.4", + "core-graphics", + "foreign-types", + "libc", + "objc", +] + +[[package]] +name = "cocoa-foundation" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c6234cbb2e4c785b456c0644748b1ac416dd045799740356f8363dfe00c93f7" +dependencies = [ + "bitflags 1.3.2", + "block", + "core-foundation 0.9.4", + "core-graphics-types", + "libc", + "objc", +] + [[package]] name = "combine" version = "4.6.7" @@ -186,6 +228,16 @@ dependencies = [ "memchr", ] +[[package]] +name = "core-foundation" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "core-foundation" version = "0.10.1" @@ -202,6 +254,30 @@ version = "0.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" +[[package]] +name = "core-graphics" +version = "0.23.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c07782be35f9e1140080c6b96f0d44b739e2278479f64e02fdab4e32dfd8b081" +dependencies = [ + "bitflags 1.3.2", + "core-foundation 0.9.4", + "core-graphics-types", + "foreign-types", + "libc", +] + +[[package]] +name = "core-graphics-types" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "45390e6114f68f718cc7a830514a96f903cccd70d02a8f6d9f643ac4ba45afaf" +dependencies = [ + "bitflags 1.3.2", + "core-foundation 0.9.4", + "libc", +] + [[package]] name = "cpufeatures" version = "0.2.17" @@ -323,6 +399,39 @@ version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" +[[package]] +name = "foldhash" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" + +[[package]] +name = "foreign-types" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d737d9aa519fb7b749cbc3b962edcf310a8dd1f4b67c91c4f83975dbdd17d965" +dependencies = [ + "foreign-types-macros", + "foreign-types-shared", +] + +[[package]] +name = "foreign-types-macros" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a5c6c585bc94aaf2c7b51dd4c2ba22680844aba4c687be581871a6f518c5742" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "foreign-types-shared" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa9a19cbb55df58761df49b23516a86d432839add4af60fc256da840f66ed35b" + [[package]] name = "form_urlencoded" version = "1.2.2" @@ -343,24 +452,24 @@ dependencies = [ [[package]] name = "futures-channel" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" +checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d" dependencies = [ "futures-core", ] [[package]] name = "futures-core" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" +checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" [[package]] name = "futures-macro" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" +checksum = "e835b70203e41293343137df5c0664546da5745f82ec9b84d40be8336958447b" dependencies = [ "proc-macro2", "quote", @@ -369,28 +478,27 @@ dependencies = [ [[package]] name = "futures-sink" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" +checksum = "c39754e157331b013978ec91992bde1ac089843443c49cbc7f46150b0fad0893" [[package]] name = "futures-task" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" +checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" [[package]] name = "futures-util" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" +checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" dependencies = [ "futures-core", "futures-macro", "futures-sink", "futures-task", "pin-project-lite", - "pin-utils", "slab", ] @@ -416,13 +524,26 @@ dependencies = [ "wasip2", ] +[[package]] +name = "getrandom" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "139ef39800118c7683f2fd3c98c1b23c09ae076556b435f8e9064ae108aaeeec" +dependencies = [ + "cfg-if", + "libc", + "r-efi", + "wasip2", + "wasip3", +] + [[package]] name = "git2" version = "0.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b903b73e45dc0c6c596f2d37eccece7c1c8bb6e4407b001096387c63d0d93724" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.0", "libc", "libgit2-sys", "log", @@ -523,12 +644,27 @@ dependencies = [ "memmap2", ] +[[package]] +name = "hashbrown" +version = "0.15.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" +dependencies = [ + "foldhash", +] + [[package]] name = "hashbrown" version = "0.16.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + [[package]] name = "http" version = "1.4.0" @@ -603,12 +739,11 @@ dependencies = [ [[package]] name = "hyper-util" -version = "0.1.19" +version = "0.1.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "727805d60e7938b76b826a6ef209eb70eaa1812794f9424d4a4e2d740662df5f" +checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0" dependencies = [ "bytes", - "futures-core", "http", "http-body", "hyper", @@ -722,6 +857,12 @@ dependencies = [ "zerovec", ] +[[package]] +name = "id-arena" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" + [[package]] name = "idna" version = "1.1.0" @@ -750,7 +891,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7714e70437a7dc3ac8eb7e6f8df75fd8eb422675fc7678aff7364301092b1017" dependencies = [ "equivalent", - "hashbrown", + "hashbrown 0.16.1", + "serde", + "serde_core", ] [[package]] @@ -775,7 +918,7 @@ dependencies = [ [[package]] name = "ironpad" -version = "0.1.0" +version = "0.2.0" dependencies = [ "axum", "chrono", @@ -795,9 +938,12 @@ dependencies = [ "tower-http", "tracing", "tracing-subscriber", + "tray-item", "uuid", "walkdir", "webbrowser", + "windows-sys 0.52.0", + "winresource", ] [[package]] @@ -834,7 +980,7 @@ version = "0.1.34" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9afb3de4395d6b3e67a780b6de64b51c978ecf11cb9a462c66be7d4ca9039d33" dependencies = [ - "getrandom", + "getrandom 0.3.4", "libc", ] @@ -875,10 +1021,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" [[package]] -name = "libc" -version = "0.2.180" +name = "leb128fmt" +version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bcc35a38544a891a5f7c865aca548a982ccb3b8650a5b06d0fd33a10283c56fc" +checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" + +[[package]] +name = "libc" +version = "0.2.182" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6800badb6cb2082ffd7b6a67e6125bb39f18782f793520caee8cb8846be06112" [[package]] name = "libgit2-sys" @@ -900,9 +1052,9 @@ version = "0.1.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3d0b95e02c851351f877147b7deea7b1afb1df71b63aa5f8270716e0c5720616" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.0", "libc", - "redox_syscall 0.7.0", + "redox_syscall 0.7.1", ] [[package]] @@ -952,6 +1104,15 @@ version = "0.4.29" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" +[[package]] +name = "malloc_buf" +version = "0.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62bb907fe88d54d8d9ce32a3cceab4218ed2f6b7d35617cafe9adf84e43919cb" +dependencies = [ + "libc", +] + [[package]] name = "markdown" version = "1.0.0" @@ -978,15 +1139,15 @@ checksum = "47e1ffaa40ddd1f3ed91f717a33c8c0ee23fff369e3aa8772b9605cc1d22f4c3" [[package]] name = "memchr" -version = "2.7.6" +version = "2.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" +checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" [[package]] name = "memmap2" -version = "0.9.9" +version = "0.9.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "744133e4a0e0a658e1374cf3bf8e415c4052a15a111acd372764c55b4177d490" +checksum = "714098028fe011992e1c3962653c96b2d578c4b4bce9036e15ff220319b1e0e3" dependencies = [ "libc", ] @@ -1059,7 +1220,7 @@ version = "6.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6205bd8bb1e454ad2e27422015fb5e4f2bcc7e08fa8f27058670d208324a4d2d" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.0", "crossbeam-channel", "filetime", "fsevent-sys", @@ -1104,6 +1265,26 @@ dependencies = [ "autocfg", ] +[[package]] +name = "objc" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "915b1b472bc21c53464d6c8461c9d3af805ba1ef837e1cac254428f4a77177b1" +dependencies = [ + "malloc_buf", +] + +[[package]] +name = "objc-foundation" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1add1b659e36c9607c7aab864a76c7a4c2760cd0cd2e120f3fb8b952c7e22bf9" +dependencies = [ + "block", + "objc", + "objc_id", +] + [[package]] name = "objc2" version = "0.6.3" @@ -1125,10 +1306,19 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3e0adef53c21f888deb4fa59fc59f7eb17404926ee8a6f59f5df0fd7f9f3272" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.0", "objc2", ] +[[package]] +name = "objc_id" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c92d4ddb4bd7b50d730c215ff871754d0da6b2178849f8a2a2ab69712d0c073b" +dependencies = [ + "objc", +] + [[package]] name = "once_cell" version = "1.21.3" @@ -1163,6 +1353,12 @@ dependencies = [ "vcpkg", ] +[[package]] +name = "padlock" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c10569378a1dacd9f30dbe7ae49e054d2c45dc2f8ee49899903e09c3924e8b6f" + [[package]] name = "parking_lot" version = "0.12.5" @@ -1228,6 +1424,16 @@ dependencies = [ "zerocopy", ] +[[package]] +name = "prettyplease" +version = "0.2.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" +dependencies = [ + "proc-macro2", + "syn", +] + [[package]] name = "proc-macro2" version = "1.0.106" @@ -1278,7 +1484,7 @@ version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c" dependencies = [ - "getrandom", + "getrandom 0.3.4", ] [[package]] @@ -1287,23 +1493,23 @@ version = "0.5.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.0", ] [[package]] name = "redox_syscall" -version = "0.7.0" +version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49f3fe0889e69e2ae9e41f4d6c4c0181701d00e4697b356fb1f74173a5e0ee27" +checksum = "35985aa610addc02e24fc232012c86fd11f14111180f902b67e2d5331f8ebf2b" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.0", ] [[package]] name = "regex-automata" -version = "0.4.13" +version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5276caf25ac86c8d810222b3dbb938e512c55c6831a10f3e6ed1c93b84041f1c" +checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f" dependencies = [ "aho-corasick", "memchr", @@ -1312,9 +1518,9 @@ dependencies = [ [[package]] name = "regex-syntax" -version = "0.8.8" +version = "0.8.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58" +checksum = "a96887878f22d7bad8a3b6dc5b7440e0ada9a245242924394987b21cf2210a4c" [[package]] name = "rustversion" @@ -1324,9 +1530,9 @@ checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" [[package]] name = "ryu" -version = "1.0.22" +version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a50f4cf475b65d88e057964e0e9bb1f0aa9bbb2036dc65c64596b42932536984" +checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" [[package]] name = "same-file" @@ -1343,6 +1549,12 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" +[[package]] +name = "semver" +version = "1.0.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2" + [[package]] name = "serde" version = "1.0.228" @@ -1397,6 +1609,15 @@ dependencies = [ "serde_core", ] +[[package]] +name = "serde_spanned" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8bbf91e5a4d6315eee45e704372590b30e260ee83af6639d64557f51b067776" +dependencies = [ + "serde_core", +] + [[package]] name = "serde_urlencoded" version = "0.7.1" @@ -1494,9 +1715,9 @@ checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" [[package]] name = "syn" -version = "2.0.114" +version = "2.0.116" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d4d107df263a3013ef9b1879b0df87d706ff80f65a86ea879bd9c31f9b307c2a" +checksum = "3df424c70518695237746f84cede799c9c58fcb37450d7b23716568cc8bc69cb" dependencies = [ "proc-macro2", "quote", @@ -1641,6 +1862,45 @@ dependencies = [ "tokio", ] +[[package]] +name = "toml" +version = "0.9.12+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf92845e79fc2e2def6a5d828f0801e29a2f8acc037becc5ab08595c7d5e9863" +dependencies = [ + "indexmap", + "serde_core", + "serde_spanned", + "toml_datetime", + "toml_parser", + "toml_writer", + "winnow", +] + +[[package]] +name = "toml_datetime" +version = "0.7.5+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92e1cfed4a3038bc5a127e35a2d360f145e1f4b971b551a2ba5fd7aedf7e1347" +dependencies = [ + "serde_core", +] + +[[package]] +name = "toml_parser" +version = "1.0.8+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0742ff5ff03ea7e67c8ae6c93cac239e0d9784833362da3f9a9c1da8dfefcbdc" +dependencies = [ + "winnow", +] + +[[package]] +name = "toml_writer" +version = "1.0.6+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab16f14aed21ee8bfd8ec22513f7287cd4a91aa92e44edfe2c17ddd004e92607" + [[package]] name = "tower" version = "0.5.3" @@ -1663,7 +1923,7 @@ version = "0.6.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d4e6559d53cc268e5031cd8429d05415bc4cb4aefc4aa5d6cc35fbf5b924a1f8" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.0", "bytes", "futures-core", "futures-util", @@ -1757,6 +2017,22 @@ dependencies = [ "tracing-log", ] +[[package]] +name = "tray-item" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59d4bd406170690dc30eabb3badc67a085beaf9b2c3b1923afcc9c26a2191353" +dependencies = [ + "cocoa", + "core-graphics", + "libc", + "objc", + "objc-foundation", + "objc_id", + "padlock", + "windows-sys 0.52.0", +] + [[package]] name = "tungstenite" version = "0.28.0" @@ -1794,9 +2070,15 @@ checksum = "70ba288e709927c043cbe476718d37be306be53fb1fafecd0dbe36d072be2580" [[package]] name = "unicode-ident" -version = "1.0.22" +version = "1.0.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "unicode-xid" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" [[package]] name = "unsafe-libyaml" @@ -1830,11 +2112,11 @@ checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" [[package]] name = "uuid" -version = "1.20.0" +version = "1.21.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ee48d38b119b0cd71fe4141b30f5ba9c7c5d9f4e7a3a8b4a674e4b6ef789976f" +checksum = "b672338555252d43fd2240c714dc444b8c6fb0a5c5335e65a07bba7742735ddb" dependencies = [ - "getrandom", + "getrandom 0.4.1", "js-sys", "wasm-bindgen", ] @@ -1882,6 +2164,15 @@ dependencies = [ "wit-bindgen", ] +[[package]] +name = "wasip3" +version = "0.4.0+wasi-0.3.0-rc-2026-01-06" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5" +dependencies = [ + "wit-bindgen", +] + [[package]] name = "wasm-bindgen" version = "0.2.108" @@ -1927,6 +2218,40 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "wasm-encoder" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319" +dependencies = [ + "leb128fmt", + "wasmparser", +] + +[[package]] +name = "wasm-metadata" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909" +dependencies = [ + "anyhow", + "indexmap", + "wasm-encoder", + "wasmparser", +] + +[[package]] +name = "wasmparser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" +dependencies = [ + "bitflags 2.11.0", + "hashbrown 0.15.5", + "indexmap", + "semver", +] + [[package]] name = "web-sys" version = "0.3.85" @@ -1939,11 +2264,11 @@ dependencies = [ [[package]] name = "webbrowser" -version = "1.0.6" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "00f1243ef785213e3a32fa0396093424a3a6ea566f9948497e5a2309261a4c97" +checksum = "3f00bb839c1cf1e3036066614cbdcd035ecf215206691ea646aa3c60a24f68f2" dependencies = [ - "core-foundation", + "core-foundation 0.10.1", "jni", "log", "ndk-context", @@ -2039,6 +2364,15 @@ dependencies = [ "windows-targets 0.48.5", ] +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets 0.52.6", +] + [[package]] name = "windows-sys" version = "0.60.2" @@ -2087,6 +2421,22 @@ dependencies = [ "windows_x86_64_msvc 0.48.5", ] +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", + "windows_i686_gnullvm 0.52.6", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", +] + [[package]] name = "windows-targets" version = "0.53.5" @@ -2097,7 +2447,7 @@ dependencies = [ "windows_aarch64_gnullvm 0.53.1", "windows_aarch64_msvc 0.53.1", "windows_i686_gnu 0.53.1", - "windows_i686_gnullvm", + "windows_i686_gnullvm 0.53.1", "windows_i686_msvc 0.53.1", "windows_x86_64_gnu 0.53.1", "windows_x86_64_gnullvm 0.53.1", @@ -2116,6 +2466,12 @@ version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + [[package]] name = "windows_aarch64_gnullvm" version = "0.53.1" @@ -2134,6 +2490,12 @@ version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + [[package]] name = "windows_aarch64_msvc" version = "0.53.1" @@ -2152,12 +2514,24 @@ version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + [[package]] name = "windows_i686_gnu" version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + [[package]] name = "windows_i686_gnullvm" version = "0.53.1" @@ -2176,6 +2550,12 @@ version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + [[package]] name = "windows_i686_msvc" version = "0.53.1" @@ -2194,6 +2574,12 @@ version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + [[package]] name = "windows_x86_64_gnu" version = "0.53.1" @@ -2212,6 +2598,12 @@ version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + [[package]] name = "windows_x86_64_gnullvm" version = "0.53.1" @@ -2230,17 +2622,121 @@ version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + [[package]] name = "windows_x86_64_msvc" version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" +[[package]] +name = "winnow" +version = "0.7.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a5364e9d77fcdeeaa6062ced926ee3381faa2ee02d3eb83a5c27a8825540829" + +[[package]] +name = "winresource" +version = "0.1.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e287ced0f21cd11f4035fe946fd3af145f068d1acb708afd248100f89ec7432d" +dependencies = [ + "toml", + "version_check", +] + [[package]] name = "wit-bindgen" version = "0.51.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" +dependencies = [ + "wit-bindgen-rust-macro", +] + +[[package]] +name = "wit-bindgen-core" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc" +dependencies = [ + "anyhow", + "heck", + "wit-parser", +] + +[[package]] +name = "wit-bindgen-rust" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21" +dependencies = [ + "anyhow", + "heck", + "indexmap", + "prettyplease", + "syn", + "wasm-metadata", + "wit-bindgen-core", + "wit-component", +] + +[[package]] +name = "wit-bindgen-rust-macro" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c0f9bfd77e6a48eccf51359e3ae77140a7f50b1e2ebfe62422d8afdaffab17a" +dependencies = [ + "anyhow", + "prettyplease", + "proc-macro2", + "quote", + "syn", + "wit-bindgen-core", + "wit-bindgen-rust", +] + +[[package]] +name = "wit-component" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" +dependencies = [ + "anyhow", + "bitflags 2.11.0", + "indexmap", + "log", + "serde", + "serde_derive", + "serde_json", + "wasm-encoder", + "wasm-metadata", + "wasmparser", + "wit-parser", +] + +[[package]] +name = "wit-parser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736" +dependencies = [ + "anyhow", + "id-arena", + "indexmap", + "log", + "semver", + "serde", + "serde_derive", + "serde_json", + "unicode-xid", + "wasmparser", +] [[package]] name = "writeable" @@ -2273,18 +2769,18 @@ dependencies = [ [[package]] name = "zerocopy" -version = "0.8.37" +version = "0.8.39" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7456cf00f0685ad319c5b1693f291a650eaf345e941d082fc4e03df8a03996ac" +checksum = "db6d35d663eadb6c932438e763b262fe1a70987f9ae936e60158176d710cae4a" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.8.37" +version = "0.8.39" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1328722bbf2115db7e19d69ebcc15e795719e2d66b60827c6a69a117365e37a0" +checksum = "4122cd3169e94605190e77839c9a40d40ed048d305bfdc146e7df40ab0f3e517" dependencies = [ "proc-macro2", "quote", @@ -2347,6 +2843,6 @@ dependencies = [ [[package]] name = "zmij" -version = "1.0.19" +version = "1.0.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ff05f8caa9038894637571ae6b9e29466c1f4f829d26c9b28f869a29cbe3445" +checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" diff --git a/backend/Cargo.toml b/backend/Cargo.toml index 43b1ce2..08ef581 100644 --- a/backend/Cargo.toml +++ b/backend/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ironpad" -version = "0.1.0" +version = "0.2.0" edition = "2021" [dependencies] @@ -46,4 +46,15 @@ uuid = { version = "1.0", features = ["v4"] } # Utilities lazy_static = "1.4" -tokio-util = { version = "0.7", features = ["io"] } \ No newline at end of file +tokio-util = { version = "0.7", features = ["io"] } + +# System tray (production mode) +tray-item = "0.10" + +# Windows icon loading (for tray icon) +[target.'cfg(target_os = "windows")'.dependencies] +windows-sys = { version = "0.52", features = ["Win32_UI_WindowsAndMessaging", "Win32_System_LibraryLoader"] } + +# Build dependencies (Windows icon embedding) +[target.'cfg(target_os = "windows")'.build-dependencies] +winresource = "0.1" \ No newline at end of file diff --git a/backend/assets/icon-32x32.png b/backend/assets/icon-32x32.png new file mode 100644 index 0000000..5a3c089 Binary files /dev/null and b/backend/assets/icon-32x32.png differ diff --git a/backend/assets/ironpad.ico b/backend/assets/ironpad.ico new file mode 100644 index 0000000..b71d9c9 Binary files /dev/null and b/backend/assets/ironpad.ico differ diff --git a/backend/build.rs b/backend/build.rs new file mode 100644 index 0000000..2a643cc --- /dev/null +++ b/backend/build.rs @@ -0,0 +1,10 @@ +fn main() { + // On Windows, embed the application icon into the .exe + // This sets both the Explorer icon and makes it available as a resource + #[cfg(target_os = "windows")] + { + let mut res = winresource::WindowsResource::new(); + res.set_icon("assets/ironpad.ico"); + res.compile().expect("Failed to compile Windows resources"); + } +} diff --git a/backend/src/main.rs b/backend/src/main.rs index e07f8bd..96cc76d 100644 --- a/backend/src/main.rs +++ b/backend/src/main.rs @@ -1,3 +1,6 @@ +// Hide console window on Windows in release builds (production mode) +#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] + use std::net::SocketAddr; use std::sync::Arc; @@ -26,17 +29,29 @@ async fn find_available_port() -> (TcpListener, u16) { panic!("No available ports in range 3000–3010"); } -#[tokio::main] -async fn main() { - // Logging +fn main() { tracing_subscriber::fmt().init(); - - // Resolve data directory (production vs development mode) config::init_data_dir(); - // Find port and bind (listener kept alive to avoid race condition) + if config::is_production() { + run_with_tray(); + } else { + // Development mode: normal tokio runtime, no tray + let rt = tokio::runtime::Runtime::new().expect("Failed to create tokio runtime"); + rt.block_on(run_server(None)); + } +} + +/// Start the Axum server. In tray mode, sends the bound port through `port_tx` +/// before entering the serve loop. +async fn run_server(port_tx: Option>) { let (listener, port) = find_available_port().await; + // Notify tray thread of the bound port + if let Some(tx) = port_tx { + let _ = tx.send(port); + } + // WebSocket state (shared across handlers) let ws_state = Arc::new(websocket::WsState::new()); @@ -94,7 +109,6 @@ async fn main() { .layer(cors); // Check for embedded frontend (production mode) - // Resolve relative to the executable's directory, not the working directory let has_frontend = config::is_production(); if has_frontend { @@ -116,23 +130,110 @@ async fn main() { } // Start server - info!("🚀 Ironpad running on http://localhost:{port}"); - - // Auto-open browser in production mode - if has_frontend { - let url = format!("http://localhost:{}", port); - tokio::spawn(async move { - // Small delay to ensure server is ready - tokio::time::sleep(std::time::Duration::from_millis(300)).await; - if let Err(e) = webbrowser::open(&url) { - tracing::warn!( - "Failed to open browser: {}. Open http://localhost:{} manually.", - e, - port - ); - } - }); - } + info!("Ironpad running on http://localhost:{port}"); axum::serve(listener, app).await.expect("Server failed"); } + +// --------------------------------------------------------------------------- +// System tray (production mode) +// --------------------------------------------------------------------------- + +/// Build a platform-appropriate tray icon. +/// +/// On Windows the Ironpad icon is embedded in the .exe via winresource (build.rs). +/// We load it with LoadIconW using the resource ID assigned by winresource. +#[cfg(target_os = "windows")] +fn tray_icon() -> tray_item::IconSource { + let hicon = unsafe { + // winresource embeds the icon at resource ID 1. + // GetModuleHandleW(null) = current exe, MAKEINTRESOURCE(1) = 1 as PCWSTR. + let hinstance = + windows_sys::Win32::System::LibraryLoader::GetModuleHandleW(std::ptr::null()); + windows_sys::Win32::UI::WindowsAndMessaging::LoadIconW(hinstance, 1 as *const u16) + }; + tray_item::IconSource::RawIcon(hicon) +} + +#[cfg(target_os = "macos")] +fn tray_icon() -> tray_item::IconSource { + tray_item::IconSource::Resource("") +} + +#[cfg(target_os = "linux")] +fn tray_icon() -> tray_item::IconSource { + tray_item::IconSource::Resource("application-x-executable") +} + +#[cfg(not(any(target_os = "windows", target_os = "macos", target_os = "linux")))] +fn tray_icon() -> tray_item::IconSource { + tray_item::IconSource::Resource("") +} + +/// Production mode: run the server on a background thread, tray on main thread. +/// The main thread drives the tray event loop (required on macOS; safe everywhere). +fn run_with_tray() { + use std::sync::mpsc; + + enum TrayMessage { + OpenBrowser, + Quit, + } + + // Channel to receive the dynamically-bound port from the server thread + let (port_tx, port_rx) = mpsc::channel::(); + + // Start the Axum server on a background thread with its own tokio runtime + std::thread::spawn(move || { + let rt = tokio::runtime::Runtime::new().expect("Failed to create tokio runtime"); + rt.block_on(run_server(Some(port_tx))); + }); + + // Wait for the server to report its port + let port = port_rx.recv().expect("Server failed to start"); + let url = format!("http://localhost:{}", port); + + // Auto-open browser after a short delay (non-blocking) + let url_for_open = url.clone(); + std::thread::spawn(move || { + std::thread::sleep(std::time::Duration::from_millis(400)); + let _ = webbrowser::open(&url_for_open); + }); + + // Set up system tray icon and menu + let (tx, rx) = mpsc::sync_channel::(2); + + let mut tray = match tray_item::TrayItem::new("Ironpad", tray_icon()) { + Ok(t) => t, + Err(e) => { + eprintln!("Failed to create system tray: {}. Running headless.", e); + // Keep the process alive so the server thread continues + loop { + std::thread::park(); + } + } + }; + + let tx_open = tx.clone(); + let _ = tray.add_menu_item("Open in Browser", move || { + let _ = tx_open.send(TrayMessage::OpenBrowser); + }); + + let tx_quit = tx; + let _ = tray.add_menu_item("Quit", move || { + let _ = tx_quit.send(TrayMessage::Quit); + }); + + // Main-thread event loop — processes tray menu actions + for msg in rx { + match msg { + TrayMessage::OpenBrowser => { + let _ = webbrowser::open(&url); + } + TrayMessage::Quit => { + info!("Quit requested from system tray"); + std::process::exit(0); + } + } + } +} diff --git a/backend/src/routes/projects.rs b/backend/src/routes/projects.rs index 077d386..3e87230 100644 --- a/backend/src/routes/projects.rs +++ b/backend/src/routes/projects.rs @@ -10,8 +10,9 @@ use std::fs; use crate::config; use crate::routes::tasks::{ - create_task_handler, delete_task_handler, get_task_handler, list_project_tasks_handler, - toggle_task_handler, update_task_content_handler, update_task_meta_handler, CreateTaskRequest, + add_comment_handler, create_task_handler, delete_comment_handler, delete_task_handler, + get_task_handler, list_project_tasks_handler, toggle_task_handler, + update_task_content_handler, update_task_meta_handler, AddCommentRequest, CreateTaskRequest, UpdateTaskMetaRequest, }; use crate::services::filesystem; @@ -91,6 +92,14 @@ pub fn router() -> Router { ) .route("/{id}/tasks/{task_id}/toggle", put(toggle_project_task)) .route("/{id}/tasks/{task_id}/meta", put(update_project_task_meta)) + .route( + "/{id}/tasks/{task_id}/comments", + axum::routing::post(add_project_task_comment), + ) + .route( + "/{id}/tasks/{task_id}/comments/{comment_index}", + axum::routing::delete(delete_project_task_comment), + ) // Note routes .route( "/{id}/notes", @@ -143,6 +152,19 @@ async fn delete_project_task(Path((id, task_id)): Path<(String, String)>) -> imp delete_task_handler(id, task_id).await } +async fn add_project_task_comment( + Path((id, task_id)): Path<(String, String)>, + Json(payload): Json, +) -> impl IntoResponse { + add_comment_handler(id, task_id, payload).await +} + +async fn delete_project_task_comment( + Path((id, task_id, comment_index)): Path<(String, String, usize)>, +) -> impl IntoResponse { + delete_comment_handler(id, task_id, comment_index).await +} + async fn list_projects() -> impl IntoResponse { match list_projects_impl() { Ok(projects) => Json(projects).into_response(), diff --git a/backend/src/routes/tasks.rs b/backend/src/routes/tasks.rs index 09ff119..1f3bd54 100644 --- a/backend/src/routes/tasks.rs +++ b/backend/src/routes/tasks.rs @@ -7,6 +7,13 @@ use crate::config; use crate::services::filesystem; use crate::services::frontmatter; +/// A single comment entry on a task +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Comment { + pub date: String, + pub text: String, +} + /// Task summary for list views #[derive(Debug, Clone, Serialize)] pub struct Task { @@ -25,6 +32,7 @@ pub struct Task { pub path: String, pub created: String, pub updated: String, + pub last_comment: Option, } /// Task with full content for detail view @@ -46,6 +54,7 @@ pub struct TaskWithContent { pub created: String, pub updated: String, pub content: String, + pub comments: Vec, } #[derive(Debug, Deserialize)] @@ -67,6 +76,11 @@ pub struct UpdateTaskMetaRequest { pub recurrence_interval: Option, } +#[derive(Debug, Deserialize)] +pub struct AddCommentRequest { + pub text: String, +} + pub fn router() -> Router { Router::new().route("/", get(list_all_tasks_handler)) } @@ -178,6 +192,43 @@ pub async fn delete_task_handler(project_id: String, task_id: String) -> impl In } } +/// Add a comment to a task +pub async fn add_comment_handler( + project_id: String, + task_id: String, + payload: AddCommentRequest, +) -> impl IntoResponse { + match add_comment_impl(&project_id, &task_id, &payload.text) { + Ok(task) => (StatusCode::CREATED, Json(task)).into_response(), + Err(err) if err.contains("not found") => (StatusCode::NOT_FOUND, err).into_response(), + Err(err) => ( + StatusCode::INTERNAL_SERVER_ERROR, + format!("Failed to add comment: {}", err), + ) + .into_response(), + } +} + +/// Delete a comment from a task by index +pub async fn delete_comment_handler( + project_id: String, + task_id: String, + comment_index: usize, +) -> impl IntoResponse { + match delete_comment_impl(&project_id, &task_id, comment_index) { + Ok(task) => Json(task).into_response(), + Err(err) if err.contains("not found") => (StatusCode::NOT_FOUND, err).into_response(), + Err(err) if err.contains("out of range") => { + (StatusCode::BAD_REQUEST, err).into_response() + } + Err(err) => ( + StatusCode::INTERNAL_SERVER_ERROR, + format!("Failed to delete comment: {}", err), + ) + .into_response(), + } +} + // ============ Implementation Functions ============ fn get_tasks_dir(project_id: &str) -> std::path::PathBuf { @@ -233,6 +284,29 @@ fn list_project_tasks_impl(project_id: &str) -> Result, String> { Ok(tasks) } +/// Parse comments from frontmatter YAML sequence. +fn parse_comments(fm: &serde_yaml::Mapping) -> Vec { + fm.get(&serde_yaml::Value::from("comments")) + .and_then(|v| v.as_sequence()) + .map(|seq| { + seq.iter() + .filter_map(|item| { + let map = item.as_mapping()?; + let date = map + .get(&serde_yaml::Value::from("date")) + .and_then(|v| v.as_str()) + .map(String::from)?; + let text = map + .get(&serde_yaml::Value::from("text")) + .and_then(|v| v.as_str()) + .map(String::from)?; + Some(Comment { date, text }) + }) + .collect() + }) + .unwrap_or_default() +} + /// Shared helper: extract common task fields from frontmatter. /// Eliminates duplication between parse_task_file and parse_task_with_content. fn extract_task_fields(fm: &serde_yaml::Mapping, path: &StdPath, project_id: &str) -> Task { @@ -242,6 +316,9 @@ fn extract_task_fields(fm: &serde_yaml::Mapping, path: &StdPath, project_id: &st .unwrap_or("") .to_string(); + let comments = parse_comments(fm); + let last_comment = comments.last().map(|c| c.text.clone()); + Task { id: frontmatter::get_str_or(fm, "id", &filename), title: frontmatter::get_str_or(fm, "title", "Untitled"), @@ -258,6 +335,7 @@ fn extract_task_fields(fm: &serde_yaml::Mapping, path: &StdPath, project_id: &st path: format!("projects/{}/tasks/{}.md", project_id, filename), created: frontmatter::get_str_or(fm, "created", ""), updated: frontmatter::get_str_or(fm, "updated", ""), + last_comment, } } @@ -355,6 +433,7 @@ fn create_task_impl( created: now_str.clone(), updated: now_str, content: body, + comments: Vec::new(), }) } @@ -407,6 +486,7 @@ fn parse_task_with_content( project_id: &str, ) -> Result { let task = extract_task_fields(fm, path, project_id); + let comments = parse_comments(fm); Ok(TaskWithContent { id: task.id, title: task.title, @@ -424,6 +504,7 @@ fn parse_task_with_content( created: task.created, updated: task.updated, content: body.to_string(), + comments, }) } @@ -683,6 +764,7 @@ fn create_recurring_task_impl( created: now_str.clone(), updated: now_str, content: body, + comments: Vec::new(), }) } @@ -776,6 +858,102 @@ fn update_task_meta_impl( Ok(task) } +/// Serialize a Vec into a YAML sequence Value. +fn comments_to_yaml(comments: &[Comment]) -> serde_yaml::Value { + let seq: Vec = comments + .iter() + .map(|c| { + let mut map = serde_yaml::Mapping::new(); + map.insert( + serde_yaml::Value::from("date"), + serde_yaml::Value::from(c.date.as_str()), + ); + map.insert( + serde_yaml::Value::from("text"), + serde_yaml::Value::from(c.text.as_str()), + ); + serde_yaml::Value::Mapping(map) + }) + .collect(); + serde_yaml::Value::Sequence(seq) +} + +fn add_comment_impl( + project_id: &str, + task_id: &str, + text: &str, +) -> Result { + let task_path = find_task_path(project_id, task_id)?; + + let existing = fs::read_to_string(&task_path).map_err(|e| e.to_string())?; + let (mut fm, body, _) = frontmatter::parse_frontmatter(&existing); + + // Parse existing comments and append the new one + let mut comments = parse_comments(&fm); + let now = chrono::Utc::now().to_rfc3339(); + comments.push(Comment { + date: now.clone(), + text: text.to_string(), + }); + + // Write comments back to frontmatter + fm.insert( + serde_yaml::Value::from("comments"), + comments_to_yaml(&comments), + ); + + // Update timestamp + fm.insert( + serde_yaml::Value::from("updated"), + serde_yaml::Value::from(now), + ); + + let new_content = frontmatter::serialize_frontmatter(&fm, &body)?; + filesystem::atomic_write(&task_path, new_content.as_bytes())?; + + parse_task_with_content(&fm, &body, &task_path, project_id) +} + +fn delete_comment_impl( + project_id: &str, + task_id: &str, + comment_index: usize, +) -> Result { + let task_path = find_task_path(project_id, task_id)?; + + let existing = fs::read_to_string(&task_path).map_err(|e| e.to_string())?; + let (mut fm, body, _) = frontmatter::parse_frontmatter(&existing); + + let mut comments = parse_comments(&fm); + if comment_index >= comments.len() { + return Err("Comment index out of range".to_string()); + } + + comments.remove(comment_index); + + // Write comments back (or remove key if empty) + if comments.is_empty() { + fm.remove(&serde_yaml::Value::from("comments")); + } else { + fm.insert( + serde_yaml::Value::from("comments"), + comments_to_yaml(&comments), + ); + } + + // Update timestamp + let now = chrono::Utc::now().to_rfc3339(); + fm.insert( + serde_yaml::Value::from("updated"), + serde_yaml::Value::from(now), + ); + + let new_content = frontmatter::serialize_frontmatter(&fm, &body)?; + filesystem::atomic_write(&task_path, new_content.as_bytes())?; + + parse_task_with_content(&fm, &body, &task_path, project_id) +} + fn delete_task_impl(project_id: &str, task_id: &str) -> Result<(), String> { let task_path = find_task_path(project_id, task_id)?; diff --git a/build-local.ps1 b/build-local.ps1 new file mode 100644 index 0000000..159d6dc --- /dev/null +++ b/build-local.ps1 @@ -0,0 +1,40 @@ +# build-local.ps1 — Build a local release package for testing +# Creates: release/ironpad.exe + release/static/ +# Usage: .\build-local.ps1 + +$ErrorActionPreference = "Stop" +$root = $PSScriptRoot + +Write-Host "`n=== Ironpad Local Build ===" -ForegroundColor Cyan + +# 1. Build frontend +Write-Host "`n[1/4] Building frontend..." -ForegroundColor Yellow +Push-Location "$root\frontend" +npm run build +if ($LASTEXITCODE -ne 0) { Pop-Location; throw "Frontend build failed" } +Pop-Location + +# 2. Build backend (release) +Write-Host "`n[2/4] Building backend (release)..." -ForegroundColor Yellow +Push-Location "$root\backend" +cargo build --release +if ($LASTEXITCODE -ne 0) { Pop-Location; throw "Backend build failed" } +Pop-Location + +# 3. Assemble release folder +Write-Host "`n[3/4] Assembling release folder..." -ForegroundColor Yellow +$releaseDir = "$root\release" +if (Test-Path $releaseDir) { Remove-Item -Recurse -Force $releaseDir } +New-Item -ItemType Directory -Path $releaseDir | Out-Null +New-Item -ItemType Directory -Path "$releaseDir\static" | Out-Null + +Copy-Item "$root\backend\target\release\ironpad.exe" "$releaseDir\ironpad.exe" +Copy-Item "$root\frontend\dist\*" "$releaseDir\static\" -Recurse + +# 4. Done +$size = [math]::Round((Get-Item "$releaseDir\ironpad.exe").Length / 1MB, 1) +Write-Host "`n[4/4] Done!" -ForegroundColor Green +Write-Host " Binary: $releaseDir\ironpad.exe ($size MB)" +Write-Host " Static: $releaseDir\static\" +Write-Host "`nTo run: cd release && .\ironpad.exe" -ForegroundColor Cyan +Write-Host "(Tray icon will appear - right-click for menu)`n" diff --git a/docs/API.md b/docs/API.md index 1b37640..4f7f508 100644 --- a/docs/API.md +++ b/docs/API.md @@ -267,7 +267,12 @@ GET /api/projects/:id/tasks "priority": "high", "due_date": "2026-02-10", "is_active": true, - "content": "## Requirements\n\n- Item 1\n- Item 2", + "tags": ["backend", "api"], + "parent_id": null, + "recurrence": null, + "recurrence_interval": null, + "project_id": "ferrite", + "last_comment": "API endpoint done, moving to frontend", "path": "projects/ferrite/tasks/task-20260205-123456.md", "created": "2026-02-05T12:34:56Z", "updated": "2026-02-05T12:34:56Z" @@ -338,6 +343,72 @@ PUT /api/projects/:id/tasks/:taskId/toggle DELETE /api/projects/:id/tasks/:taskId ``` +### Add Comment + +```http +POST /api/projects/:id/tasks/:taskId/comments +Content-Type: application/json + +{ + "text": "Started work on this — API integration is in progress." +} +``` + +**Response:** `201 Created` +```json +{ + "id": "task-20260216-120000", + "title": "Implement feature X", + "completed": false, + "section": "Active", + "is_active": true, + "comments": [ + { + "date": "2026-02-16T10:30:00+00:00", + "text": "Created initial spec" + }, + { + "date": "2026-02-16T12:00:00+00:00", + "text": "Started work on this — API integration is in progress." + } + ], + "content": "## Requirements\n\n- Item 1\n- Item 2", + "...": "other task fields" +} +``` + +Comments are stored as a YAML sequence in the task's frontmatter. The response returns the full `TaskWithContent` object with all comments. + +### Delete Comment + +```http +DELETE /api/projects/:id/tasks/:taskId/comments/:commentIndex +``` + +Removes the comment at the given zero-based index. + +**Response:** +```json +{ + "id": "task-20260216-120000", + "comments": [], + "...": "full TaskWithContent" +} +``` + +### Comment in List Views + +When listing tasks (`GET /api/projects/:id/tasks` or `GET /api/tasks`), each task includes a `last_comment` field with the text of the most recent comment (or `null` if no comments exist). This enables showing a quick status summary without loading the full task. + +```json +{ + "id": "task-20260216-120000", + "title": "Implement feature X", + "last_comment": "Started work on this — API integration is in progress.", + "...": "other task fields" +} +``` + --- ## All Tasks diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index f5d2211..f909b77 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -320,6 +320,11 @@ interface Project { ### Task ```typescript +interface Comment { + date: string // ISO 8601 timestamp + text: string // Comment body +} + interface Task { id: string // e.g., "task-20260205-123456" title: string @@ -328,13 +333,52 @@ interface Task { priority?: string due_date?: string is_active: boolean - content: string // Markdown description + tags: string[] + parent_id?: string // Links subtask to parent + recurrence?: string // "daily" | "weekly" | "monthly" | "yearly" + recurrence_interval?: number + last_comment?: string // Most recent comment text (list views) path: string created: string updated: string } + +interface TaskWithContent extends Task { + content: string // Markdown description + comments: Comment[] // Full comment history +} ``` +#### Task File Format + +```markdown +--- +id: ferrite-task-20260216-120000 +type: task +title: Implement feature X +completed: false +section: Active +priority: normal +is_active: true +tags: + - backend + - api +comments: + - date: "2026-02-16T10:30:00+00:00" + text: Started initial research + - date: "2026-02-16T14:00:00+00:00" + text: API endpoint done, moving to frontend +created: "2026-02-16T12:00:00+00:00" +updated: "2026-02-16T14:00:00+00:00" +--- + +# Implement feature X + +Detailed description in markdown... +``` + +Comments are stored as a YAML sequence directly in frontmatter, keeping everything in a single file. The `last_comment` field in list views is derived at read time from the last entry in the sequence. + ## API Design ### REST Conventions diff --git a/docs/system-tray-implementation.md b/docs/system-tray-implementation.md index 0c3d7a4..17bfda7 100644 --- a/docs/system-tray-implementation.md +++ b/docs/system-tray-implementation.md @@ -1,4 +1,6 @@ -# System Tray Implementation (v0.2.0) +# System Tray Implementation (v0.2.0) ✅ + +**Status:** Implemented **Goal:** Replace the CMD window with a system tray icon. Users interact via tray menu: "Open in Browser" or "Quit". No console window on Windows. @@ -6,100 +8,101 @@ ## Overview -- **Scope:** Single codebase, cross-platform. CI/CD unchanged—same build pipeline produces one binary per OS. -- **Complexity:** Low–medium. Uses a cross-platform Rust crate; platform-specific code is minimal. +- **Scope:** Single codebase, cross-platform. CI/CD unchanged — same build pipeline produces one binary per OS. +- **Crate:** `tray-item = "0.10"` (cross-platform tray icon with simple API) +- **Windows icon:** `windows-sys = "0.52"` for loading the standard application icon via `LoadIconW` --- -## Implementation Steps +## What Was Implemented -### 1. Add Tray Crate Dependency - -Add to `backend/Cargo.toml`: +### 1. Dependencies (`backend/Cargo.toml`) ```toml # System tray (production mode) tray-item = "0.10" + +# Windows system icon loading (for tray icon) +[target.'cfg(target_os = "windows")'.dependencies] +windows-sys = { version = "0.52", features = ["Win32_UI_WindowsAndMessaging"] } ``` -Alternative: `tray-icon` (more features, heavier; requires event loop integration). - -### 2. Windows: Hide Console Window - -Add near the top of `backend/src/main.rs` (after `mod` declarations if any, before `fn main`): +### 2. Windows: Hide Console Window (`backend/src/main.rs`) ```rust #![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] ``` -- **Debug builds:** Console remains (for logs). -- **Release builds:** No CMD window on Windows. +- **Debug builds (`cargo run`):** Console remains for logs. +- **Release builds (`cargo build --release`):** No CMD window on Windows. -### 3. Remove Auto-Open Browser on Startup +### 3. Restructured `main()` for Dual-Mode Operation -In `main.rs`, remove or conditionally disable the auto-open logic (the `tokio::spawn` block that calls `webbrowser::open()`). The user will open the browser from the tray menu instead. - -### 4. Add Tray Icon and Menu (Production Mode Only) - -When `has_frontend` is true (production mode): - -1. Create tray icon with an appropriate icon (or placeholder). -2. Add menu items: - - **"Open in Browser"** — calls `webbrowser::open()` with `http://localhost:{port}`. - - **"Quit"** — shuts down the server and exits the process. - -### 5. Threading Considerations - -- **macOS:** Some tray crates expect event handling on the main thread. May need to run tray logic on main thread and spawn the Axum server on a background thread, or use crate-specific patterns. -- **Windows/Linux:** Usually more flexible; verify with the chosen crate’s docs. - -### 6. CI/CD Changes (If Needed) - -Current `release.yml` builds for Windows, macOS, and Linux. Likely no changes required. - -If using a crate that needs GTK on Linux (e.g. `tray-icon`), add to the "Install system dependencies (Linux)" step: - -```yaml -sudo apt-get install -y cmake libgtk-3-dev libappindicator3-dev +``` +main() +├── Development mode (no static/index.html next to exe) +│ └── Normal tokio runtime on main thread, API-only, no tray +└── Production mode (static/index.html exists) + └── run_with_tray() + ├── Background thread: tokio runtime + Axum server + ├── mpsc channel: server sends bound port back to main thread + ├── Auto-open browser (400ms delay) + └── Main thread: tray icon + event loop ``` -Note: Linux users would then need GTK installed at runtime. For `tray-item`, check whether it has different Linux deps. +### 4. Tray Icon (Platform-Specific) + +| Platform | Icon Source | +|----------|------------| +| Windows | `IDI_APPLICATION` via `LoadIconW` (standard system app icon) | +| macOS | `IconSource::Resource("")` (default icon) | +| Linux | `IconSource::Resource("application-x-executable")` (icon theme) | + +### 5. Tray Menu + +- **"Open in Browser"** — calls `webbrowser::open()` with `http://localhost:{port}` +- **"Quit"** — calls `std::process::exit(0)` to shut down server and exit + +### 6. Threading Model + +The tray event loop runs on the **main thread** (required on macOS, safe on all platforms). The Axum server runs on a **background thread** with its own `tokio::runtime::Runtime`. Port discovery uses `std::sync::mpsc::channel`. --- ## Behaviour Summary -| Before (v0.1.0) | After (v0.2.0) | -|------------------------|--------------------------------| -| CMD window visible | No console window (Windows) | -| Browser opens on start | Browser opens via tray menu | -| Quit via Ctrl+C | Quit via tray menu | +| Before (v0.1.0) | After (v0.2.0) | +|------------------------|------------------------------------| +| CMD window visible | No console window (Windows release)| +| Browser opens on start | Browser opens on start + via tray | +| Quit via Ctrl+C | Quit via tray menu or Ctrl+C (dev) | --- ## Testing Checklist -- [ ] Windows: No CMD window when running release binary. -- [ ] Windows: Tray icon appears; "Open in Browser" opens correct URL. -- [ ] Windows: "Quit" exits cleanly. -- [ ] macOS: Tray icon in menu bar; menu works. -- [ ] Linux: Tray icon in system tray; menu works. -- [ ] Development mode (`cargo run`): Behaviour unchanged (no tray, API-only). +- [x] Backend compiles with new dependencies (`cargo check`) +- [ ] Windows: No CMD window when running release binary +- [ ] Windows: Tray icon appears; "Open in Browser" opens correct URL +- [ ] Windows: "Quit" exits cleanly +- [ ] macOS: Tray icon in menu bar; menu works +- [ ] Linux: Tray icon in system tray; menu works +- [ ] Development mode (`cargo run`): Behaviour unchanged (no tray, API-only) --- ## Icon Asset -You’ll need a tray icon (e.g. 16×16 or 32×32 PNG). Options: +Currently using platform default icons (Windows system app icon, macOS default, Linux icon theme). To use a custom branded icon: -- Extract from existing branding/logo. -- Use a simple placeholder (e.g. filled circle) for initial implementation. -- Store in `backend/` or `backend/static/` and load at runtime. +1. Create a 16×16 or 32×32 PNG/ICO +2. On Windows: embed via `.rc` resource file and `build.rs`, use `IconSource::Resource("icon-name")` +3. On macOS/Linux: use `IconSource::Data { width, height, data }` with raw RGBA bytes --- ## References - [tray-item crate](https://crates.io/crates/tray-item) -- [tray-icon crate](https://crates.io/crates/tray-icon) (alternative) -- `#![windows_subsystem = "windows"]` — [Rust embed documentation](https://doc.rust-lang.org/reference/conditional-compilation.html#windows_subsystem) +- [tray-icon crate](https://crates.io/crates/tray-icon) (alternative, heavier) +- `#![windows_subsystem = "windows"]` — [Rust conditional compilation](https://doc.rust-lang.org/reference/conditional-compilation.html#windows_subsystem) diff --git a/frontend/index.html b/frontend/index.html index 5feef3a..a5ef858 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -6,6 +6,8 @@ + + Ironpad