From 781ea2809734ba915cb66b1b3dc011698cf92b51 Mon Sep 17 00:00:00 2001 From: skepsismusic Date: Mon, 16 Feb 2026 13:48:54 +0100 Subject: [PATCH] Release v0.2.0: Task comments, recurring calendar, system tray, app branding New features: - Task comments with date-stamped entries and last-comment summary - Recurring tasks expanded on calendar (daily/weekly/monthly/yearly) - System tray mode replacing CMD window (Windows/macOS/Linux) - Ironpad logo as exe icon, tray icon, favicon, and header logo Technical changes: - Backend restructured for dual-mode: dev (API-only) / prod (tray + server) - tray-item crate for cross-platform tray, winresource for icon embedding - Calendar view refactored with CalendarEntry interface for recurring merging - Added CHANGELOG.md, build-local.ps1, version bumped to 0.2.0 Co-authored-by: Cursor --- .gitignore | 3 + CHANGELOG.md | 39 ++ README.md | 13 +- ROADMAP.md | 54 +-- ai-context.md | 37 +- backend/Cargo.lock | 628 ++++++++++++++++++++++++--- backend/Cargo.toml | 15 +- backend/assets/icon-32x32.png | Bin 0 -> 1912 bytes backend/assets/ironpad.ico | Bin 0 -> 40747 bytes backend/build.rs | 10 + backend/src/main.rs | 149 ++++++- backend/src/routes/projects.rs | 26 +- backend/src/routes/tasks.rs | 178 ++++++++ build-local.ps1 | 40 ++ docs/API.md | 73 +++- docs/ARCHITECTURE.md | 46 +- docs/system-tray-implementation.md | 117 ++--- frontend/index.html | 2 + frontend/package.json | 2 +- frontend/public/favicon.ico | Bin 0 -> 5976 bytes frontend/public/logo-180.png | Bin 0 -> 20875 bytes frontend/public/logo-32.png | Bin 0 -> 1912 bytes frontend/src/api/client.ts | 17 +- frontend/src/components/TopBar.vue | 14 +- frontend/src/stores/tasks.ts | 38 ++ frontend/src/types/index.ts | 7 + frontend/src/views/CalendarView.vue | 163 ++++++- frontend/src/views/DashboardView.vue | 53 ++- frontend/src/views/TasksView.vue | 230 ++++++++++ 29 files changed, 1735 insertions(+), 219 deletions(-) create mode 100644 CHANGELOG.md create mode 100644 backend/assets/icon-32x32.png create mode 100644 backend/assets/ironpad.ico create mode 100644 backend/build.rs create mode 100644 build-local.ps1 create mode 100644 frontend/public/favicon.ico create mode 100644 frontend/public/logo-180.png create mode 100644 frontend/public/logo-32.png 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 0000000000000000000000000000000000000000..5a3c089cb0c5535ea4ee78743b2978c416d62c18 GIT binary patch literal 1912 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 Cm}zGK literal 0 HcmV?d00001 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