diff --git a/.gitignore b/.gitignore index a547bf3..5224870 100644 --- a/.gitignore +++ b/.gitignore @@ -12,6 +12,12 @@ dist dist-ssr *.local +# Rust build artifacts +src-tauri/target/ + +# User vault data +vault/ + # Editor directories and files .vscode/* !.vscode/extensions.json diff --git a/CHANGELOG.md b/CHANGELOG.md index 5f2af2a..2c3eff6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,25 @@ All notable changes to Graph Notes will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/), and this project adheres to [Semantic Versioning](https://semver.org/). +## [1.0.0] — 2026-03-09 + +### 🎉 First Stable Release + +Graph Notes reaches 1.0 — a local-first, graph-based note-taking app built with Tauri, React, and Rust. + +### Fixed +- **Rust compilation** — resolved duplicate `dirs_config_path()` definition and removed reference to unlinked `dirs` crate +- **Content Security Policy** — replaced `null` CSP with a proper baseline policy allowing local resources and Google Fonts +- **Canvas dependency** — updated `@blinksgg/canvas` to correct local file path + +### Removed +- **Dead code** — removed unused `cache.rs` module (196 lines) that was never compiled (no `mod cache;` declaration, missing `notify` crate dependency) + +### Changed +- **README** — replaced Vite template boilerplate with comprehensive project documentation +- **.gitignore** — added `src-tauri/target/` and `vault/` exclusions +- **Version** — bumped from 0.9.0 → 1.0.0 across `package.json`, `Cargo.toml`, and `tauri.conf.json` + ## [0.9.0] — 2026-03-09 ### Added @@ -173,27 +192,3 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/), and this - **Daily Notes** — Auto-generated daily journal entries accessible from sidebar shortcut - **Auto-Save** — Debounced 500ms save on every keystroke - **Custom Scrollbars** — Minimal 5px scrollbars matching the dark theme - -## [0.4.0] - 2026-03-07 (origin) - -### Added -- **VaultCache** (`src-tauri/src/cache.rs`): In-memory note cache with mtime-based invalidation -- **`init_vault_cache`/`get_cache_stats`**: Startup scan + diagnostics -- **Cache-backed commands**: `read_note`, `build_graph`, `search_vault` from cache -- **Frontend LRU cache** (`src/lib/noteCache.ts`): 20-note stale-while-revalidate -- **File watcher** (`notify` crate): FS change detection foundation - -## [0.3.0] - 2026-03-07 (origin) - -### Added -- **Frontmatter Parsing**: YAML metadata, `read_note_with_meta`, TOC panel, templates, attachments - -## [0.2.0] - 2026-03-07 (origin) - -### Added -- **Command Palette**, keyboard shortcuts, full-text search, rename/delete, graph filtering - -## [0.1.0] - 2025-06-01 (origin) - -### Added -- Tauri 2 + React 19, editor, wikilinks, graph, sidebar, backlinks, daily notes diff --git a/README.md b/README.md index 102e366..0868ceb 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,86 @@ -# Tauri + React + Typescript +# Graph Notes -This template should help get you started developing with Tauri, React and Typescript in Vite. +A local-first, graph-based note-taking app built with Tauri, React, and Rust. Think Obsidian meets Roam — your notes live as plain Markdown files on disk, connected by `[[wikilinks]]` and visualized as an interactive knowledge graph. -## Recommended IDE Setup +## Features -- [VS Code](https://code.visualstudio.com/) + [Tauri](https://marketplace.visualstudio.com/items?itemName=tauri-apps.tauri-vscode) + [rust-analyzer](https://marketplace.visualstudio.com/items?itemName=rust-lang.rust-analyzer) +**Core Editing** +- Rich contenteditable editor with inline Markdown styling +- `[[Wikilink]]` autocomplete with note creation on the fly +- Slash commands, heading folding, split-pane editing +- Tabbed editor with drag-reorder + +**Knowledge Graph** +- Force-directed graph visualization with semantic zoom +- Graph filtering, focus mode, orphan detection +- Local backlink graph and analytics dashboard + +**Organization** +- Sidebar file tree with drag-and-drop, folders, favorites, pinning +- Full-text search, command palette (`⌘K`), tags +- Calendar view, timeline view, kanban board + +**Advanced** +- Frontmatter properties panel and database views (table/gallery/list) +- Dataview queries — inline `TABLE ... SORT ...` blocks +- Canvas whiteboard for freeform visual thinking +- Spaced repetition flashcards (SM-2 scheduling) +- Note encryption (AES-256-GCM with Argon2 key derivation) +- Git sync (commit/push/pull panel) +- Version history with inline diff viewer + +**Customization** +- 5 built-in themes with live preview +- Custom CSS snippets editor +- Configurable keyboard shortcuts +- Workspace layouts (save/restore window arrangements) + +**Import / Export** +- Export vault as ZIP, individual notes as HTML or PDF +- Import Markdown folders from Obsidian, Notion, etc. + +## Tech Stack + +| Layer | Technology | +|---|---| +| Desktop shell | [Tauri v2](https://tauri.app) | +| Frontend | React 19, TypeScript, Vite | +| Backend | Rust (filesystem, search, encryption, graph) | +| Styling | Tailwind CSS 4 + custom design tokens | +| Graph engine | [@blinksgg/canvas](https://github.com/blinksgg), d3-force, graphology | + +## Development + +```bash +# Install dependencies +npm install + +# Run in development mode (starts both Vite dev server and Tauri) +npm run tauri dev + +# Build for production +npm run tauri build +``` + +### Prerequisites + +- [Node.js](https://nodejs.org) ≥ 18 +- [Rust](https://rustup.rs) toolchain +- [Tauri v2 prerequisites](https://v2.tauri.app/start/prerequisites/) + +## Project Structure + +``` +├── src/ # React frontend +│ ├── components/ # 33 UI components +│ ├── lib/ # Commands, frontmatter, wikilinks, note cache +│ └── index.css # Design system (tokens, component styles) +├── src-tauri/ # Rust backend +│ └── src/lib.rs # Tauri commands (notes, graph, search, encryption, git) +├── vault/ # Default vault directory (gitignored) +└── package.json +``` + +## License + +MIT diff --git a/package-lock.json b/package-lock.json index 3ad48b2..01a31bf 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,19 +1,20 @@ { "name": "graph-notes", - "version": "0.6.0", + "version": "1.0.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "graph-notes", - "version": "0.6.0", + "version": "1.0.0", "dependencies": { - "@blinksgg/canvas": "file:../blinksgg/gg-antifragile/packages/canvas", + "@blinksgg/canvas": "file:../space-operator/gg/packages/canvas", "@tauri-apps/api": "^2", "@tauri-apps/plugin-dialog": "^2", "@tauri-apps/plugin-fs": "^2", "@tauri-apps/plugin-opener": "^2", "d3-force": "^3.0.0", + "dompurify": "^3.3.2", "graphology": "^0.26.0", "highlight.js": "^11.11.1", "jotai": "^2.18.0", @@ -26,6 +27,7 @@ "devDependencies": { "@tailwindcss/vite": "^4.2.1", "@tauri-apps/cli": "^2", + "@types/dompurify": "^3.0.5", "@types/react": "^19.1.8", "@types/react-dom": "^19.1.6", "@vitejs/plugin-react": "^4.6.0", @@ -37,6 +39,7 @@ "../blinksgg/gg-antifragile/packages/canvas": { "name": "@blinksgg/canvas", "version": "0.13.0", + "extraneous": true, "dependencies": { "@supabase/supabase-js": "^2.49.5", "@use-gesture/react": "^10.3.1", @@ -83,6 +86,56 @@ "react-dom": "^19.0.0" } }, + "../space-operator/gg/packages/canvas": { + "name": "@blinksgg/canvas", + "version": "0.35.0", + "dependencies": { + "@supabase/supabase-js": "^2.49.5", + "@use-gesture/react": "^10.3.1", + "debug": "^4.4.3", + "graphology": "^0.26.0", + "graphology-types": "^0.24.8", + "jotai-family": "^1.0.1" + }, + "devDependencies": { + "@babel/core": "^7.29.0", + "@babel/preset-react": "^7.28.5", + "@babel/preset-typescript": "^7.28.5", + "@blocknote/core": "^0.45.0", + "@blocknote/react": "^0.45.0", + "@blocknote/shadcn": "^0.45.0", + "@tanstack/react-query": "^5.17.0", + "@testing-library/jest-dom": "^6.9.1", + "@testing-library/react": "^16.3.2", + "@testing-library/user-event": "^14.6.1", + "@types/d3-force": "^3.0.10", + "@types/debug": "^4.1.12", + "@types/node": "^24.5.2", + "@types/react": "^19.1.13", + "@types/react-dom": "^19.1.9", + "babel-plugin-react-compiler": "^1.0.0", + "d3-force": "^3.0.0", + "esbuild-plugin-babel": "^0.2.3", + "jotai": "^2.6.0", + "jsdom": "^26.1.0", + "react": "^19.1.1", + "react-dom": "^19.1.1", + "tsup": "^8.0.0", + "typescript": "^5.3.0", + "vitest": "^3.2.1" + }, + "peerDependencies": { + "@blocknote/core": "^0.45.0", + "@blocknote/react": "^0.45.0", + "@blocknote/shadcn": "^0.45.0", + "@tanstack/react-query": "^5.17.0", + "d3-force": "^3.0.0", + "jotai": "^2.6.0", + "jotai-tanstack-query": "*", + "react": "^19.0.0", + "react-dom": "^19.0.0" + } + }, "node_modules/@antfu/install-pkg": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@antfu/install-pkg/-/install-pkg-1.1.0.tgz", @@ -379,7 +432,7 @@ } }, "node_modules/@blinksgg/canvas": { - "resolved": "../blinksgg/gg-antifragile/packages/canvas", + "resolved": "../space-operator/gg/packages/canvas", "link": true }, "node_modules/@braintree/sanitize-url": { @@ -2126,6 +2179,16 @@ "@types/d3-selection": "*" } }, + "node_modules/@types/dompurify": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@types/dompurify/-/dompurify-3.0.5.tgz", + "integrity": "sha512-1Wg0g3BtQF7sSb27fJQAKck1HECM6zV1EB66j8JH9i3LCjYabJa0FSdiSgsD5K/RbrsR0SiraKacLB+T8ZVYAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/trusted-types": "*" + } + }, "node_modules/@types/estree": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", @@ -2163,8 +2226,8 @@ "version": "2.0.7", "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz", "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==", - "license": "MIT", - "optional": true + "devOptional": true, + "license": "MIT" }, "node_modules/@vitejs/plugin-react": { "version": "4.7.0", diff --git a/package.json b/package.json index e8cf630..88b81ca 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "graph-notes", "private": true, - "version": "0.9.0", + "version": "1.0.0", "type": "module", "scripts": { "dev": "vite", @@ -10,12 +10,13 @@ "tauri": "tauri" }, "dependencies": { - "@blinksgg/canvas": "file:../blinksgg/gg-antifragile/packages/canvas", + "@blinksgg/canvas": "file:../space-operator/gg/packages/canvas", "@tauri-apps/api": "^2", "@tauri-apps/plugin-dialog": "^2", "@tauri-apps/plugin-fs": "^2", "@tauri-apps/plugin-opener": "^2", "d3-force": "^3.0.0", + "dompurify": "^3.3.2", "graphology": "^0.26.0", "highlight.js": "^11.11.1", "jotai": "^2.18.0", @@ -28,6 +29,7 @@ "devDependencies": { "@tailwindcss/vite": "^4.2.1", "@tauri-apps/cli": "^2", + "@types/dompurify": "^3.0.5", "@types/react": "^19.1.8", "@types/react-dom": "^19.1.6", "@vitejs/plugin-react": "^4.6.0", @@ -35,4 +37,4 @@ "typescript": "~5.8.3", "vite": "^7.0.4" } -} \ No newline at end of file +} diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index 0a3cb12..619512a 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -8,6 +8,41 @@ version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" +[[package]] +name = "aead" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d122413f284cf2d62fb1b7db97e02edb8cda96d769b16e443a4f6195e35662b0" +dependencies = [ + "crypto-common", + "generic-array", +] + +[[package]] +name = "aes" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0" +dependencies = [ + "cfg-if", + "cipher", + "cpufeatures", +] + +[[package]] +name = "aes-gcm" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "831010a0f742e1209b3bcea8fab6a8e149051ba6099432c8cb2cc117dec3ead1" +dependencies = [ + "aead", + "aes", + "cipher", + "ctr", + "ghash", + "subtle", +] + [[package]] name = "aho-corasick" version = "1.1.4" @@ -47,6 +82,27 @@ version = "1.0.102" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" +[[package]] +name = "arbitrary" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3d036a3c4ab069c7b410a2ce876bd74808d2d0888a82667669f8e783a898bf1" +dependencies = [ + "derive_arbitrary", +] + +[[package]] +name = "argon2" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c3610892ee6e0cbce8ae2700349fcf8f98adb0dbfbee85aec3c9179d29cc072" +dependencies = [ + "base64ct", + "blake2", + "cpufeatures", + "password-hash", +] + [[package]] name = "async-broadcast" version = "0.7.2" @@ -225,6 +281,12 @@ version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" +[[package]] +name = "base64ct" +version = "1.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06" + [[package]] name = "bitflags" version = "1.3.2" @@ -240,6 +302,15 @@ dependencies = [ "serde_core", ] +[[package]] +name = "blake2" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46502ad458c9a52b69d4d4d32775c788b7a1b85e8bc9d482d92250fc0e3f8efe" +dependencies = [ + "digest", +] + [[package]] name = "block-buffer" version = "0.10.4" @@ -319,6 +390,25 @@ dependencies = [ "serde", ] +[[package]] +name = "bzip2" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49ecfb22d906f800d4fe833b6282cf4dc1c298f5057ca0b5445e5c209735ca47" +dependencies = [ + "bzip2-sys", +] + +[[package]] +name = "bzip2-sys" +version = "0.1.13+1.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "225bff33b2141874fe80d71e07d6eec4f85c5c216453dd96388240f96e1acc14" +dependencies = [ + "cc", + "pkg-config", +] + [[package]] name = "cairo-rs" version = "0.18.5" @@ -393,6 +483,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "aebf35691d1bfb0ac386a69bac2fde4dd276fb618cf8bf4f5318fe285e821bb2" dependencies = [ "find-msvc-tools", + "jobserver", + "libc", "shlex", ] @@ -443,6 +535,16 @@ dependencies = [ "windows-link 0.2.1", ] +[[package]] +name = "cipher" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" +dependencies = [ + "crypto-common", + "inout", +] + [[package]] name = "combine" version = "4.6.7" @@ -462,6 +564,12 @@ dependencies = [ "crossbeam-utils", ] +[[package]] +name = "constant_time_eq" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c74b8349d32d297c9134b8c88677813a227df8f779daa29bfc29c183fe3dca6" + [[package]] name = "convert_case" version = "0.4.0" @@ -527,6 +635,21 @@ dependencies = [ "libc", ] +[[package]] +name = "crc" +version = "3.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5eb8a2a1cd12ab0d987a5d5e825195d372001a4094a0376319d5a0ad71c1ba0d" +dependencies = [ + "crc-catalog", +] + +[[package]] +name = "crc-catalog" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19d374276b40fb8bbdee95aef7c7fa6b5316ec764510eb64b8dd0e2ed0d7e7f5" + [[package]] name = "crc32fast" version = "1.5.0" @@ -558,6 +681,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" dependencies = [ "generic-array", + "rand_core 0.6.4", "typenum", ] @@ -598,6 +722,15 @@ dependencies = [ "syn 2.0.117", ] +[[package]] +name = "ctr" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0369ee1ad671834580515889b80f2ea915f23b8be8d0daa4bbaf2ac5c7590835" +dependencies = [ + "cipher", +] + [[package]] name = "darling" version = "0.21.3" @@ -633,6 +766,12 @@ dependencies = [ "syn 2.0.117", ] +[[package]] +name = "deflate64" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "807800ff3288b621186fe0a8f3392c4652068257302709c24efd918c3dffcdc2" + [[package]] name = "deranged" version = "0.5.8" @@ -643,6 +782,17 @@ dependencies = [ "serde_core", ] +[[package]] +name = "derive_arbitrary" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e567bd82dcff979e4b03460c307b3cdc9e96fde3d73bed1496d2bc75d9dd62a" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + [[package]] name = "derive_more" version = "0.99.20" @@ -664,6 +814,7 @@ checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" dependencies = [ "block-buffer", "crypto-common", + "subtle", ] [[package]] @@ -889,17 +1040,6 @@ dependencies = [ "rustc_version", ] -[[package]] -name = "filetime" -version = "0.2.27" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f98844151eee8917efc50bd9e8318cb963ae8b297431495d3f758616ea5c57db" -dependencies = [ - "cfg-if", - "libc", - "libredox", -] - [[package]] name = "find-msvc-tools" version = "0.1.9" @@ -964,15 +1104,6 @@ dependencies = [ "percent-encoding", ] -[[package]] -name = "fsevent-sys" -version = "4.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76ee7a02da4d231650c7cea31349b889be2f45ddb3ef3032d2ec8185f6313fd2" -dependencies = [ - "libc", -] - [[package]] name = "futf" version = "0.1.5" @@ -1214,9 +1345,11 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" dependencies = [ "cfg-if", + "js-sys", "libc", "r-efi 5.3.0", "wasip2", + "wasm-bindgen", ] [[package]] @@ -1232,6 +1365,16 @@ dependencies = [ "wasip3", ] +[[package]] +name = "ghash" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0d8a4362ccb29cb0b265253fb0a2728f592895ee6854fd9bc13f2ffda266ff1" +dependencies = [ + "opaque-debug", + "polyval", +] + [[package]] name = "gio" version = "0.18.4" @@ -1330,10 +1473,13 @@ dependencies = [ [[package]] name = "graph-notes" -version = "0.1.0" +version = "1.0.0" dependencies = [ + "aes-gcm", + "argon2", + "base64 0.22.1", "chrono", - "notify", + "rand 0.8.5", "regex", "serde", "serde_json", @@ -1343,6 +1489,7 @@ dependencies = [ "tauri-plugin-fs", "tauri-plugin-opener", "walkdir", + "zip", ] [[package]] @@ -1442,6 +1589,15 @@ version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" +[[package]] +name = "hmac" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" +dependencies = [ + "digest", +] + [[package]] name = "html5ever" version = "0.29.1" @@ -1718,23 +1874,12 @@ dependencies = [ ] [[package]] -name = "inotify" -version = "0.9.6" +name = "inout" +version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8069d3ec154eb856955c1c0fbffefbf5f3c40a104ec912d4797314c1801abff" +checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01" dependencies = [ - "bitflags 1.3.2", - "inotify-sys", - "libc", -] - -[[package]] -name = "inotify-sys" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e05c02b5e89bff3b946cedeca278abc628fe811e604f027c45a8aa3cf793d0eb" -dependencies = [ - "libc", + "generic-array", ] [[package]] @@ -1823,6 +1968,16 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130" +[[package]] +name = "jobserver" +version = "0.1.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9afb3de4395d6b3e67a780b6de64b51c978ecf11cb9a462c66be7d4ca9039d33" +dependencies = [ + "getrandom 0.3.4", + "libc", +] + [[package]] name = "js-sys" version = "0.3.91" @@ -1866,26 +2021,6 @@ dependencies = [ "unicode-segmentation", ] -[[package]] -name = "kqueue" -version = "1.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eac30106d7dce88daf4a3fcb4879ea939476d5074a9b7ddd0fb97fa4bed5596a" -dependencies = [ - "kqueue-sys", - "libc", -] - -[[package]] -name = "kqueue-sys" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed9625ffda8729b85e45cf04090035ac368927b8cebc34898e7c120f52e4838b" -dependencies = [ - "bitflags 1.3.2", - "libc", -] - [[package]] name = "kuchikiki" version = "0.8.8-speedreader" @@ -1950,10 +2085,7 @@ version = "0.1.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1744e39d1d6a9948f4f388969627434e31128196de472883b39f148769bfe30a" dependencies = [ - "bitflags 2.11.0", "libc", - "plain", - "redox_syscall 0.7.3", ] [[package]] @@ -1983,6 +2115,27 @@ version = "0.4.29" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" +[[package]] +name = "lzma-rs" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "297e814c836ae64db86b36cf2a557ba54368d03f6afcd7d947c266692f71115e" +dependencies = [ + "byteorder", + "crc", +] + +[[package]] +name = "lzma-sys" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5fda04ab3764e6cde78b9974eec4f779acaba7c4e84b36eca3cf77c581b85d27" +dependencies = [ + "cc", + "libc", + "pkg-config", +] + [[package]] name = "mac" version = "0.1.1" @@ -2051,18 +2204,6 @@ dependencies = [ "simd-adler32", ] -[[package]] -name = "mio" -version = "0.8.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4a650543ca06a924e8b371db273b2756685faae30f8487da1b56505a8f78b0c" -dependencies = [ - "libc", - "log", - "wasi 0.11.1+wasi-snapshot-preview1", - "windows-sys 0.48.0", -] - [[package]] name = "mio" version = "1.1.1" @@ -2137,25 +2278,6 @@ version = "0.1.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72ef4a56884ca558e5ddb05a1d1e7e1bfd9a68d9ed024c21704cc98872dae1bb" -[[package]] -name = "notify" -version = "6.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6205bd8bb1e454ad2e27422015fb5e4f2bcc7e08fa8f27058670d208324a4d2d" -dependencies = [ - "bitflags 2.11.0", - "crossbeam-channel", - "filetime", - "fsevent-sys", - "inotify", - "kqueue", - "libc", - "log", - "mio 0.8.11", - "walkdir", - "windows-sys 0.48.0", -] - [[package]] name = "num-conv" version = "0.2.0" @@ -2322,6 +2444,12 @@ version = "1.21.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" +[[package]] +name = "opaque-debug" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381" + [[package]] name = "open" version = "5.3.3" @@ -2399,17 +2527,38 @@ checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" dependencies = [ "cfg-if", "libc", - "redox_syscall 0.5.18", + "redox_syscall", "smallvec", "windows-link 0.2.1", ] +[[package]] +name = "password-hash" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "346f04948ba92c43e8469c1ee6736c7563d71012b17d40745260fe106aac2166" +dependencies = [ + "base64ct", + "rand_core 0.6.4", + "subtle", +] + [[package]] name = "pathdiff" version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "df94ce210e5bc13cb6651479fa48d14f601d9858cfe0467f43ae157023b938d3" +[[package]] +name = "pbkdf2" +version = "0.12.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8ed6a7761f76e3b9f92dfb0a60a6a6477c61024b775147ff0973a02653abaf2" +dependencies = [ + "digest", + "hmac", +] + [[package]] name = "percent-encoding" version = "2.3.2" @@ -2579,12 +2728,6 @@ version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" -[[package]] -name = "plain" -version = "0.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b4596b6d070b27117e987119b4dac604f3c58cfb0b191112e24771b2faeac1a6" - [[package]] name = "plist" version = "1.8.0" @@ -2625,6 +2768,18 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "polyval" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d1fe60d06143b2430aa532c94cfe9e29783047f06c0d7fd359a9a51b729fa25" +dependencies = [ + "cfg-if", + "cpufeatures", + "opaque-debug", + "universal-hash", +] + [[package]] name = "potential_utf" version = "0.1.4" @@ -2859,15 +3014,6 @@ dependencies = [ "bitflags 2.11.0", ] -[[package]] -name = "redox_syscall" -version = "0.7.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ce70a74e890531977d37e532c34d45e9055d2409ed08ddba14529471ed0be16" -dependencies = [ - "bitflags 2.11.0", -] - [[package]] name = "redox_users" version = "0.5.2" @@ -3266,6 +3412,17 @@ dependencies = [ "stable_deref_trait", ] +[[package]] +name = "sha1" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + [[package]] name = "sha2" version = "0.10.9" @@ -3348,7 +3505,7 @@ dependencies = [ "objc2-foundation", "objc2-quartz-core", "raw-window-handle", - "redox_syscall 0.5.18", + "redox_syscall", "tracing", "wasm-bindgen", "web-sys", @@ -3418,6 +3575,12 @@ version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + [[package]] name = "swift-rs" version = "1.0.7" @@ -3945,7 +4108,7 @@ checksum = "27ad5e34374e03cfffefc301becb44e9dc3c17584f414349ebe29ed26661822d" dependencies = [ "bytes", "libc", - "mio 1.1.1", + "mio", "pin-project-lite", "socket2", "windows-sys 0.61.2", @@ -4255,6 +4418,16 @@ version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" +[[package]] +name = "universal-hash" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc1de2c688dc15305988b563c3854064043356019f97a4b46276fe734c4f07ea" +dependencies = [ + "crypto-common", + "subtle", +] + [[package]] name = "url" version = "2.5.8" @@ -4775,15 +4948,6 @@ dependencies = [ "windows-targets 0.42.2", ] -[[package]] -name = "windows-sys" -version = "0.48.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" -dependencies = [ - "windows-targets 0.48.5", -] - [[package]] name = "windows-sys" version = "0.59.0" @@ -4826,21 +4990,6 @@ dependencies = [ "windows_x86_64_msvc 0.42.2", ] -[[package]] -name = "windows-targets" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" -dependencies = [ - "windows_aarch64_gnullvm 0.48.5", - "windows_aarch64_msvc 0.48.5", - "windows_i686_gnu 0.48.5", - "windows_i686_msvc 0.48.5", - "windows_x86_64_gnu 0.48.5", - "windows_x86_64_gnullvm 0.48.5", - "windows_x86_64_msvc 0.48.5", -] - [[package]] name = "windows-targets" version = "0.52.6" @@ -4898,12 +5047,6 @@ version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" -[[package]] -name = "windows_aarch64_gnullvm" -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" @@ -4922,12 +5065,6 @@ version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" -[[package]] -name = "windows_aarch64_msvc" -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" @@ -4946,12 +5083,6 @@ version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" -[[package]] -name = "windows_i686_gnu" -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" @@ -4982,12 +5113,6 @@ version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" -[[package]] -name = "windows_i686_msvc" -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" @@ -5006,12 +5131,6 @@ version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" -[[package]] -name = "windows_x86_64_gnu" -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" @@ -5030,12 +5149,6 @@ version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" -[[package]] -name = "windows_x86_64_gnullvm" -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" @@ -5054,12 +5167,6 @@ version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" -[[package]] -name = "windows_x86_64_msvc" -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" @@ -5260,6 +5367,15 @@ dependencies = [ "pkg-config", ] +[[package]] +name = "xz2" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "388c44dc09d76f1536602ead6d325eb532f5c122f17782bd57fb47baeeb767e2" +dependencies = [ + "lzma-sys", +] + [[package]] name = "yoke" version = "0.8.1" @@ -5385,6 +5501,26 @@ dependencies = [ "synstructure", ] +[[package]] +name = "zeroize" +version = "1.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" +dependencies = [ + "zeroize_derive", +] + +[[package]] +name = "zeroize_derive" +version = "1.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85a5b4158499876c763cb03bc4e49185d3cccbabb15b33c627f7884f43db852e" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + [[package]] name = "zerotrie" version = "0.2.3" @@ -5418,12 +5554,82 @@ dependencies = [ "syn 2.0.117", ] +[[package]] +name = "zip" +version = "2.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fabe6324e908f85a1c52063ce7aa26b68dcb7eb6dbc83a2d148403c9bc3eba50" +dependencies = [ + "aes", + "arbitrary", + "bzip2", + "constant_time_eq", + "crc32fast", + "crossbeam-utils", + "deflate64", + "displaydoc", + "flate2", + "getrandom 0.3.4", + "hmac", + "indexmap 2.13.0", + "lzma-rs", + "memchr", + "pbkdf2", + "sha1", + "thiserror 2.0.18", + "time", + "xz2", + "zeroize", + "zopfli", + "zstd", +] + [[package]] name = "zmij" version = "1.0.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" +[[package]] +name = "zopfli" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f05cd8797d63865425ff89b5c4a48804f35ba0ce8d125800027ad6017d2b5249" +dependencies = [ + "bumpalo", + "crc32fast", + "log", + "simd-adler32", +] + +[[package]] +name = "zstd" +version = "0.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e91ee311a569c327171651566e07972200e76fcfe2242a4fa446149a3881c08a" +dependencies = [ + "zstd-safe", +] + +[[package]] +name = "zstd-safe" +version = "7.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f49c4d5f0abb602a93fb8736af2a4f4dd9512e36f7f570d66e65ff867ed3b9d" +dependencies = [ + "zstd-sys", +] + +[[package]] +name = "zstd-sys" +version = "2.0.16+zstd.1.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e19ebc2adc8f83e43039e79776e3fda8ca919132d68a1fed6a5faca2683748" +dependencies = [ + "cc", + "pkg-config", +] + [[package]] name = "zvariant" version = "5.10.0" diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 001f414..5560b99 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "graph-notes" -version = "0.9.0" +version = "1.0.0" description = "A graph-based note-taking app" authors = ["you"] edition = "2021" diff --git a/src-tauri/src/cache.rs b/src-tauri/src/cache.rs deleted file mode 100644 index 1352eab..0000000 --- a/src-tauri/src/cache.rs +++ /dev/null @@ -1,195 +0,0 @@ -use std::collections::HashMap; -use std::fs; -use std::path::{Path, PathBuf}; -use std::sync::{Arc, Mutex}; -use std::time::SystemTime; - -use notify::{RecommendedWatcher, RecursiveMode, Watcher, Event, EventKind}; -use walkdir::WalkDir; - -use crate::{NoteMeta, HeadingEntry, parse_frontmatter, extract_headings, extract_wikilinks}; - -/* ── Cached Note ───────────────────────────────────────────── */ - -#[derive(Debug, Clone)] -pub struct CachedNote { - pub content: String, - pub meta: NoteMeta, - pub body: String, - pub headings: Vec, - pub links: Vec, - pub mtime: SystemTime, - pub rel_path: String, - pub name: String, -} - -/* ── Vault Cache ───────────────────────────────────────────── */ - -pub struct VaultCache { - pub vault_path: PathBuf, - pub entries: HashMap, - pub hits: u64, - pub misses: u64, -} - -impl VaultCache { - pub fn new(vault_path: &str) -> Self { - VaultCache { - vault_path: PathBuf::from(vault_path), - entries: HashMap::new(), - hits: 0, - misses: 0, - } - } - - /// Scan all .md files in the vault and populate the cache. - pub fn scan_all(&mut self) { - let vault = self.vault_path.clone(); - if !vault.exists() { - return; - } - - // Collect paths first to avoid borrow conflict - let paths: Vec = WalkDir::new(&vault) - .into_iter() - .filter_map(|e| e.ok()) - .filter(|e| { - e.path().extension().map_or(false, |ext| ext == "md") - && !e.path().strip_prefix(&vault) - .map_or(false, |p| { - let s = p.to_string_lossy(); - s.starts_with("_templates") || s.starts_with("attachments") - }) - }) - .filter_map(|entry| { - entry - .path() - .strip_prefix(&vault) - .ok() - .map(|p| p.to_string_lossy().to_string()) - }) - .collect(); - - for rel in paths { - self.load_entry(&rel); - } - } - - /// Load or reload a single entry from disk. - pub fn load_entry(&mut self, rel_path: &str) -> Option<&CachedNote> { - let full = self.vault_path.join(rel_path); - let mtime = fs::metadata(&full).ok()?.modified().ok()?; - let content = fs::read_to_string(&full).ok()?; - - let (meta, body) = parse_frontmatter(&content); - let headings = extract_headings(&content); - let links = extract_wikilinks(&content); - let name = Path::new(rel_path) - .file_stem() - .unwrap_or_default() - .to_string_lossy() - .to_string(); - - self.entries.insert( - rel_path.to_string(), - CachedNote { - content, - meta, - body, - headings, - links, - mtime, - rel_path: rel_path.to_string(), - name, - }, - ); - - self.entries.get(rel_path) - } - - /// Get a cached entry, reloading from disk if mtime changed. - pub fn get(&mut self, rel_path: &str) -> Option<&CachedNote> { - let full = self.vault_path.join(rel_path); - let disk_mtime = fs::metadata(&full).ok().and_then(|m| m.modified().ok()); - - if let Some(cached) = self.entries.get(rel_path) { - if let Some(dm) = disk_mtime { - if cached.mtime >= dm { - self.hits += 1; - return self.entries.get(rel_path); - } - } - } - - self.misses += 1; - self.load_entry(rel_path) - } - - /// Invalidate (remove) a single entry. - pub fn invalidate(&mut self, rel_path: &str) { - self.entries.remove(rel_path); - } - - /// Invalidate and reload an entry (for file changes). - pub fn refresh(&mut self, rel_path: &str) { - self.entries.remove(rel_path); - let full = self.vault_path.join(rel_path); - if full.exists() { - self.load_entry(rel_path); - } - } -} - -/* ── File Watcher ──────────────────────────────────────────── */ - -pub type SharedCache = Arc>; - -/// Start a filesystem watcher on the vault directory. -/// Returns the watcher handle (must be kept alive) and processes events -/// by invalidating cache entries and calling the callback. -pub fn start_watcher( - cache: SharedCache, - vault_path: &str, - on_change: F, -) -> Option -where - F: Fn(Vec) + Send + 'static, -{ - let vault = PathBuf::from(vault_path); - let vault_for_closure = vault.clone(); - let cache_for_closure = cache.clone(); - - let mut watcher = notify::recommended_watcher(move |res: Result| { - if let Ok(event) = res { - match event.kind { - EventKind::Create(_) | EventKind::Modify(_) | EventKind::Remove(_) => { - let mut changed: Vec = Vec::new(); - - for path in &event.paths { - if path.extension().map_or(false, |e| e == "md") { - if let Ok(rel) = path.strip_prefix(&vault_for_closure) { - let rel_str = rel.to_string_lossy().to_string(); - if let Ok(mut c) = cache_for_closure.lock() { - match event.kind { - EventKind::Remove(_) => c.invalidate(&rel_str), - _ => c.refresh(&rel_str), - } - } - changed.push(rel_str); - } - } - } - - if !changed.is_empty() { - on_change(changed); - } - } - _ => {} - } - } - }) - .ok()?; - - watcher.watch(vault.as_path(), RecursiveMode::Recursive).ok()?; - Some(watcher) -} diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 37bb4cb..ad55ffa 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -1,6 +1,7 @@ use serde::{Deserialize, Serialize}; use std::fs; use std::path::{Path, PathBuf}; +use std::sync::LazyLock; use walkdir::WalkDir; use regex::Regex; use chrono::Local; @@ -37,9 +38,12 @@ fn normalize_note_name(name: &str) -> String { name.trim().to_lowercase() } +static WIKILINK_RE: LazyLock = LazyLock::new(|| { + Regex::new(r"\[\[([^\]|]+)(?:\|[^\]]+)?\]\]").unwrap() +}); + fn extract_wikilinks(content: &str) -> Vec { - let re = Regex::new(r"\[\[([^\]|]+)(?:\|[^\]]+)?\]\]").unwrap(); - re.captures_iter(content) + WIKILINK_RE.captures_iter(content) .map(|cap| cap[1].trim().to_string()) .collect() } @@ -94,15 +98,39 @@ fn list_notes(vault_path: String) -> Result, String> { Ok(build_tree(vault, vault)) } +/// Validate that a relative path stays within the vault root. +/// Returns the canonical full path, or an error if it escapes. +fn safe_vault_path(vault_path: &str, relative_path: &str) -> Result { + let vault = Path::new(vault_path) + .canonicalize() + .map_err(|e| format!("Invalid vault path: {}", e))?; + let full = vault.join(relative_path); + // Canonicalize only works if the file exists; for new files, normalize manually + let normalized = if full.exists() { + full.canonicalize().map_err(|e| format!("Invalid path: {}", e))? + } else { + // For new files: resolve parent (must exist) + filename + let parent = full.parent() + .ok_or_else(|| "Invalid path".to_string())?; + let parent_canon = parent.canonicalize() + .unwrap_or_else(|_| parent.to_path_buf()); + parent_canon.join(full.file_name().unwrap_or_default()) + }; + if !normalized.starts_with(&vault) { + return Err("Path escapes the vault directory".to_string()); + } + Ok(normalized) +} + #[tauri::command] fn read_note(vault_path: String, relative_path: String) -> Result { - let full_path = Path::new(&vault_path).join(&relative_path); + let full_path = safe_vault_path(&vault_path, &relative_path)?; fs::read_to_string(&full_path).map_err(|e| format!("Failed to read note: {}", e)) } #[tauri::command] fn write_note(vault_path: String, relative_path: String, content: String) -> Result<(), String> { - let full_path = Path::new(&vault_path).join(&relative_path); + let full_path = safe_vault_path(&vault_path, &relative_path)?; // Ensure parent directory exists if let Some(parent) = full_path.parent() { @@ -114,7 +142,7 @@ fn write_note(vault_path: String, relative_path: String, content: String) -> Res #[tauri::command] fn delete_note(vault_path: String, relative_path: String) -> Result<(), String> { - let full_path = Path::new(&vault_path).join(&relative_path); + let full_path = safe_vault_path(&vault_path, &relative_path)?; if full_path.is_file() { fs::remove_file(&full_path).map_err(|e| format!("Failed to delete note: {}", e)) } else { @@ -367,9 +395,12 @@ pub struct TagInfo { pub notes: Vec, } +static TAG_RE: LazyLock = LazyLock::new(|| { + Regex::new(r"(?:^|[\s,;(])(#[a-zA-Z][a-zA-Z0-9_/-]*)").unwrap() +}); + fn extract_tags(content: &str) -> Vec { - let re = Regex::new(r"(?:^|[\s,;(])(#[a-zA-Z][a-zA-Z0-9_/-]*)").unwrap(); - let mut tags: Vec = re + let mut tags: Vec = TAG_RE .captures_iter(content) .map(|cap| cap[1].to_string()) .collect(); @@ -1302,7 +1333,7 @@ fn load_fold_state(vault_path: String, note_path: String) -> Result, #[tauri::command] fn get_custom_css() -> Result { - let config_dir = dirs_config_path(); + let config_dir = dirs_config_dir(); let css_path = config_dir.join("custom.css"); if css_path.exists() { fs::read_to_string(&css_path).map_err(|e| e.to_string()) @@ -1313,15 +1344,11 @@ fn get_custom_css() -> Result { #[tauri::command] fn set_custom_css(css: String) -> Result<(), String> { - let config_dir = dirs_config_path(); + let config_dir = dirs_config_dir(); fs::create_dir_all(&config_dir).map_err(|e| e.to_string())?; fs::write(config_dir.join("custom.css"), css).map_err(|e| e.to_string()) } -fn dirs_config_path() -> PathBuf { - dirs::config_dir().unwrap_or_else(|| PathBuf::from(".")).join("graph-notes") -} - /* ── Workspace Layouts ──────────────────────────────────── */ #[tauri::command] diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index 66c196d..dca54ee 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -1,7 +1,7 @@ { "$schema": "https://schema.tauri.app/config/2", "productName": "Graph Notes", - "version": "0.9.0", + "version": "1.0.0", "identifier": "com.graphnotes.app", "build": { "beforeDevCommand": "npm run dev", @@ -22,7 +22,7 @@ } ], "security": { - "csp": null + "csp": "default-src 'self' tauri: https://tauri.localhost; script-src 'self' 'unsafe-eval' 'unsafe-inline'; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; font-src 'self' https://fonts.gstatic.com data:; img-src 'self' asset: https://asset.localhost http://asset.localhost blob: data: tauri: https://tauri.localhost; connect-src 'self' ipc: http://ipc.localhost tauri: https://tauri.localhost" } }, "plugins": { diff --git a/src/App.tsx b/src/App.tsx index 1fbe928..9699175 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -13,7 +13,7 @@ import { KanbanView } from "./components/KanbanView"; import { SearchReplace } from "./components/SearchReplace"; import { FlashcardView } from "./components/FlashcardView"; import { CSSEditor, useCustomCssInit } from "./components/CSSEditor"; -import { TabBar, type Tab } from "./components/TabBar"; +import { TabBar } from "./components/TabBar"; import { WhiteboardView } from "./components/WhiteboardView"; import { DatabaseView } from "./components/DatabaseView"; import { GitPanel } from "./components/GitPanel"; @@ -82,8 +82,6 @@ export default function App() { const [focusMode, setFocusMode] = useState(false); const [searchReplaceOpen, setSearchReplaceOpen] = useState(false); const [cssEditorOpen, setCssEditorOpen] = useState(false); - const [tabs, setTabs] = useState([]); - const [activeTab, setActiveTab] = useState(0); const navigate = useNavigate(); // Apply saved theme + custom CSS on mount @@ -110,8 +108,8 @@ export default function App() { } if (!path) { - // Default to the project's vault directory - path = "/home/amir/code/notes/vault"; + // No stored vault path — use a sensible default + path = "./vault"; console.log("[GraphNotes] Using default vault path:", path); try { await setVaultPath(path); @@ -318,6 +316,10 @@ export default function App() { e.preventDefault(); setSearchReplaceOpen(v => !v); break; + case "u": + e.preventDefault(); + setCssEditorOpen(v => !v); + break; } }; window.addEventListener("keydown", handler); @@ -398,27 +400,6 @@ export default function App() { ); } -/* ── Note View ──────────────────────────────────────────────── */ -function NoteView() { - const { path } = useParams<{ path: string }>(); - const { vaultPath, setCurrentNote, noteContent, setNoteContent } = useVault(); - const decodedPath = decodeURIComponent(path || ""); - - useEffect(() => { - if (!decodedPath || !vaultPath) return; - setCurrentNote(decodedPath); - readNote(vaultPath, decodedPath).then(setNoteContent).catch(() => setNoteContent("")); - }, [decodedPath, vaultPath, setCurrentNote, setNoteContent]); - - return ( -
-
- -
- -
- ); -} /* ── Daily View ─────────────────────────────────────────────── */ function DailyView() { diff --git a/src/components/Editor.tsx b/src/components/Editor.tsx index f7664bb..619f049 100644 --- a/src/components/Editor.tsx +++ b/src/components/Editor.tsx @@ -3,6 +3,7 @@ import { useVault } from "../App"; import { writeNote, saveAttachment, saveSnapshot, getWritingGoal, setWritingGoal as setWritingGoalCmd, isEncrypted, encryptNote, decryptNote } from "../lib/commands"; import { extractWikilinks } from "../lib/wikilinks"; import { marked } from "marked"; +import DOMPurify from "dompurify"; import { PropertiesPanel } from "./PropertiesPanel"; import { TableOfContents } from "./TableOfContents"; import { SlashMenu } from "./SlashMenu"; @@ -59,20 +60,35 @@ export function Editor() { ) : []; + // Refs to avoid stale closures in debounced save + const currentNoteRef = useRef(currentNote); + const vaultPathRef = useRef(vaultPath); + currentNoteRef.current = currentNote; + vaultPathRef.current = vaultPath; + const lastSnapshotRef2 = useRef(0); + // ── Save with debounce ── const saveContent = useCallback( (value: string) => { setNoteContent(value); if (saveTimeoutRef.current) clearTimeout(saveTimeoutRef.current); saveTimeoutRef.current = setTimeout(async () => { - if (vaultPath && currentNote) { + const v = vaultPathRef.current; + const n = currentNoteRef.current; + if (v && n) { setIsSaving(true); - await writeNote(vaultPath, currentNote, value); + await writeNote(v, n, value); setIsSaving(false); + // Auto-snapshot on save (max 1 per 5 min) + const now = Date.now(); + if (now - lastSnapshotRef2.current > 5 * 60 * 1000 && value.length > 50) { + lastSnapshotRef2.current = now; + saveSnapshot(v, n).catch(() => { }); + } } }, 500); }, - [vaultPath, currentNote, setNoteContent] + [setNoteContent] ); // ── Extract raw markdown from contenteditable DOM ── @@ -415,10 +431,11 @@ export function Editor() { /\[\[([^\]|]+)(?:\|([^\]]+))?\]\]/g, (_m, target, display) => { const label = display?.trim() || target.trim(); - return `${label}`; + const safeTarget = target.trim().replace(/"/g, '"'); + return `${DOMPurify.sanitize(label)}`; } ); - return html; + return DOMPurify.sanitize(html, { ADD_ATTR: ['data-target'] }); })(); // Mermaid post-processing (render in a separate effect) @@ -492,15 +509,7 @@ export function Editor() { }).catch(() => { }); }, [isPreview, renderedMarkdown]); - // Auto-snapshot on save (max 1 per 5 min) - useEffect(() => { - if (!vaultPath || !currentNote || !noteContent) return; - const now = Date.now(); - if (now - lastSnapshotRef.current > 5 * 60 * 1000 && noteContent.length > 50) { - lastSnapshotRef.current = now; - saveSnapshot(vaultPath, currentNote).catch(() => { }); - } - }, [vaultPath, currentNote, noteContent]); + // Snapshot logic is now in the saveContent debounce callback above // Load writing goal useEffect(() => { @@ -514,25 +523,16 @@ export function Editor() { isEncrypted(vaultPath, currentNote).then(setNoteEncrypted).catch(() => setNoteEncrypted(false)); }, [vaultPath, currentNote]); - // Widget rendering in preview + // Widget rendering in preview (single pass to avoid clobbering DOM) useEffect(() => { if (!isPreview || !mermaidRef.current) return; const container = mermaidRef.current; - // {{progress:N}} → progress bar - container.innerHTML = container.innerHTML.replace( - /\{\{progress:(\d+)\}\}/g, - (_, n) => `
${n}%
` - ); - // {{counter:N}} → counter badge - container.innerHTML = container.innerHTML.replace( - /\{\{counter:(\d+)\}\}/g, - (_, n) => `${n}` - ); - // {{toggle:on/off}} → toggle indicator - container.innerHTML = container.innerHTML.replace( - /\{\{toggle:(on|off)\}\}/g, - (_, state) => `${state === 'on' ? '●' : '○'}` - ); + let html = container.innerHTML; + html = html + .replace(/\{\{progress:(\d+)\}\}/g, (_, n) => `
${n}%
`) + .replace(/\{\{counter:(\d+)\}\}/g, (_, n) => `${n}`) + .replace(/\{\{toggle:(on|off)\}\}/g, (_, state) => `${state === 'on' ? '●' : '○'}`); + if (html !== container.innerHTML) container.innerHTML = html; }, [isPreview, renderedMarkdown]); // Right-click for refactoring @@ -570,12 +570,9 @@ export function Editor() { range.insertNode(textNode); range.collapse(false); } - // Trigger save - const raw = ceRef.current?.innerText || ""; - setNoteContent(raw); - if (currentNote) { - await writeNote(vaultPath, currentNote, raw); - } + // Trigger save using same extraction as regular input + const raw = domToMarkdown(ceRef.current!); + saveContent(raw); } catch (err) { console.error("Image paste failed:", err); } @@ -847,8 +844,7 @@ export function Editor() { setNoteEncrypted(false); setLockScreenOpen(false); setLockError(null); - // Save decrypted - await writeNote(vaultPath, currentNote, content); + // Note: decrypted content is only held in memory, not written to disk } catch { setLockError("Wrong password"); } diff --git a/src/components/GraphView.tsx b/src/components/GraphView.tsx index 226ec2f..a196665 100644 --- a/src/components/GraphView.tsx +++ b/src/components/GraphView.tsx @@ -1,4 +1,4 @@ -import { useEffect, useState, useCallback } from "react"; +import { useEffect, useState, useCallback, useRef } from "react"; import { useNavigate } from "react-router-dom"; import { Provider as JotaiProvider } from "jotai"; import { Canvas, CanvasStyleProvider, registerBuiltinCommands } from "@blinksgg/canvas"; @@ -60,8 +60,12 @@ export function GraphView() { ), []); - const handleNodeClick = useCallback((nodeId: string) => { - navigate(`/note/${encodeURIComponent(nodeId)}`); + // Navigate to selected node + const handleSelectionChange = useCallback((selectedNodeIds: Set) => { + if (selectedNodeIds.size === 1) { + const [nodeId] = selectedNodeIds; + navigate(`/note/${encodeURIComponent(nodeId)}`); + } }, [navigate]); return ( @@ -70,7 +74,7 @@ export function GraphView() {
diff --git a/src/components/Sidebar.tsx b/src/components/Sidebar.tsx index 53b5567..e5fb65b 100644 --- a/src/components/Sidebar.tsx +++ b/src/components/Sidebar.tsx @@ -168,9 +168,7 @@ export function Sidebar() { if (newPath === sourcePath) return; try { - const oldName = sourcePath.replace(/\.md$/, "").split("/").pop() || ""; await renameNote(vaultPath, sourcePath, newPath); - await updateWikilinks(vaultPath, oldName, oldName); await refreshNotes(); if (location.pathname === `/note/${encodeURIComponent(sourcePath)}`) { navigate(`/note/${encodeURIComponent(newPath)}`, { replace: true }); diff --git a/src/components/WhiteboardView.tsx b/src/components/WhiteboardView.tsx index c8dbbf3..2ab1048 100644 --- a/src/components/WhiteboardView.tsx +++ b/src/components/WhiteboardView.tsx @@ -1,20 +1,18 @@ -import { useEffect, useState, useCallback } from "react"; +import { useCallback } from "react"; import { useParams } from "react-router-dom"; import { Provider as JotaiProvider } from "jotai"; import { Canvas, CanvasStyleProvider, registerBuiltinCommands, ViewportControls } from "@blinksgg/canvas"; import { useVault } from "../App"; -import { saveCanvas, loadCanvas } from "../lib/commands"; +import { saveCanvas } from "../lib/commands"; registerBuiltinCommands(); -const CARD_COLORS = ["#8b5cf6", "#3b82f6", "#10b981", "#f59e0b", "#f43f5e", "#ec4899"]; - /** * WhiteboardView — Freeform visual thinking canvas. */ export function WhiteboardView() { const { name } = useParams<{ name: string }>(); - const { vaultPath, navigateToNote } = useVault(); + const { vaultPath } = useVault(); const renderNode = useCallback(({ node, isSelected }: any) => { const nodeType = node.dbData?.node_type || "card"; @@ -53,13 +51,6 @@ export function WhiteboardView() {
{ - const label = nodeData?.label || (nodeData as any)?.dbData?.label; - if (label) navigateToNote(label); - }} - onBackgroundDoubleClick={(worldPos) => { - // Could add node creation here - }} minZoom={0.1} maxZoom={5} >