├── .gitignore ├── karta_server ├── src │ ├── layout │ │ ├── mod.rs │ │ └── default_layouts.rs │ ├── context │ │ ├── context_settings.rs │ │ ├── mod.rs │ │ └── context.rs │ ├── main.rs │ ├── graph_traits │ │ ├── mod.rs │ │ └── graph_edge.rs │ ├── lib.rs │ ├── elements │ │ ├── mod.rs │ │ ├── view_node.rs │ │ └── nodetype.rs │ ├── server │ │ ├── write_endpoints_tests │ │ │ ├── mod.rs │ │ │ ├── test_node_creation.rs │ │ │ ├── test_helpers.rs │ │ │ └── test_context.rs │ │ ├── asset_endpoints.rs │ │ ├── search_endpoints.rs │ │ ├── edge_endpoints.rs │ │ └── settings.rs │ ├── graph_agdb │ │ ├── mod.rs │ │ └── graph_core.rs │ └── fs_reader │ │ └── mod.rs ├── fs_graph.code-workspace ├── .vscode │ └── launch.json ├── .gitignore ├── Cargo.toml ├── docs │ └── perf_reports │ │ ├── fs_graph_test_opening_folder_connections__indexes_folder_contents.ron │ │ └── fs_graph_test_node_context_can_be_indexed.ron └── README.md ├── karta_svelte ├── .npmrc ├── src-tauri │ ├── build.rs │ ├── icons │ │ ├── 32x32.png │ │ ├── 64x64.png │ │ ├── icon.icns │ │ ├── icon.ico │ │ ├── icon.png │ │ ├── 128x128.png │ │ ├── 128x128@2x.png │ │ ├── StoreLogo.png │ │ ├── icon_1024.png │ │ ├── Square30x30Logo.png │ │ ├── Square44x44Logo.png │ │ ├── Square71x71Logo.png │ │ ├── Square89x89Logo.png │ │ ├── Square107x107Logo.png │ │ ├── Square142x142Logo.png │ │ ├── Square150x150Logo.png │ │ ├── Square284x284Logo.png │ │ ├── Square310x310Logo.png │ │ ├── ios │ │ │ ├── AppIcon-512@2x.png │ │ │ ├── AppIcon-20x20@1x.png │ │ │ ├── AppIcon-20x20@2x.png │ │ │ ├── AppIcon-20x20@3x.png │ │ │ ├── AppIcon-29x29@1x.png │ │ │ ├── AppIcon-29x29@2x.png │ │ │ ├── AppIcon-29x29@3x.png │ │ │ ├── AppIcon-40x40@1x.png │ │ │ ├── AppIcon-40x40@2x.png │ │ │ ├── AppIcon-40x40@3x.png │ │ │ ├── AppIcon-60x60@2x.png │ │ │ ├── AppIcon-60x60@3x.png │ │ │ ├── AppIcon-76x76@1x.png │ │ │ ├── AppIcon-76x76@2x.png │ │ │ ├── AppIcon-20x20@2x-1.png │ │ │ ├── AppIcon-29x29@2x-1.png │ │ │ ├── AppIcon-40x40@2x-1.png │ │ │ └── AppIcon-83.5x83.5@2x.png │ │ └── android │ │ │ ├── mipmap-hdpi │ │ │ ├── ic_launcher.png │ │ │ ├── ic_launcher_round.png │ │ │ └── ic_launcher_foreground.png │ │ │ ├── mipmap-mdpi │ │ │ ├── ic_launcher.png │ │ │ ├── ic_launcher_round.png │ │ │ └── ic_launcher_foreground.png │ │ │ ├── mipmap-xhdpi │ │ │ ├── ic_launcher.png │ │ │ ├── ic_launcher_round.png │ │ │ └── ic_launcher_foreground.png │ │ │ ├── mipmap-xxhdpi │ │ │ ├── ic_launcher.png │ │ │ ├── ic_launcher_round.png │ │ │ └── ic_launcher_foreground.png │ │ │ └── mipmap-xxxhdpi │ │ │ ├── ic_launcher.png │ │ │ ├── ic_launcher_round.png │ │ │ └── ic_launcher_foreground.png │ ├── src │ │ └── main.rs │ ├── .gitignore │ ├── capabilities │ │ └── default.json │ ├── Entitlements.plist │ ├── Cargo.toml │ └── tauri.conf.json ├── src │ ├── lib │ │ ├── debug │ │ │ ├── index.ts │ │ │ ├── config.ts │ │ │ └── loggers.ts │ │ ├── index.ts │ │ ├── constants.ts │ │ ├── stores │ │ │ └── TutorialStore.ts │ │ ├── apiBase.ts │ │ ├── karta │ │ │ ├── VaultStore.ts │ │ │ ├── NotificationStore.ts │ │ │ ├── HistoryStore.ts │ │ │ ├── EdgeSelectionStore.ts │ │ │ ├── SelectionStore.ts │ │ │ ├── SettingsStore.ts │ │ │ ├── ColorPickerStore.ts │ │ │ └── ViewportStore.ts │ │ ├── components │ │ │ ├── Notification.svelte │ │ │ ├── ToolPalette.svelte │ │ │ ├── KartaDebugOverlay.svelte │ │ │ ├── ColorPickerPopup.svelte │ │ │ ├── FilterMenu.svelte │ │ │ ├── ThemeEditor.svelte │ │ │ ├── ConfirmationDialog.svelte │ │ │ ├── ContextMenu.svelte │ │ │ ├── Toolbar.svelte │ │ │ ├── FilterMenuDropdown.svelte │ │ │ ├── CreateNodeMenu.svelte │ │ │ ├── TutorialModal.svelte │ │ │ └── ServerSetupModal.svelte │ │ ├── util │ │ │ ├── edgeVisibility.ts │ │ │ ├── menuPositioning.ts │ │ │ └── PersistenceService.ts │ │ ├── tools │ │ │ ├── ContextTool.ts │ │ │ └── ConnectTool.ts │ │ ├── node_types │ │ │ ├── types.ts │ │ │ ├── GenericNode.svelte │ │ │ ├── DirectoryNode.svelte │ │ │ ├── FileNode.svelte │ │ │ ├── RootNode.svelte │ │ │ └── registry.ts │ │ └── tauri │ │ │ └── server.ts │ ├── routes │ │ ├── +layout.ts │ │ ├── +layout.svelte │ │ └── +page.svelte │ ├── app.d.ts │ ├── app.html │ ├── hooks.server.ts │ └── app.css ├── static │ ├── favicon.ico │ ├── karta_logo_hd.png │ ├── tutorial-sections.json │ └── tutorial-sections.md ├── vite.config.ts ├── .gitignore ├── tailwind.config.js ├── svelte.config.js ├── tsconfig.json ├── package.json └── README.md ├── website ├── src │ ├── routes │ │ ├── +layout.ts │ │ └── +layout.svelte │ ├── app.html │ └── app.css ├── static │ ├── favicon.png │ ├── karta-icon.png │ ├── hero-screenshot.png │ ├── interface-screenshot.png │ └── favicon.svg ├── vite.config.ts ├── README.md ├── tsconfig.json ├── svelte.config.js ├── tailwind.config.js ├── .gitignore ├── package.json └── architecture.md ├── docs ├── karta_logo.png └── links │ └── Link to 2024-10-07.md ├── .github └── workflows │ └── deploy-website.yml └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | .vscode 2 | -------------------------------------------------------------------------------- /karta_server/src/layout/mod.rs: -------------------------------------------------------------------------------- 1 | // -------------------------------------------------------------------------------- /karta_svelte/.npmrc: -------------------------------------------------------------------------------- 1 | engine-strict=true 2 | -------------------------------------------------------------------------------- /karta_server/src/layout/default_layouts.rs: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /website/src/routes/+layout.ts: -------------------------------------------------------------------------------- 1 | export const prerender = true; 2 | -------------------------------------------------------------------------------- /karta_svelte/src-tauri/build.rs: -------------------------------------------------------------------------------- 1 | fn main() { 2 | tauri_build::build() 3 | } 4 | -------------------------------------------------------------------------------- /docs/karta_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/teodosin/karta/HEAD/docs/karta_logo.png -------------------------------------------------------------------------------- /karta_svelte/src/lib/debug/index.ts: -------------------------------------------------------------------------------- 1 | export * from './config'; 2 | export * from './loggers'; -------------------------------------------------------------------------------- /karta_svelte/src/routes/+layout.ts: -------------------------------------------------------------------------------- 1 | export const ssr = false; 2 | export const prerender = true; 3 | -------------------------------------------------------------------------------- /website/static/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/teodosin/karta/HEAD/website/static/favicon.png -------------------------------------------------------------------------------- /docs/links/Link to 2024-10-07.md: -------------------------------------------------------------------------------- 1 | /home/viktor/OneDrive/PROJECTS/--OBSIDIAN--/Obsidian Vault/2024-10-07.md -------------------------------------------------------------------------------- /karta_svelte/src/lib/index.ts: -------------------------------------------------------------------------------- 1 | // place files you want to import through the `$lib` alias in this folder. 2 | -------------------------------------------------------------------------------- /website/src/routes/+layout.svelte: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /website/static/karta-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/teodosin/karta/HEAD/website/static/karta-icon.png -------------------------------------------------------------------------------- /karta_svelte/static/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/teodosin/karta/HEAD/karta_svelte/static/favicon.ico -------------------------------------------------------------------------------- /website/static/hero-screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/teodosin/karta/HEAD/website/static/hero-screenshot.png -------------------------------------------------------------------------------- /karta_server/fs_graph.code-workspace: -------------------------------------------------------------------------------- 1 | { 2 | "folders": [ 3 | { 4 | "path": "." 5 | } 6 | ], 7 | "settings": {} 8 | } -------------------------------------------------------------------------------- /karta_svelte/src-tauri/icons/32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/teodosin/karta/HEAD/karta_svelte/src-tauri/icons/32x32.png -------------------------------------------------------------------------------- /karta_svelte/src-tauri/icons/64x64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/teodosin/karta/HEAD/karta_svelte/src-tauri/icons/64x64.png -------------------------------------------------------------------------------- /karta_svelte/src-tauri/icons/icon.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/teodosin/karta/HEAD/karta_svelte/src-tauri/icons/icon.icns -------------------------------------------------------------------------------- /karta_svelte/src-tauri/icons/icon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/teodosin/karta/HEAD/karta_svelte/src-tauri/icons/icon.ico -------------------------------------------------------------------------------- /karta_svelte/src-tauri/icons/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/teodosin/karta/HEAD/karta_svelte/src-tauri/icons/icon.png -------------------------------------------------------------------------------- /karta_svelte/static/karta_logo_hd.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/teodosin/karta/HEAD/karta_svelte/static/karta_logo_hd.png -------------------------------------------------------------------------------- /website/static/interface-screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/teodosin/karta/HEAD/website/static/interface-screenshot.png -------------------------------------------------------------------------------- /karta_svelte/src-tauri/icons/128x128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/teodosin/karta/HEAD/karta_svelte/src-tauri/icons/128x128.png -------------------------------------------------------------------------------- /karta_svelte/src-tauri/icons/128x128@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/teodosin/karta/HEAD/karta_svelte/src-tauri/icons/128x128@2x.png -------------------------------------------------------------------------------- /karta_svelte/src-tauri/icons/StoreLogo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/teodosin/karta/HEAD/karta_svelte/src-tauri/icons/StoreLogo.png -------------------------------------------------------------------------------- /karta_svelte/src-tauri/icons/icon_1024.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/teodosin/karta/HEAD/karta_svelte/src-tauri/icons/icon_1024.png -------------------------------------------------------------------------------- /karta_svelte/src/lib/constants.ts: -------------------------------------------------------------------------------- 1 | export const KARTA_VERSION = '0.1.0'; 2 | export const ROOT_NODE_ID = '00000000-0000-0000-0000-000000000000'; -------------------------------------------------------------------------------- /karta_svelte/src-tauri/icons/Square30x30Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/teodosin/karta/HEAD/karta_svelte/src-tauri/icons/Square30x30Logo.png -------------------------------------------------------------------------------- /karta_svelte/src-tauri/icons/Square44x44Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/teodosin/karta/HEAD/karta_svelte/src-tauri/icons/Square44x44Logo.png -------------------------------------------------------------------------------- /karta_svelte/src-tauri/icons/Square71x71Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/teodosin/karta/HEAD/karta_svelte/src-tauri/icons/Square71x71Logo.png -------------------------------------------------------------------------------- /karta_svelte/src-tauri/icons/Square89x89Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/teodosin/karta/HEAD/karta_svelte/src-tauri/icons/Square89x89Logo.png -------------------------------------------------------------------------------- /karta_svelte/src-tauri/icons/Square107x107Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/teodosin/karta/HEAD/karta_svelte/src-tauri/icons/Square107x107Logo.png -------------------------------------------------------------------------------- /karta_svelte/src-tauri/icons/Square142x142Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/teodosin/karta/HEAD/karta_svelte/src-tauri/icons/Square142x142Logo.png -------------------------------------------------------------------------------- /karta_svelte/src-tauri/icons/Square150x150Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/teodosin/karta/HEAD/karta_svelte/src-tauri/icons/Square150x150Logo.png -------------------------------------------------------------------------------- /karta_svelte/src-tauri/icons/Square284x284Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/teodosin/karta/HEAD/karta_svelte/src-tauri/icons/Square284x284Logo.png -------------------------------------------------------------------------------- /karta_svelte/src-tauri/icons/Square310x310Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/teodosin/karta/HEAD/karta_svelte/src-tauri/icons/Square310x310Logo.png -------------------------------------------------------------------------------- /karta_svelte/src-tauri/icons/ios/AppIcon-512@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/teodosin/karta/HEAD/karta_svelte/src-tauri/icons/ios/AppIcon-512@2x.png -------------------------------------------------------------------------------- /karta_svelte/src-tauri/icons/ios/AppIcon-20x20@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/teodosin/karta/HEAD/karta_svelte/src-tauri/icons/ios/AppIcon-20x20@1x.png -------------------------------------------------------------------------------- /karta_svelte/src-tauri/icons/ios/AppIcon-20x20@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/teodosin/karta/HEAD/karta_svelte/src-tauri/icons/ios/AppIcon-20x20@2x.png -------------------------------------------------------------------------------- /karta_svelte/src-tauri/icons/ios/AppIcon-20x20@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/teodosin/karta/HEAD/karta_svelte/src-tauri/icons/ios/AppIcon-20x20@3x.png -------------------------------------------------------------------------------- /karta_svelte/src-tauri/icons/ios/AppIcon-29x29@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/teodosin/karta/HEAD/karta_svelte/src-tauri/icons/ios/AppIcon-29x29@1x.png -------------------------------------------------------------------------------- /karta_svelte/src-tauri/icons/ios/AppIcon-29x29@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/teodosin/karta/HEAD/karta_svelte/src-tauri/icons/ios/AppIcon-29x29@2x.png -------------------------------------------------------------------------------- /karta_svelte/src-tauri/icons/ios/AppIcon-29x29@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/teodosin/karta/HEAD/karta_svelte/src-tauri/icons/ios/AppIcon-29x29@3x.png -------------------------------------------------------------------------------- /karta_svelte/src-tauri/icons/ios/AppIcon-40x40@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/teodosin/karta/HEAD/karta_svelte/src-tauri/icons/ios/AppIcon-40x40@1x.png -------------------------------------------------------------------------------- /karta_svelte/src-tauri/icons/ios/AppIcon-40x40@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/teodosin/karta/HEAD/karta_svelte/src-tauri/icons/ios/AppIcon-40x40@2x.png -------------------------------------------------------------------------------- /karta_svelte/src-tauri/icons/ios/AppIcon-40x40@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/teodosin/karta/HEAD/karta_svelte/src-tauri/icons/ios/AppIcon-40x40@3x.png -------------------------------------------------------------------------------- /karta_svelte/src-tauri/icons/ios/AppIcon-60x60@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/teodosin/karta/HEAD/karta_svelte/src-tauri/icons/ios/AppIcon-60x60@2x.png -------------------------------------------------------------------------------- /karta_svelte/src-tauri/icons/ios/AppIcon-60x60@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/teodosin/karta/HEAD/karta_svelte/src-tauri/icons/ios/AppIcon-60x60@3x.png -------------------------------------------------------------------------------- /karta_svelte/src-tauri/icons/ios/AppIcon-76x76@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/teodosin/karta/HEAD/karta_svelte/src-tauri/icons/ios/AppIcon-76x76@1x.png -------------------------------------------------------------------------------- /karta_svelte/src-tauri/icons/ios/AppIcon-76x76@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/teodosin/karta/HEAD/karta_svelte/src-tauri/icons/ios/AppIcon-76x76@2x.png -------------------------------------------------------------------------------- /karta_svelte/src-tauri/icons/ios/AppIcon-20x20@2x-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/teodosin/karta/HEAD/karta_svelte/src-tauri/icons/ios/AppIcon-20x20@2x-1.png -------------------------------------------------------------------------------- /karta_svelte/src-tauri/icons/ios/AppIcon-29x29@2x-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/teodosin/karta/HEAD/karta_svelte/src-tauri/icons/ios/AppIcon-29x29@2x-1.png -------------------------------------------------------------------------------- /karta_svelte/src-tauri/icons/ios/AppIcon-40x40@2x-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/teodosin/karta/HEAD/karta_svelte/src-tauri/icons/ios/AppIcon-40x40@2x-1.png -------------------------------------------------------------------------------- /karta_svelte/src-tauri/icons/ios/AppIcon-83.5x83.5@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/teodosin/karta/HEAD/karta_svelte/src-tauri/icons/ios/AppIcon-83.5x83.5@2x.png -------------------------------------------------------------------------------- /karta_svelte/src-tauri/icons/android/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/teodosin/karta/HEAD/karta_svelte/src-tauri/icons/android/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /karta_svelte/src-tauri/icons/android/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/teodosin/karta/HEAD/karta_svelte/src-tauri/icons/android/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /karta_svelte/src-tauri/icons/android/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/teodosin/karta/HEAD/karta_svelte/src-tauri/icons/android/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /karta_svelte/src-tauri/icons/android/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/teodosin/karta/HEAD/karta_svelte/src-tauri/icons/android/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /karta_svelte/src-tauri/icons/android/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/teodosin/karta/HEAD/karta_svelte/src-tauri/icons/android/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /karta_svelte/src-tauri/icons/android/mipmap-hdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/teodosin/karta/HEAD/karta_svelte/src-tauri/icons/android/mipmap-hdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /karta_svelte/src-tauri/icons/android/mipmap-mdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/teodosin/karta/HEAD/karta_svelte/src-tauri/icons/android/mipmap-mdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /karta_svelte/src-tauri/icons/android/mipmap-xhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/teodosin/karta/HEAD/karta_svelte/src-tauri/icons/android/mipmap-xhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /karta_svelte/src-tauri/icons/android/mipmap-xxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/teodosin/karta/HEAD/karta_svelte/src-tauri/icons/android/mipmap-xxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /karta_svelte/src-tauri/icons/android/mipmap-xxxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/teodosin/karta/HEAD/karta_svelte/src-tauri/icons/android/mipmap-xxxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /karta_svelte/src-tauri/icons/android/mipmap-hdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/teodosin/karta/HEAD/karta_svelte/src-tauri/icons/android/mipmap-hdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /karta_svelte/src-tauri/icons/android/mipmap-mdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/teodosin/karta/HEAD/karta_svelte/src-tauri/icons/android/mipmap-mdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /karta_svelte/src-tauri/icons/android/mipmap-xhdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/teodosin/karta/HEAD/karta_svelte/src-tauri/icons/android/mipmap-xhdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /karta_svelte/src-tauri/icons/android/mipmap-xxhdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/teodosin/karta/HEAD/karta_svelte/src-tauri/icons/android/mipmap-xxhdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /karta_svelte/src-tauri/icons/android/mipmap-xxxhdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/teodosin/karta/HEAD/karta_svelte/src-tauri/icons/android/mipmap-xxxhdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /karta_svelte/src/lib/debug/config.ts: -------------------------------------------------------------------------------- 1 | import { writable } from 'svelte/store'; 2 | 3 | // Master switch for all debugging. Can be controlled from anywhere in the app. 4 | export const isDebugMode = writable(false); -------------------------------------------------------------------------------- /karta_svelte/src-tauri/src/main.rs: -------------------------------------------------------------------------------- 1 | // Prevents additional console window on Windows in release, DO NOT REMOVE!! 2 | #![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] 3 | 4 | fn main() { 5 | app_lib::run(); 6 | } 7 | -------------------------------------------------------------------------------- /website/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { sveltekit } from '@sveltejs/kit/vite'; 2 | import tailwindcss from '@tailwindcss/vite'; 3 | import { defineConfig } from 'vite'; 4 | 5 | export default defineConfig({ 6 | plugins: [tailwindcss(), sveltekit()] 7 | }); 8 | -------------------------------------------------------------------------------- /karta_svelte/src-tauri/.gitignore: -------------------------------------------------------------------------------- 1 | # Generated by Cargo 2 | # will have compiled files and executables 3 | /target/ 4 | /gen/schemas 5 | 6 | # Tauri specific 7 | .tauri/ 8 | WixTools/ 9 | 10 | # Development logs 11 | *.log 12 | 13 | # Temporary files 14 | *.tmp 15 | *.temp 16 | -------------------------------------------------------------------------------- /karta_server/.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [] 7 | } -------------------------------------------------------------------------------- /karta_svelte/src/lib/stores/TutorialStore.ts: -------------------------------------------------------------------------------- 1 | import { writable } from 'svelte/store'; 2 | 3 | export const isTutorialOpen = writable(false); 4 | 5 | export function openTutorial() { 6 | isTutorialOpen.set(true); 7 | } 8 | 9 | export function closeTutorial() { 10 | isTutorialOpen.set(false); 11 | } 12 | -------------------------------------------------------------------------------- /karta_svelte/src-tauri/capabilities/default.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "../gen/schemas/desktop-schema.json", 3 | "identifier": "default", 4 | "description": "enables the default permissions", 5 | "windows": [ 6 | "main" 7 | ], 8 | "permissions": [ 9 | "core:default", 10 | "dialog:default" 11 | ] 12 | } -------------------------------------------------------------------------------- /website/static/favicon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | K 5 | 6 | -------------------------------------------------------------------------------- /karta_svelte/src/app.d.ts: -------------------------------------------------------------------------------- 1 | // See https://svelte.dev/docs/kit/types#app.d.ts 2 | // for information about these interfaces 3 | declare global { 4 | namespace App { 5 | // interface Error {} 6 | // interface Locals {} 7 | // interface PageData {} 8 | // interface PageState {} 9 | // interface Platform {} 10 | } 11 | } 12 | 13 | export {}; 14 | -------------------------------------------------------------------------------- /karta_svelte/vite.config.ts: -------------------------------------------------------------------------------- 1 | import tailwindcss from '@tailwindcss/vite'; 2 | import { sveltekit } from '@sveltejs/kit/vite'; 3 | import { defineConfig } from 'vite'; 4 | 5 | import type { UserConfig } from 'vite'; 6 | 7 | const config: UserConfig = { 8 | plugins: [tailwindcss(), sveltekit()] 9 | }; 10 | 11 | export default defineConfig(config); 12 | -------------------------------------------------------------------------------- /website/README.md: -------------------------------------------------------------------------------- 1 | # Karta Website 2 | 3 | This is the landing page for Karta, built with SvelteKit. 4 | 5 | ## Development 6 | 7 | ```bash 8 | npm install 9 | npm run dev 10 | ``` 11 | 12 | ## Building 13 | 14 | ```bash 15 | npm run build 16 | ``` 17 | 18 | The built site will be in the `build` directory, ready for deployment to GitHub Pages. 19 | -------------------------------------------------------------------------------- /website/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./.svelte-kit/tsconfig.json", 3 | "compilerOptions": { 4 | "allowJs": true, 5 | "checkJs": true, 6 | "esModuleInterop": true, 7 | "forceConsistentCasingInFileNames": true, 8 | "resolveJsonModule": true, 9 | "skipLibCheck": true, 10 | "sourceMap": true, 11 | "strict": true, 12 | "moduleResolution": "bundler" 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /website/src/app.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | %sveltekit.head% 8 | 9 | 10 |
%sveltekit.body%
11 | 12 | 13 | -------------------------------------------------------------------------------- /karta_server/.gitignore: -------------------------------------------------------------------------------- 1 | # Rust build artifacts 2 | /target/ 3 | **/*.rs.bk 4 | 5 | # agdb database files 6 | *.agdb 7 | 8 | # Performance reports (development artifacts) 9 | docs/perf_reports/ 10 | 11 | # Development logs 12 | *.log 13 | 14 | # RON config files (development only) 15 | dev_config.ron 16 | local_config.ron 17 | test_config.ron 18 | 19 | # Temporary files 20 | *.tmp 21 | *.temp 22 | 23 | # Development databases 24 | dev.db 25 | test.db 26 | -------------------------------------------------------------------------------- /karta_svelte/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | 3 | # Output 4 | .output 5 | .vercel 6 | .netlify 7 | .wrangler 8 | /.svelte-kit 9 | /build 10 | dist/ 11 | 12 | # OS 13 | .DS_Store 14 | Thumbs.db 15 | 16 | # Env 17 | .env 18 | .env.* 19 | !.env.example 20 | !.env.test 21 | 22 | # Vite 23 | vite.config.js.timestamp-* 24 | vite.config.ts.timestamp-* 25 | 26 | # IndexedDB development files 27 | *.idb 28 | 29 | # Development logs 30 | *.log 31 | 32 | # Tauri build artifacts 33 | src-tauri/target/ 34 | -------------------------------------------------------------------------------- /karta_server/src/context/context_settings.rs: -------------------------------------------------------------------------------- 1 | use serde::{Deserialize, Serialize}; 2 | 3 | 4 | 5 | #[derive(Serialize, Deserialize, Clone, Debug)] 6 | pub struct ContextSettings { 7 | zoom_scale: f32, 8 | view_rel_pos_x: f32, 9 | view_rel_pos_y: f32, 10 | } 11 | 12 | impl Default for ContextSettings { 13 | fn default() -> Self { 14 | Self { 15 | zoom_scale: 1.0, 16 | view_rel_pos_x: 0.0, 17 | view_rel_pos_y: 0.0, 18 | } 19 | } 20 | } -------------------------------------------------------------------------------- /karta_server/src/main.rs: -------------------------------------------------------------------------------- 1 | // use karta_server::prelude::run_server; 2 | 3 | use karta_server::prelude::{cli_load_or_create_vault, run_server}; 4 | 5 | #[tokio::main] 6 | async fn main() { 7 | let vault_path = cli_load_or_create_vault(); 8 | let vault_path = match vault_path { 9 | Ok(path) => path, 10 | Err(_) => { 11 | println!("No vault selected. Exiting..."); 12 | std::process::exit(1); 13 | } 14 | }; 15 | run_server(vault_path).await; 16 | } 17 | -------------------------------------------------------------------------------- /karta_svelte/src/lib/apiBase.ts: -------------------------------------------------------------------------------- 1 | // Centralized API base URL resolution. 2 | // In Tauri (desktop) we want to call the local server directly. 3 | // In web/dev we can keep relative paths to use the SvelteKit proxy. 4 | 5 | export const API_BASE: string = 6 | typeof window !== 'undefined' && '__TAURI__' in window 7 | ? 'http://127.0.0.1:7370' 8 | : ''; 9 | 10 | export function api(path: string): string { 11 | const p = path.startsWith('/') ? path : `/${path}`; 12 | return `${API_BASE}${p}`; 13 | } 14 | -------------------------------------------------------------------------------- /karta_svelte/tailwind.config.js: -------------------------------------------------------------------------------- 1 | import defaultTheme from 'tailwindcss/defaultTheme'; 2 | 3 | /** @type {import('tailwindcss').Config} */ 4 | export default { 5 | content: ['./src/**/*.{html,js,svelte,ts}'], 6 | darkMode: 'class', // Enable dark mode using the class strategy 7 | theme: { 8 | extend: { 9 | fontFamily: { 10 | sans: ['Nunito Sans', ...defaultTheme.fontFamily.sans], 11 | }, 12 | colors: { 13 | 'panel-bg': '#29151d', 14 | }, 15 | }, 16 | }, 17 | plugins: [], 18 | } 19 | -------------------------------------------------------------------------------- /website/svelte.config.js: -------------------------------------------------------------------------------- 1 | import adapter from '@sveltejs/adapter-static'; 2 | import { vitePreprocess } from '@sveltejs/vite-plugin-svelte'; 3 | 4 | /** @type {import('@sveltejs/kit').Config} */ 5 | const config = { 6 | preprocess: vitePreprocess(), 7 | 8 | kit: { 9 | adapter: adapter({ 10 | pages: 'build', 11 | assets: 'build', 12 | fallback: undefined, 13 | precompress: false, 14 | strict: true 15 | }), 16 | paths: { 17 | base: process.env.NODE_ENV === 'production' ? '/karta' : '' 18 | } 19 | } 20 | }; 21 | 22 | export default config; 23 | -------------------------------------------------------------------------------- /karta_svelte/src/app.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Karta 8 | 9 | 10 | %sveltekit.head% 11 | 12 | 13 |
%sveltekit.body%
14 | 15 | 16 | -------------------------------------------------------------------------------- /karta_svelte/src/lib/karta/VaultStore.ts: -------------------------------------------------------------------------------- 1 | import { writable } from 'svelte/store'; 2 | 3 | export const vaultName = writable(null); 4 | 5 | export async function initializeVault() { 6 | 7 | try { 8 | const response = await fetch('http://localhost:7370/'); 9 | 10 | if (!response.ok) { 11 | throw new Error('Failed to fetch vault info'); 12 | } 13 | 14 | const data = await response.json(); 15 | vaultName.set(data.vault_name); 16 | 17 | } catch (error) { 18 | console.error('[VaultStore] Error initializing vault:', error); 19 | vaultName.set(null); 20 | } 21 | } -------------------------------------------------------------------------------- /website/tailwind.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('tailwindcss').Config} */ 2 | export default { 3 | content: ['./src/**/*.{html,js,svelte,ts}'], 4 | theme: { 5 | extend: { 6 | colors: { 7 | 'karta-red': '#431d1f', 8 | 'karta-red-light': '#741f2f', 9 | 'karta-orange': '#f4902d', 10 | 'karta-blue': '#60a5fa', 11 | 'karta-dark': '#000000', 12 | 'karta-gray': '#505050', 13 | 'karta-text': '#f0f0f0' 14 | }, 15 | fontFamily: { 16 | 'sans': ['Nunito', 'system-ui', 'sans-serif'] 17 | } 18 | }, 19 | }, 20 | plugins: [], 21 | } 22 | -------------------------------------------------------------------------------- /karta_svelte/svelte.config.js: -------------------------------------------------------------------------------- 1 | import adapter from '@sveltejs/adapter-static'; 2 | import { vitePreprocess } from '@sveltejs/vite-plugin-svelte'; 3 | 4 | /** @type {import('@sveltejs/kit').Config} */ 5 | const config = { 6 | // Consult https://svelte.dev/docs/kit/integrations 7 | // for more information about preprocessors 8 | preprocess: vitePreprocess(), 9 | 10 | kit: { 11 | adapter: adapter({ 12 | pages: 'build', 13 | assets: 'build', 14 | fallback: 'index.html', 15 | precompress: false, 16 | strict: true 17 | }), 18 | paths: { relative: true } 19 | } 20 | }; 21 | 22 | export default config; 23 | -------------------------------------------------------------------------------- /karta_svelte/src-tauri/Entitlements.plist: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | com.apple.security.app-sandbox 6 | com.apple.security.files.user-selected.read-write 7 | com.apple.security.files.bookmarks.app-scope 8 | 9 | com.apple.security.network.client 10 | com.apple.security.network.server 11 | 12 | -------------------------------------------------------------------------------- /website/.gitignore: -------------------------------------------------------------------------------- 1 | # Dependencies 2 | node_modules/ 3 | 4 | # Build output 5 | build/ 6 | .svelte-kit/ 7 | 8 | # Logs 9 | npm-debug.log* 10 | yarn-debug.log* 11 | yarn-error.log* 12 | 13 | # Runtime data 14 | pids 15 | *.pid 16 | *.seed 17 | *.pid.lock 18 | 19 | # Coverage directory used by tools like istanbul 20 | coverage/ 21 | 22 | # Dependency directories 23 | node_modules/ 24 | 25 | # Optional npm cache directory 26 | .npm 27 | 28 | # Optional REPL history 29 | .node_repl_history 30 | 31 | # Output of 'npm pack' 32 | *.tgz 33 | 34 | # Yarn Integrity file 35 | .yarn-integrity 36 | 37 | # dotenv environment variables file 38 | .env 39 | 40 | # IDE 41 | .vscode/ 42 | .idea/ 43 | 44 | # OS 45 | .DS_Store 46 | Thumbs.db 47 | -------------------------------------------------------------------------------- /karta_server/src/graph_traits/mod.rs: -------------------------------------------------------------------------------- 1 | use std::{error::Error, path::PathBuf}; 2 | 3 | use crate::elements; 4 | use elements::*; 5 | 6 | use graph_core::GraphCore; 7 | use graph_edge::GraphEdge; 8 | use graph_node::GraphNodes; 9 | 10 | pub(crate) mod graph_core; 11 | pub(crate) mod graph_node; 12 | pub(crate) mod graph_edge; 13 | 14 | #[derive(Clone, PartialEq, Debug)] 15 | pub enum StoragePath { 16 | Default, 17 | Custom(PathBuf), 18 | } 19 | 20 | impl StoragePath { 21 | pub fn new(path: PathBuf) -> Self { 22 | Self::Custom(path) 23 | } 24 | 25 | pub fn strg_path(&self) -> Option { 26 | match self { 27 | Self::Default => None, 28 | Self::Custom(path) => Some(path.clone()), 29 | } 30 | } 31 | } 32 | 33 | /// The main graph trait. 34 | pub(crate) trait Graph: GraphCore + GraphNodes + GraphEdge {} 35 | 36 | -------------------------------------------------------------------------------- /website/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "karta-website", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "vite dev", 7 | "build": "vite build", 8 | "preview": "vite preview", 9 | "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json", 10 | "check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch" 11 | }, 12 | "devDependencies": { 13 | "@sveltejs/adapter-static": "^3.0.9", 14 | "@sveltejs/kit": "^2.16.0", 15 | "@sveltejs/vite-plugin-svelte": "^5.0.0", 16 | "@tailwindcss/typography": "^0.5.15", 17 | "@tailwindcss/vite": "^4.0.0", 18 | "svelte": "^5.9.0", 19 | "svelte-check": "^4.0.0", 20 | "tailwindcss": "^4.0.0", 21 | "typescript": "^5.0.0", 22 | "vite": "^6.0.0" 23 | }, 24 | "type": "module", 25 | "dependencies": { 26 | "lucide-svelte": "^0.454.0" 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /karta_server/src/lib.rs: -------------------------------------------------------------------------------- 1 | #![allow(warnings)] 2 | 3 | mod elements; 4 | mod graph_traits; 5 | mod graph_agdb; 6 | 7 | mod fs_reader; 8 | mod context; 9 | mod layout; 10 | 11 | mod server; 12 | 13 | mod utils; 14 | 15 | pub(crate) const SERVER_VERSION: &str = "0.1.0"; 16 | 17 | pub mod prelude { 18 | pub use crate::elements::{ 19 | attribute::Attribute, 20 | edge::Edge, 21 | node::DataNode, 22 | view_node::ViewNode, 23 | node_path::NodePath, 24 | nodetype::NodeTypeId, 25 | SysTime, 26 | }; 27 | 28 | pub use crate::graph_traits::{ 29 | graph_core::GraphCore, 30 | graph_edge::GraphEdge, 31 | graph_node::GraphNodes, 32 | StoragePath, 33 | }; 34 | 35 | pub use crate::graph_agdb::GraphAgdb; 36 | 37 | pub use crate::context::*; 38 | 39 | pub use crate::layout::*; 40 | 41 | pub use crate::server::*; 42 | } -------------------------------------------------------------------------------- /website/src/app.css: -------------------------------------------------------------------------------- 1 | /* Import Google Fonts: Nunito (base), Lustria, Bodoni Moda */ 2 | @import url('https://fonts.googleapis.com/css2?family=Bodoni+Moda:ital,opsz,wght@0,6..96,400..900;1,6..96,400..900&family=Lustria&family=Nunito:ital,wght@0,200..1000;1,200..1000&display=swap'); 3 | @import 'tailwindcss'; 4 | 5 | :root { 6 | --color-panel-bg: #431d1f; 7 | --color-viewport-bg: #000000; 8 | --color-text-color: #f0f0f0; 9 | --color-panel-hl: #741f2f; 10 | --color-focal-hl: #f4902d; 11 | --color-contrast-color: #60a5fa; 12 | --color-connection-color: #505050; 13 | } 14 | 15 | html, body { 16 | margin: 0; 17 | padding: 0; 18 | font-family: 'Nunito', sans-serif; 19 | background-color: var(--color-viewport-bg); 20 | color: var(--color-text-color); 21 | } 22 | 23 | * { 24 | box-sizing: border-box; 25 | font-family: 'Nunito', sans-serif; 26 | } 27 | 28 | html { 29 | scroll-behavior: smooth; 30 | } 31 | -------------------------------------------------------------------------------- /karta_svelte/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./.svelte-kit/tsconfig.json", 3 | "compilerOptions": { 4 | "allowJs": true, 5 | "checkJs": true, 6 | "esModuleInterop": true, 7 | "allowSyntheticDefaultImports": true, 8 | "forceConsistentCasingInFileNames": true, 9 | "resolveJsonModule": true, 10 | "skipLibCheck": true, 11 | "sourceMap": true, 12 | "strict": true, 13 | "moduleResolution": "bundler", 14 | "lib": ["es2022", "dom", "dom.iterable"], 15 | "paths": { 16 | "$lib": ["./src/lib"], 17 | "$lib/*": ["./src/lib/*"] 18 | } 19 | } 20 | // Path aliases are handled by https://svelte.dev/docs/kit/configuration#alias 21 | // except $lib which is handled by https://svelte.dev/docs/kit/configuration#files 22 | // 23 | // If you want to overwrite includes/excludes, make sure to copy over the relevant includes/excludes 24 | // from the referenced tsconfig.json - TypeScript does not merge them in 25 | } 26 | -------------------------------------------------------------------------------- /karta_server/src/elements/mod.rs: -------------------------------------------------------------------------------- 1 | use std::{path::PathBuf, time::SystemTime}; 2 | 3 | use agdb::{DbElement, DbError, DbId, DbKeyValue, DbUserValue, DbValue, QueryId, UserValue}; 4 | 5 | pub (crate) mod node; 6 | pub (crate) mod node_path; 7 | pub (crate) mod nodetype; 8 | pub (crate) mod edge; 9 | pub (crate) mod attribute; 10 | pub (crate) mod view_node; 11 | 12 | 13 | 14 | #[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)] 15 | pub struct SysTime(SystemTime); 16 | 17 | impl From for DbValue { 18 | fn from(time: SysTime) -> Self { 19 | time.0.duration_since(SystemTime::UNIX_EPOCH).unwrap().as_secs().into() 20 | } 21 | } 22 | 23 | impl TryFrom for SysTime { 24 | type Error = DbError; 25 | 26 | fn try_from(value: DbValue) -> Result { 27 | Ok(SysTime(SystemTime::UNIX_EPOCH + std::time::Duration::from_secs(value.to_u64().unwrap()))) 28 | } 29 | } 30 | 31 | 32 | 33 | 34 | 35 | 36 | -------------------------------------------------------------------------------- /karta_svelte/src-tauri/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "karta" 3 | version = "0.1.0" 4 | description = "Karta - A node-based graph tool for organizing complex creative projects" 5 | authors = ["teodosin"] 6 | license = "GPL 3" 7 | repository = "https://github.com/teodosin/karta_ecosystem" 8 | edition = "2021" 9 | rust-version = "1.77.2" 10 | 11 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 12 | 13 | [lib] 14 | name = "app_lib" 15 | crate-type = ["staticlib", "cdylib", "rlib"] 16 | 17 | [build-dependencies] 18 | tauri-build = { version = "2.3.1", features = [] } 19 | 20 | [dependencies] 21 | tauri = { version = "2", features = [] } 22 | serde = { version = "1", features = ["derive"] } 23 | serde_json = "1" 24 | tauri-plugin-log = "2" 25 | tauri-plugin-dialog = "2" 26 | karta_server = { path = "../../karta_server" } 27 | reqwest = { version = "0.12", features = ["json"] } 28 | ron = "0.10" 29 | tokio = { version = "1.47.1", features = ["time"] } 30 | log = "0.4" 31 | dirs = "5.0" 32 | base64 = "0.22" 33 | 34 | [target.'cfg(target_os = "macos")'.dependencies] 35 | cocoa = "0.25" 36 | objc = "0.2" 37 | 38 | [workspace] 39 | -------------------------------------------------------------------------------- /karta_server/src/server/write_endpoints_tests/mod.rs: -------------------------------------------------------------------------------- 1 | // Re-export common imports needed by all test modules 2 | pub use crate::{ 3 | context::context::Context, 4 | elements::{ 5 | node::DataNode, 6 | view_node::ViewNode, 7 | node_path::{NodeHandle, NodePath}, 8 | nodetype::NodeTypeId, 9 | }, 10 | graph_traits::graph_node::GraphNodes, 11 | server::{ 12 | karta_service::KartaService, 13 | AppState, 14 | write_endpoints::{ 15 | CreateNodePayload, UpdateNodePayload, RenameNodeByPathPayload, RenameNodeResponse, 16 | MoveNodesPayload, MoveOperation, MovedNodeInfo, MoveNodesResponse, MoveError, 17 | UpdateNodeResponse, DeleteNodesPayload, DeleteNodesResponse, DeletedNodeInfo, FailedDeletion 18 | } 19 | }, 20 | utils::utils::KartaServiceTestContext, 21 | }; 22 | pub use axum::{ 23 | body::Body, 24 | http::{self, Request, StatusCode}, 25 | Router, 26 | }; 27 | pub use tower::ServiceExt; 28 | 29 | mod test_helpers; 30 | mod test_context; 31 | mod test_node_creation; 32 | mod test_node_movement; 33 | mod test_node_rename; 34 | mod test_node_deletion; 35 | 36 | pub use test_helpers::*; 37 | -------------------------------------------------------------------------------- /karta_svelte/src-tauri/tauri.conf.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "../node_modules/@tauri-apps/cli/config.schema.json", 3 | "productName": "karta", 4 | "version": "0.1.0", 5 | "identifier": "com.karta.dev", 6 | "build": { 7 | "frontendDist": "../build", 8 | "devUrl": "http://localhost:7360", 9 | "beforeDevCommand": "pnpm dev --port 7360", 10 | "beforeBuildCommand": "pnpm build" 11 | }, 12 | "app": { 13 | "withGlobalTauri": true, 14 | "windows": [ 15 | { 16 | "title": "Karta", 17 | "width": 1200, 18 | "height": 800, 19 | "resizable": true, 20 | "fullscreen": false 21 | } 22 | ], 23 | "security": { 24 | "csp": null 25 | } 26 | }, 27 | "plugins": { 28 | "shell": { 29 | "all": false, 30 | "execute": true, 31 | "sidecar": true, 32 | "open": false 33 | } 34 | }, 35 | "bundle": { 36 | "active": true, 37 | "targets": "all", 38 | "icon": [ 39 | "icons/32x32.png", 40 | "icons/128x128.png", 41 | "icons/128x128@2x.png", 42 | "icons/icon.icns", 43 | "icons/icon.ico" 44 | ], 45 | "macOS": { 46 | "entitlements": "./Entitlements.plist" 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /karta_svelte/src/lib/components/Notification.svelte: -------------------------------------------------------------------------------- 1 | 14 | 15 |
19 | {#each currentNotifications as notification (notification.id)} 20 |
31 | {notification.message} 32 |
33 | {/each} 34 |
-------------------------------------------------------------------------------- /karta_server/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "karta_server" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 7 | 8 | [dependencies] 9 | agdb = { version = "0.11.2", features = ["serde"] } 10 | axum = { version = "0.8.4", features = ["macros"] } 11 | blake3 = "1.8.2" 12 | directories = "6.0.0" 13 | ron = "0.10.1" 14 | rustyline = "16.0.0" 15 | serde = { version = "1.0.219", features = ["serde_derive"] } 16 | serde_json = "1.0.140" 17 | tokio = { version = "1.45.1", features = ["rt-multi-thread", "macros", "fs"] } 18 | tracing = "0.1.41" 19 | tracing-subscriber = { version = "0.3.19", features = ["env-filter"] } 20 | uuid = { version = "1.12.1", features = ["serde", "v5", "v4"]} # Added v4 feature 21 | tower-http = { version = "0.5.2", features = ["cors"] } # Added for CORS 22 | dunce = "1.0.4" # Added for path canonicalization 23 | mime_guess = "2.0.5" 24 | walkdir = "2.5.0" 25 | trash = "5.2" # For safe deletion to system trash 26 | chrono = { version = "0.4", features = ["serde"] } # For timestamps 27 | fuzzy-matcher = "0.3.7" 28 | 29 | [dev-dependencies] 30 | git2 = "0.20.2" 31 | tower = { version = "0.4", features = ["util"] } # For ServiceExt 32 | -------------------------------------------------------------------------------- /karta_svelte/src/lib/karta/NotificationStore.ts: -------------------------------------------------------------------------------- 1 | import { writable } from 'svelte/store'; 2 | import { v4 as uuidv4 } from 'uuid'; 3 | 4 | export type NotificationType = 'success' | 'error' | 'info'; 5 | 6 | export interface Notification { 7 | id: string; 8 | type: NotificationType; 9 | message: string; 10 | duration: number; 11 | } 12 | 13 | const { subscribe, update } = writable([]); 14 | 15 | function show(message: string, type: NotificationType = 'info', duration: number = 3000) { 16 | const id = uuidv4(); 17 | const notification: Notification = { 18 | id, 19 | type, 20 | message, 21 | duration 22 | }; 23 | 24 | update((notifications) => [...notifications, notification]); 25 | 26 | setTimeout(() => { 27 | remove(id); 28 | }, duration); 29 | } 30 | 31 | function remove(id: string) { 32 | update((notifications) => notifications.filter((n) => n.id !== id)); 33 | } 34 | 35 | export const notifications = { 36 | subscribe, 37 | show, 38 | remove, 39 | success: (message: string, duration?: number) => show(message, 'success', duration), 40 | error: (message: string, duration?: number) => show(message, 'error', duration), 41 | info: (message: string, duration?: number) => show(message, 'info', duration) 42 | }; -------------------------------------------------------------------------------- /karta_server/src/context/mod.rs: -------------------------------------------------------------------------------- 1 | use std::{error::Error, io::Write, path::PathBuf}; 2 | 3 | use context_db::ContextDb; 4 | use serde::{Deserialize, Serialize}; 5 | use uuid::Uuid; 6 | 7 | pub mod context_db; 8 | pub mod context; 9 | pub mod context_settings; 10 | 11 | 12 | pub fn test_ctx_db(test_name: &str) -> ContextDb { 13 | let strg_name = "karta_server"; 14 | 15 | let root = directories::ProjectDirs::from("com", "karta_server", strg_name) 16 | .unwrap() 17 | .data_dir() 18 | .to_path_buf(); 19 | 20 | let full_path = root.join(&test_name); 21 | let strg_dir = full_path.join(".karta"); 22 | 23 | let context_db = ContextDb::new("test".to_string(), PathBuf::from("."), PathBuf::from(".")); 24 | context_db 25 | } 26 | 27 | #[cfg(test)] 28 | mod tests { 29 | use super::*; 30 | 31 | 32 | #[test] 33 | fn test_context_db() { 34 | let func_name = "test_context_db"; 35 | let context_db = test_ctx_db(func_name); 36 | } 37 | 38 | // #[test] 39 | // fn context_file_name_is_uuid_with_ctx_extension() { 40 | // let func_name = "context_file_name_is_uuid_with_ctx_extension"; 41 | // let context_db = test_ctx_db(func_name); 42 | 43 | // // What are we testing here? 44 | // todo!(); 45 | // } 46 | } -------------------------------------------------------------------------------- /karta_svelte/src/lib/karta/HistoryStore.ts: -------------------------------------------------------------------------------- 1 | import { writable, get } from 'svelte/store'; 2 | import type { NodeId } from '../types/types'; 3 | import { switchContext, currentContextId } from './ContextStore'; 4 | 5 | // History Stores 6 | export const historyStack = writable([]); 7 | export const futureStack = writable([]); 8 | 9 | // History Actions 10 | export function undoContextSwitch() { 11 | 12 | const history = get(historyStack); 13 | 14 | if (history.length === 0) { 15 | return; 16 | } 17 | 18 | const previousId = history[history.length - 1]; 19 | const currentId = get(currentContextId); 20 | 21 | historyStack.update(stack => stack.slice(0, -1)); 22 | futureStack.update(stack => [...stack, currentId]); 23 | 24 | switchContext({ type: 'uuid', value: previousId }, true); 25 | } 26 | 27 | export function redoContextSwitch() { 28 | 29 | const future = get(futureStack); 30 | 31 | if (future.length === 0) { 32 | return; 33 | } 34 | 35 | const nextId = future[future.length - 1]; 36 | const currentId = get(currentContextId); 37 | 38 | futureStack.update(stack => stack.slice(0, -1)); 39 | historyStack.update(stack => [...stack, currentId]); 40 | 41 | switchContext({ type: 'uuid', value: nextId }, true); 42 | } -------------------------------------------------------------------------------- /karta_server/src/context/context.rs: -------------------------------------------------------------------------------- 1 | use serde::{Deserialize, Serialize}; 2 | use uuid::Uuid; 3 | 4 | use crate::{prelude::ViewNode, SERVER_VERSION}; 5 | 6 | use super::context_settings::ContextSettings; 7 | 8 | #[derive(Serialize, Deserialize, Clone, Debug)] 9 | pub struct Context { 10 | karta_version: String, 11 | focal: Uuid, 12 | nodes: Vec, 13 | settings: ContextSettings 14 | } 15 | 16 | impl Context { 17 | pub fn focal(&self) -> Uuid { 18 | self.focal 19 | } 20 | 21 | pub fn viewnodes(&self) -> &Vec { 22 | &self.nodes 23 | } 24 | 25 | pub fn viewnodes_mut(&mut self) -> &mut Vec { 26 | &mut self.nodes 27 | } 28 | 29 | pub fn new(focal: Uuid) -> Self { 30 | Self { 31 | karta_version: SERVER_VERSION.to_string(), 32 | focal, 33 | nodes: Vec::new(), 34 | settings: ContextSettings::default() 35 | } 36 | } 37 | 38 | pub fn with_viewnodes(focal: Uuid, nodes: Vec) -> Self { 39 | Self { 40 | karta_version: SERVER_VERSION.to_string(), 41 | focal, 42 | nodes, 43 | settings: ContextSettings::default() 44 | } 45 | } 46 | 47 | pub fn add_node(&mut self, node: ViewNode) { 48 | self.nodes.push(node); 49 | } 50 | } -------------------------------------------------------------------------------- /karta_svelte/src/hooks.server.ts: -------------------------------------------------------------------------------- 1 | import type { Handle } from '@sveltejs/kit'; 2 | 3 | /** 4 | * This hook intercepts requests to the SvelteKit server. 5 | * It's used here to proxy API requests to the backend server 6 | * during development. This is the recommended SvelteKit way to handle 7 | * API proxying, as it avoids conflicts with SvelteKit's own router. 8 | */ 9 | export const handle: Handle = async ({ event, resolve }) => { 10 | if (event.url.pathname.startsWith('/api')) { 11 | const targetUrl = new URL(event.url.pathname + event.url.search, 'http://localhost:7370'); 12 | 13 | const headers = new Headers(event.request.headers); 14 | headers.delete('host'); 15 | 16 | const isGetOrHead = event.request.method === 'GET' || event.request.method === 'HEAD'; 17 | 18 | try { 19 | const response = await fetch(targetUrl.toString(), { 20 | method: event.request.method, 21 | headers: headers, 22 | body: isGetOrHead ? undefined : event.request.body, 23 | // The 'duplex' property is only required for streaming request bodies. 24 | ...(isGetOrHead ? {} : { duplex: 'half' }) 25 | }); 26 | 27 | return response; 28 | } catch (error) { 29 | console.error(`[API Proxy] Error fetching ${targetUrl.toString()}:`, error); 30 | return new Response('API proxy error', { status: 502 }); 31 | } 32 | } 33 | 34 | return resolve(event); 35 | }; -------------------------------------------------------------------------------- /karta_svelte/src/app.css: -------------------------------------------------------------------------------- 1 | /* Import Google Fonts: Nunito (base), Lustria, Bodoni Moda */ 2 | @import url('https://fonts.googleapis.com/css2?family=Bodoni+Moda:ital,opsz,wght@0,6..96,400..900;1,6..96,400..900&family=Lustria&family=Nunito:ital,wght@0,200..1000;1,200..1000&display=swap'); 3 | @import 'tailwindcss'; 4 | @plugin '@tailwindcss/typography'; 5 | 6 | @tailwind utilities; 7 | 8 | html, body { 9 | margin: 0; 10 | margin-top: 0; 11 | padding: 0; 12 | overflow: hidden; 13 | overscroll-behavior-x: none; /* Prevent browser back/forward on horizontal swipe */ 14 | /* Explicitly set font and use CSS variables */ 15 | /* Apply base font, background, and text color using Tailwind utilities */ 16 | @apply font-sans bg-gray-900; /* Use default Tailwind classes */ 17 | color: var(--color-text-color); 18 | } 19 | 20 | * { 21 | box-sizing: border-box; 22 | /* Ensure borders don't conflict with dark theme */ 23 | border-color: theme('colors.gray.700'); /* Keep default border for now */ 24 | font-family: 'Nunito', sans-serif; 25 | } 26 | 27 | @theme { 28 | --color-panel-bg: #431d1f; 29 | --color-viewport-bg: #000000; 30 | --color-text-color: #f0f0f0; 31 | --color-panel-hl: #741f2fff; 32 | --color-contrast-color: #60a5fa; 33 | --color-connection-color: #505050; 34 | } 35 | 36 | @layer utilities { 37 | .hover-bg-panel-hl:hover { 38 | background-color: var(--color-panel-hl); 39 | } 40 | } -------------------------------------------------------------------------------- /karta_svelte/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "karta-svelte-client", 3 | "private": true, 4 | "version": "0.1.0", 5 | "type": "module", 6 | "scripts": { 7 | "dev": "vite dev", 8 | "build": "vite build", 9 | "preview": "vite preview", 10 | "prepare": "svelte-kit sync || echo ''", 11 | "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json", 12 | "check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch" 13 | }, 14 | "devDependencies": { 15 | "@sveltejs/adapter-auto": "4.0.0", 16 | "@sveltejs/adapter-static": "3.0.9", 17 | "@sveltejs/kit": "2.31.1", 18 | "@sveltejs/vite-plugin-svelte": "6.1.2", 19 | "@tailwindcss/typography": "0.5.15", 20 | "@tailwindcss/vite": "4.1.12", 21 | "@tauri-apps/cli": "2.5.0", 22 | "svelte": "5.38.1", 23 | "svelte-check": "4.0.0", 24 | "tailwindcss": "4.0.0", 25 | "typescript": "5.9.2", 26 | "vite": "7.1.2" 27 | }, 28 | "dependencies": { 29 | "@panzoom/panzoom": "4.6.0", 30 | "@tauri-apps/api": "2.1.0", 31 | "@tauri-apps/plugin-dialog": "~2", 32 | "@types/uuid": "10.0.0", 33 | "colord": "2.9.3", 34 | "idb": "8.0.2", 35 | "lucide-svelte": "0.487.0", 36 | "svelte-awesome-color-picker": "4.0.1", 37 | "svelte-portal": "2.2.1", 38 | "uuid": "11.1.0" 39 | }, 40 | "packageManager": "pnpm@10.14.0+sha512.ad27a79641b49c3e481a16a805baa71817a04bbe06a38d17e60e2eaee83f6a146c6a688125f5792e48dd5ba30e7da52a5cda4c3992b9ccf333f9ce223af84748" 41 | } 42 | -------------------------------------------------------------------------------- /karta_svelte/src/lib/components/ToolPalette.svelte: -------------------------------------------------------------------------------- 1 | 12 | 13 |
14 | 15 | 29 |
30 | -------------------------------------------------------------------------------- /karta_svelte/src/lib/util/edgeVisibility.ts: -------------------------------------------------------------------------------- 1 | import type { EdgeVisibilityMode, KartaEdge } from '$lib/types/types'; 2 | import type { NodeId } from '$lib/types/types'; 3 | 4 | /** 5 | * Determines whether an edge should be visible based on filter settings and selection state 6 | */ 7 | export function shouldShowEdge( 8 | edge: KartaEdge, 9 | visibilityMode: EdgeVisibilityMode, 10 | selectedNodeIds: Set 11 | ): boolean { 12 | switch (visibilityMode) { 13 | case 'always': 14 | return true; 15 | case 'never': 16 | return false; 17 | case 'all-selected': 18 | // Show edges for all selected nodes, even if the other end isn't selected 19 | return selectedNodeIds.size > 0 && 20 | (selectedNodeIds.has(edge.source) || selectedNodeIds.has(edge.target)); 21 | case 'between-selected': 22 | // Show if both source and target are selected, and at least one node is selected 23 | return selectedNodeIds.size > 0 && 24 | selectedNodeIds.has(edge.source) && 25 | selectedNodeIds.has(edge.target); 26 | case 'single-selected': 27 | // Show if exactly one node is selected and it's either the source or target 28 | return selectedNodeIds.size === 1 && 29 | (selectedNodeIds.has(edge.source) || selectedNodeIds.has(edge.target)); 30 | default: 31 | return true; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /.github/workflows/deploy-website.yml: -------------------------------------------------------------------------------- 1 | name: Deploy Website 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | paths: [ 'website/**' ] 7 | workflow_dispatch: 8 | 9 | permissions: 10 | contents: read 11 | pages: write 12 | id-token: write 13 | 14 | concurrency: 15 | group: "pages" 16 | cancel-in-progress: false 17 | 18 | jobs: 19 | build: 20 | runs-on: ubuntu-latest 21 | steps: 22 | - name: Checkout 23 | uses: actions/checkout@v4 24 | 25 | - name: Setup Node.js 26 | uses: actions/setup-node@v4 27 | with: 28 | node-version: '20' 29 | 30 | - name: Install pnpm 31 | uses: pnpm/action-setup@v4 32 | with: 33 | version: 8 34 | 35 | - name: Install dependencies 36 | working-directory: website 37 | run: pnpm install 38 | 39 | - name: Build website 40 | working-directory: website 41 | run: pnpm build 42 | env: 43 | NODE_ENV: production 44 | 45 | - name: Setup Pages 46 | uses: actions/configure-pages@v4 47 | 48 | - name: Upload artifact 49 | uses: actions/upload-pages-artifact@v3 50 | with: 51 | path: website/build 52 | 53 | deploy: 54 | environment: 55 | name: github-pages 56 | url: ${{ steps.deployment.outputs.page_url }} 57 | runs-on: ubuntu-latest 58 | needs: build 59 | steps: 60 | - name: Deploy to GitHub Pages 61 | id: deployment 62 | uses: actions/deploy-pages@v4 63 | -------------------------------------------------------------------------------- /karta_svelte/src/lib/components/KartaDebugOverlay.svelte: -------------------------------------------------------------------------------- 1 | 38 | 39 |
40 |
Target:
41 |
Scale: {targetScale}
42 |
Tx: {targetTx}
43 |
Ty: {targetTy}
44 |
45 |
Current ($):
46 |
Scale: {currentScale}
47 |
Tx: {currentTx}
48 |
Ty: {currentTy}
49 |
-------------------------------------------------------------------------------- /karta_server/docs/perf_reports/fs_graph_test_opening_folder_connections__indexes_folder_contents.ron: -------------------------------------------------------------------------------- 1 | [ 2 | /*[0]*/ ( 3 | commit: "d09880c77148824f504db0df259180b023c4319f", 4 | elapsed_ms: 52, 5 | db_size_bytes: 11066, 6 | timestamp: "1726070817", 7 | ), 8 | /*[1]*/ ( 9 | commit: "d09880c77148824f504db0df259180b023c4319f", 10 | elapsed_ms: 75, 11 | db_size_bytes: 11066, 12 | timestamp: "1726071769", 13 | ), 14 | /*[2]*/ ( 15 | commit: "d09880c77148824f504db0df259180b023c4319f", 16 | elapsed_ms: 73, 17 | db_size_bytes: 11066, 18 | timestamp: "1726072010", 19 | ), 20 | /*[3]*/ ( 21 | commit: "d09880c77148824f504db0df259180b023c4319f", 22 | elapsed_ms: 52, 23 | db_size_bytes: 11066, 24 | timestamp: "1726072214", 25 | ), 26 | /*[4]*/ ( 27 | commit: "d09880c77148824f504db0df259180b023c4319f", 28 | elapsed_ms: 5, 29 | db_size_bytes: 18446, 30 | timestamp: "1726072267", 31 | ), 32 | /*[5]*/ ( 33 | commit: "d09880c77148824f504db0df259180b023c4319f", 34 | elapsed_ms: 5, 35 | db_size_bytes: 18446, 36 | timestamp: "1726072405", 37 | ), 38 | /*[6]*/ ( 39 | commit: "d09880c77148824f504db0df259180b023c4319f", 40 | elapsed_ms: 0, 41 | db_size_bytes: 11132, 42 | timestamp: "1726072431", 43 | ), 44 | /*[7]*/ ( 45 | commit: "d09880c77148824f504db0df259180b023c4319f", 46 | elapsed_ms: 0, 47 | db_size_bytes: 11132, 48 | timestamp: "1726072483", 49 | ), 50 | ] -------------------------------------------------------------------------------- /karta_server/src/server/asset_endpoints.rs: -------------------------------------------------------------------------------- 1 | use axum::{ 2 | body::Body, 3 | extract::{Path, State}, 4 | response::{IntoResponse, Response}, 5 | http::{header, StatusCode}, 6 | }; 7 | use uuid::Uuid; 8 | use crate::{prelude::GraphNodes, server::AppState}; 9 | use crate::elements::node_path::NodeHandle; 10 | use tokio::fs; 11 | 12 | #[axum::debug_handler] 13 | pub async fn get_asset( 14 | State(state): State, 15 | Path(path): Path, 16 | ) -> impl IntoResponse { 17 | // With lazy indexing, we don't query the database. We construct the path directly. 18 | let absolute_path = { 19 | let service = state.service.read().unwrap(); 20 | service.vault_fs_path().join(&path) 21 | }; 22 | 23 | println!("[Asset Endpoint] Attempting to read asset from: {:?}", absolute_path); 24 | 25 | // Read the file from the filesystem 26 | let file_contents = match fs::read(&absolute_path).await { 27 | Ok(contents) => contents, 28 | Err(e) => { 29 | println!("[Asset Endpoint] Error reading file for path \"{}\": {:?}", path, e); 30 | let error_message = format!("Asset file not found on disk for path: {}", path); 31 | return (StatusCode::NOT_FOUND, error_message).into_response(); 32 | } 33 | }; 34 | 35 | // 5. Determine the MIME type from the file extension 36 | let mime_type = mime_guess::from_path(&absolute_path) 37 | .first_or_octet_stream() 38 | .to_string(); 39 | 40 | // 6. Build and return the response 41 | Response::builder() 42 | .status(StatusCode::OK) 43 | .header(header::CONTENT_TYPE, mime_type) 44 | .body(Body::from(file_contents)) 45 | .unwrap() 46 | } -------------------------------------------------------------------------------- /karta_svelte/src/lib/karta/EdgeSelectionStore.ts: -------------------------------------------------------------------------------- 1 | import { writable } from 'svelte/store'; 2 | import type { EdgeId } from '$lib/types/types'; 3 | 4 | // Store to hold the IDs of currently selected edges 5 | export const selectedEdgeIds = writable>(new Set()); 6 | 7 | // Action to clear the current edge selection 8 | export function clearEdgeSelection() { 9 | selectedEdgeIds.set(new Set()); 10 | } 11 | 12 | // Action to set the selected edges (replaces current selection) 13 | export function setSelectedEdges(edgeIds: EdgeId[]) { 14 | console.log('[EdgeSelectionStore] Setting selected edges:', edgeIds); 15 | selectedEdgeIds.set(new Set(edgeIds)); 16 | } 17 | 18 | // Action to toggle the selection state of a single edge 19 | export function toggleEdgeSelection(edgeId: EdgeId, add: boolean = false, subtract: boolean = false) { 20 | selectedEdgeIds.update(currentSelection => { 21 | console.log('[EdgeSelectionStore] Current selection before toggle:', Array.from(currentSelection)); 22 | const newSelection = new Set(currentSelection); 23 | if (add) { 24 | newSelection.add(edgeId); 25 | } else if (subtract) { 26 | newSelection.delete(edgeId); 27 | } else { 28 | // Default behavior: toggle 29 | if (newSelection.has(edgeId)) { 30 | newSelection.delete(edgeId); 31 | } else { 32 | newSelection.add(edgeId); 33 | } 34 | } 35 | console.log('[EdgeSelectionStore] New selection after toggle:', Array.from(newSelection)); 36 | return newSelection; 37 | }); 38 | } 39 | 40 | // Action to deselect a single edge 41 | export function deselectEdge(edgeId: EdgeId) { 42 | selectedEdgeIds.update(currentSelection => { 43 | const newSelection = new Set(currentSelection); 44 | newSelection.delete(edgeId); 45 | return newSelection; 46 | }); 47 | } -------------------------------------------------------------------------------- /karta_svelte/src/lib/components/ColorPickerPopup.svelte: -------------------------------------------------------------------------------- 1 | 41 | 42 | {#if $colorPickerStore.isOpen} 43 |
colorPickerStore.close()} 46 | on:contextmenu|preventDefault 47 | >
48 |
53 | 58 |
59 | {/if} -------------------------------------------------------------------------------- /karta_svelte/src/lib/util/menuPositioning.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Calculates bounds-aware position for menus to prevent them from appearing outside the viewport 3 | */ 4 | export function calculateBoundsAwarePosition( 5 | targetX: number, 6 | targetY: number, 7 | menuElement: HTMLElement 8 | ): { x: number; y: number } { 9 | const rect = menuElement.getBoundingClientRect(); 10 | const viewportWidth = window.innerWidth; 11 | const viewportHeight = window.innerHeight; 12 | 13 | let x = targetX; 14 | let y = targetY; 15 | 16 | // Minimum offset from click point to avoid covering it 17 | const minOffset = 10; 18 | 19 | console.log('menuPositioning Debug:', { 20 | target: { x: targetX, y: targetY }, 21 | menuSize: { width: rect.width, height: rect.height }, 22 | viewport: { width: viewportWidth, height: viewportHeight }, 23 | wouldExceedRight: x + rect.width > viewportWidth, 24 | wouldExceedBottom: y + rect.height > viewportHeight 25 | }); 26 | 27 | // Check right boundary 28 | if (x + rect.width > viewportWidth) { 29 | x = targetX - rect.width - minOffset; // Position to the left of click 30 | console.log('Adjusted X to left:', x); 31 | } else { 32 | x = targetX + minOffset; // Default: slightly right of click 33 | console.log('Keeping X to right:', x); 34 | } 35 | 36 | // Check bottom boundary 37 | if (y + rect.height > viewportHeight) { 38 | y = targetY - rect.height - minOffset; // Position above click 39 | console.log('Adjusted Y to above:', y); 40 | } else { 41 | y = targetY + minOffset; // Default: slightly below click 42 | console.log('Keeping Y below:', y); 43 | } 44 | 45 | // Ensure we don't go off the left edge 46 | x = Math.max(5, x); 47 | 48 | // Ensure we don't go off the top edge 49 | y = Math.max(5, y); 50 | 51 | console.log('Final position:', { x, y }); 52 | 53 | return { x, y }; 54 | } 55 | -------------------------------------------------------------------------------- /karta_server/src/elements/view_node.rs: -------------------------------------------------------------------------------- 1 | use uuid::Uuid; 2 | 3 | use crate::prelude::Attribute; 4 | 5 | use super::node::DataNode; 6 | 7 | #[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)] 8 | pub enum ViewNodeStatus { 9 | Generated, 10 | Modified, 11 | } 12 | 13 | #[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)] 14 | pub struct ViewNode { 15 | pub uuid: Uuid, 16 | 17 | pub status: ViewNodeStatus, 18 | 19 | pub is_name_visible: bool, 20 | 21 | pub relX: f32, 22 | pub relY: f32, 23 | pub width: f32, 24 | pub height: f32, 25 | pub relScale: f32, 26 | pub rotation: f32, 27 | 28 | pub attributes: Vec, 29 | } 30 | 31 | impl ViewNode { 32 | pub fn uuid(&self) -> Uuid { 33 | self.uuid 34 | } 35 | 36 | pub fn from_data_node(data_node: DataNode) -> Self { 37 | let viewnode_attributes: Vec = Vec::new(); 38 | 39 | Self { 40 | uuid: data_node.uuid(), 41 | status: ViewNodeStatus::Generated, 42 | is_name_visible: true, 43 | relX: 0.0, 44 | relY: 0.0, 45 | width: 200.0, 46 | height: 200.0, 47 | relScale: 1.0, 48 | rotation: 0.0, 49 | attributes: viewnode_attributes, 50 | } 51 | 52 | } 53 | 54 | pub fn status(&self) -> &ViewNodeStatus { 55 | &self.status 56 | } 57 | 58 | pub fn set_status(&mut self, status: ViewNodeStatus) { 59 | self.status = status; 60 | } 61 | 62 | pub fn sized(mut self, width: f32, height: f32) -> Self { 63 | self.width = width; 64 | self.height = height; 65 | self 66 | } 67 | 68 | pub fn positioned(mut self, relX: f32, relY: f32) -> Self { 69 | self.relX = relX; 70 | self.relY = relY; 71 | self 72 | } 73 | } -------------------------------------------------------------------------------- /karta_server/src/graph_agdb/mod.rs: -------------------------------------------------------------------------------- 1 | use std::{error::Error, path::PathBuf}; 2 | 3 | use agdb::{CountComparison, DbElement, DbError, DbId, DbUserValue, QueryBuilder, QueryError}; 4 | use crate::graph_traits::{Graph, StoragePath}; 5 | 6 | pub (crate) mod graph_core; 7 | pub (crate) mod graph_node; 8 | pub (crate) mod graph_edge; 9 | 10 | /// The main graph structure to be interacted with. 11 | /// 12 | /// bevy_karta_client will instantiate this as a Resource through a newtype. 13 | pub struct GraphAgdb { 14 | /// The name of the application using this library. 15 | name: String, 16 | 17 | /// AGDB database. 18 | db: agdb::Db, 19 | 20 | /// Path to the root directory of the graph. 21 | /// All paths are relative to this root. 22 | vault_fs_path: std::path::PathBuf, 23 | 24 | /// Path to the where the db is stored in the file system. 25 | /// Includes the name of the directory. 26 | storage_path: std::path::PathBuf, 27 | } 28 | 29 | 30 | /// Agdb has multiple implementations. If the size of the database is small enough, it can be stored in memory. 31 | /// If the database is too large, it can be stored in a file. 32 | /// TODO: Not in use currently. 33 | enum GraphDb { 34 | Mem(agdb::Db), 35 | File(agdb::DbFile), 36 | } 37 | 38 | impl Graph for GraphAgdb {} 39 | 40 | impl GraphAgdb { 41 | /// Direct getter for the db. Not recommended to use. If possible, 42 | /// use the other implemented functions. They are the intended way 43 | /// of interacting with the db. 44 | pub fn db(&self) -> &agdb::Db { 45 | &self.db 46 | } 47 | 48 | /// Direct mutable getter for the db. Not recommended to use. If possible, 49 | /// use the other implemented functions. They are the intended way 50 | /// of interacting with the db. 51 | pub fn db_mut(&mut self) -> &mut agdb::Db { 52 | &mut self.db 53 | } 54 | } 55 | 56 | -------------------------------------------------------------------------------- /karta_svelte/src/lib/karta/SelectionStore.ts: -------------------------------------------------------------------------------- 1 | import { writable, get } from 'svelte/store'; 2 | import type { NodeId } from '../types/types'; 3 | 4 | export const selectedNodeIds = writable>(new Set()); 5 | 6 | /** Clears the current selection. */ 7 | export function clearSelection() { 8 | selectedNodeIds.update(currentSelection => { 9 | if (currentSelection.size > 0) { 10 | return new Set(); // Return new Set to trigger update 11 | } 12 | return currentSelection; // No change if already empty 13 | }); 14 | } 15 | 16 | /** 17 | * Sets the selection to the provided node IDs. 18 | * @param nodeIds A single NodeId or an array/Set of NodeIds. 19 | */ 20 | export function setSelectedNodes(nodeIds: NodeId | NodeId[] | Set) { 21 | const idsToSelect = new Set(Array.isArray(nodeIds) ? nodeIds : nodeIds instanceof Set ? Array.from(nodeIds) : [nodeIds]); 22 | selectedNodeIds.set(idsToSelect); 23 | } 24 | 25 | 26 | /** Deselects a specific node. */ 27 | export function deselectNode(nodeId: NodeId) { 28 | selectedNodeIds.update(currentSelection => { 29 | if (!currentSelection.has(nodeId)) { 30 | return currentSelection; // No change needed 31 | } 32 | // Create a new Set without the specified nodeId 33 | const nextSelection = new Set(currentSelection); 34 | nextSelection.delete(nodeId); 35 | return nextSelection; // Return the new Set 36 | }); 37 | } 38 | 39 | /** Toggles the selection state of a single node. */ 40 | export function toggleSelection(nodeId: NodeId) { 41 | selectedNodeIds.update(currentSelection => { 42 | // Create a new Set based on the current one 43 | const nextSelection = new Set(currentSelection); 44 | if (nextSelection.has(nodeId)) { 45 | nextSelection.delete(nodeId); // Modify the new Set 46 | } else { 47 | nextSelection.add(nodeId); // Modify the new Set 48 | } 49 | return nextSelection; // Return the new Set 50 | }); 51 | } -------------------------------------------------------------------------------- /karta_svelte/src/lib/tools/ContextTool.ts: -------------------------------------------------------------------------------- 1 | import type { Tool, NodeId } from '$lib/types/types'; 2 | import { switchContext } from '$lib/karta/ContextStore'; // Import the action 3 | 4 | export class ContextTool implements Tool { 5 | readonly name = 'context'; 6 | activate() { 7 | } 8 | deactivate() { 9 | document.body.style.cursor = 'default'; // Reset cursor 10 | } 11 | 12 | // Implement onPointerDown to handle context switching on node click 13 | onPointerDown(event: PointerEvent, targetElement: EventTarget | null): void { 14 | if (event.button !== 0 || !(targetElement instanceof HTMLElement)) return; 15 | 16 | // Check if the target is a node element 17 | const nodeEl = targetElement.closest('.node-wrapper') as HTMLElement | null; // Use the new wrapper class 18 | if (!nodeEl || !nodeEl.dataset.id) return; // Exit if not a node 19 | 20 | const nodeId = nodeEl.dataset.id; 21 | 22 | // Call the KartaStore action to handle the switch 23 | switchContext({ type: 'uuid', value: nodeId }); 24 | } 25 | 26 | // --- Removed Obsolete Methods --- 27 | // onNodeMouseDown, onWindowMouseMove, onWindowMouseUp, 28 | // onCanvasClick, onCanvasMouseDown, 29 | // getNodeCursorStyle, getCanvasCursorStyle 30 | 31 | // Optional: Implement updateCursor if needed 32 | updateCursor(): void { 33 | document.body.style.cursor = 'context-menu'; // Example cursor 34 | } 35 | 36 | // Add other required methods from Tool interface if needed (onPointerMove, onPointerUp, etc.) 37 | // For now, they can be empty or omitted if not used by this tool. 38 | // onPointerMove(event: PointerEvent): void {} 39 | // onPointerUp(event: PointerEvent): void {} 40 | // onWheel(event: WheelEvent): void {} 41 | // onKeyDown(event: KeyboardEvent): void {} 42 | // onKeyUp(event: KeyboardEvent): void {} 43 | } 44 | -------------------------------------------------------------------------------- /karta_server/src/server/write_endpoints_tests/test_node_creation.rs: -------------------------------------------------------------------------------- 1 | use super::*; 2 | use super::test_helpers::*; 3 | 4 | #[tokio::test] 5 | async fn test_create_node_outside_vault_fails() { 6 | let (router, _test_ctx) = setup_test_environment("create_node_fails"); 7 | 8 | // Arrange 9 | let payload = CreateNodePayload { 10 | name: "test_node".to_string(), 11 | ntype: NodeTypeId::file_type(), 12 | parent_path: "/some_other_path".to_string(), 13 | attributes: vec![], 14 | }; 15 | let payload_json = serde_json::to_string(&payload).unwrap(); 16 | 17 | // Act 18 | let response = execute_post_request(router, "/api/nodes", payload_json).await; 19 | 20 | // Assert 21 | assert_eq!(response.status(), StatusCode::BAD_REQUEST); 22 | } 23 | 24 | #[tokio::test] 25 | async fn test_create_node_inside_vault_succeeds() { 26 | let (router, test_ctx) = setup_test_environment("create_node_succeeds"); 27 | 28 | // Arrange 29 | let payload = CreateNodePayload { 30 | name: "test_node".to_string(), 31 | ntype: NodeTypeId::file_type(), 32 | parent_path: "/vault/some_dir".to_string(), 33 | attributes: vec![], 34 | }; 35 | let payload_json = serde_json::to_string(&payload).unwrap(); 36 | 37 | // Act 38 | let response = execute_post_request(router, "/api/nodes", payload_json).await; 39 | 40 | // Assert 41 | assert_eq!(response.status(), StatusCode::OK); 42 | let body = axum::body::to_bytes(response.into_body(), usize::MAX).await.unwrap(); 43 | let created_node: DataNode = serde_json::from_slice(&body).unwrap(); 44 | assert_eq!(created_node.path().alias(), "/vault/some_dir/test_node"); 45 | 46 | // Verify it was actually inserted 47 | let node_from_db = test_ctx.with_service(|s| { 48 | s.data().open_node(&NodeHandle::Path(created_node.path())) 49 | }); 50 | assert!(node_from_db.is_ok()); 51 | } 52 | -------------------------------------------------------------------------------- /karta_svelte/src/lib/components/FilterMenu.svelte: -------------------------------------------------------------------------------- 1 | 26 | 27 |
28 | 29 |
30 | 46 |
47 |
48 | 49 | 54 | -------------------------------------------------------------------------------- /karta_svelte/src/lib/debug/loggers.ts: -------------------------------------------------------------------------------- 1 | import { isDebugMode } from './config'; 2 | import { get } from 'svelte/store'; 3 | 4 | // Base logger class 5 | function replacer(key: any, value: any) { 6 | if (value instanceof Map) { 7 | return { 8 | dataType: 'Map', 9 | value: Array.from(value.entries()), 10 | }; 11 | } 12 | return value; 13 | } 14 | 15 | class Logger { 16 | private stringifyByDefault: boolean; 17 | 18 | constructor(private name: string, { stringifyByDefault = false } = {}) { 19 | this.stringifyByDefault = stringifyByDefault; 20 | } 21 | 22 | private _log(level: 'log' | 'warn' | 'error', args: any[]) { 23 | if (!get(isDebugMode)) return; 24 | 25 | let localStringify = this.stringifyByDefault; 26 | let finalArgs = args; 27 | 28 | if (typeof args[0] === 'boolean') { 29 | localStringify = args[0]; 30 | finalArgs = args.slice(1); 31 | } 32 | 33 | const processedArgs = localStringify 34 | ? finalArgs.map(arg => (typeof arg === 'object' ? JSON.stringify(arg, replacer, 2) : arg)) 35 | : finalArgs; 36 | 37 | console[level](`[${this.name}]`, ...processedArgs); 38 | } 39 | 40 | log(...args: any[]) { 41 | this._log('log', args); 42 | } 43 | 44 | warn(...args: any[]) { 45 | this._log('warn', args); 46 | } 47 | 48 | error(...args: any[]) { 49 | this._log('error', args); 50 | } 51 | } 52 | 53 | // Specialized loggers 54 | export const lifecycleLogger = new Logger('Lifecycle'); 55 | export const storeLogger = new Logger('Store', { stringifyByDefault: true }); 56 | export const apiLogger = new Logger('API'); 57 | export const interactionLogger = new Logger('Interaction'); 58 | 59 | // Reactive logger for Svelte stores 60 | export function watchStore(store: any, name: string) { 61 | // Temporarily disabled to clean up console 62 | } -------------------------------------------------------------------------------- /karta_server/docs/perf_reports/fs_graph_test_node_context_can_be_indexed.ron: -------------------------------------------------------------------------------- 1 | [ 2 | /*[0]*/ ( 3 | commit: "f7c2f5cc7f2a730210d5f062c0f1e36f1972b3e5", 4 | elapsed_ms: 0, 5 | db_size_bytes: 11066, 6 | timestamp: "1726395022", 7 | ), 8 | /*[1]*/ ( 9 | commit: "f7c2f5cc7f2a730210d5f062c0f1e36f1972b3e5", 10 | elapsed_ms: 0, 11 | db_size_bytes: 11066, 12 | timestamp: "1726405008", 13 | ), 14 | /*[2]*/ ( 15 | commit: "f7c2f5cc7f2a730210d5f062c0f1e36f1972b3e5", 16 | elapsed_ms: 0, 17 | db_size_bytes: 11066, 18 | timestamp: "1726405084", 19 | ), 20 | /*[3]*/ ( 21 | commit: "f7c2f5cc7f2a730210d5f062c0f1e36f1972b3e5", 22 | elapsed_ms: 0, 23 | db_size_bytes: 11066, 24 | timestamp: "1726405147", 25 | ), 26 | /*[4]*/ ( 27 | commit: "f7c2f5cc7f2a730210d5f062c0f1e36f1972b3e5", 28 | elapsed_ms: 0, 29 | db_size_bytes: 11066, 30 | timestamp: "1726405233", 31 | ), 32 | /*[5]*/ ( 33 | commit: "f7c2f5cc7f2a730210d5f062c0f1e36f1972b3e5", 34 | elapsed_ms: 0, 35 | db_size_bytes: 11066, 36 | timestamp: "1726405291", 37 | ), 38 | /*[6]*/ ( 39 | commit: "f7c2f5cc7f2a730210d5f062c0f1e36f1972b3e5", 40 | elapsed_ms: 0, 41 | db_size_bytes: 11066, 42 | timestamp: "1726405587", 43 | ), 44 | /*[7]*/ ( 45 | commit: "f7c2f5cc7f2a730210d5f062c0f1e36f1972b3e5", 46 | elapsed_ms: 0, 47 | db_size_bytes: 11066, 48 | timestamp: "1726405822", 49 | ), 50 | /*[8]*/ ( 51 | commit: "f7c2f5cc7f2a730210d5f062c0f1e36f1972b3e5", 52 | elapsed_ms: 1, 53 | db_size_bytes: 11066, 54 | timestamp: "1726406002", 55 | ), 56 | /*[9]*/ ( 57 | commit: "f7c2f5cc7f2a730210d5f062c0f1e36f1972b3e5", 58 | elapsed_ms: 18, 59 | db_size_bytes: 17555, 60 | timestamp: "1726406170", 61 | ), 62 | ] -------------------------------------------------------------------------------- /karta_svelte/src/lib/node_types/types.ts: -------------------------------------------------------------------------------- 1 | // --- Karta Runtime Component --- 2 | // This file is planned for inclusion in the MIT-licensed `karta_runtime` package. 3 | // It defines types related to node type definitions and their static properties. 4 | // Essential for both editor and runtime. 5 | 6 | import type { SvelteComponent } from 'svelte'; 7 | import type { DataNode, ViewNode, TweenableNodeState, PropertyDefinition } from '$lib/types/types'; // Import base types and PropertyDefinition 8 | 9 | // Define the expected props for any node type component rendered by NodeWrapper 10 | export interface NodeTypeProps { 11 | dataNode: DataNode; 12 | viewNode: ViewNode; 13 | } 14 | 15 | // Define a specific type for our node components using Svelte 5's component type 16 | export type NodeTypeComponent = typeof SvelteComponent; 17 | 18 | // Define a type for simple icon components (optional) 19 | // Using 'any' for props as icon props can vary widely (size, strokeWidth, class, etc.) 20 | export type IconComponent = typeof SvelteComponent; 21 | 22 | 23 | // Defines the contract for a node type module 24 | export interface NodeTypeDefinition { 25 | // The unique string identifier for this node type (matches DataNode.ntype) 26 | ntype: string; 27 | 28 | // The Svelte component for rendering the node's content 29 | // component: NodeTypeComponent; // Component is the default export of the .svelte file 30 | 31 | // Function to get the default data attributes for a new node of this type 32 | // Takes an optional baseName suggestion 33 | getDefaultAttributes: (baseName?: string) => Record; 34 | 35 | // Function to get the default intrinsic visual properties (size, initial scale/rotation) 36 | // Excludes position (x, y) which is determined at creation time. 37 | getDefaultViewNodeState: () => Omit; 38 | 39 | // Optional properties for future use (e.g., in menus) 40 | displayName?: string; // User-friendly name for UI 41 | icon?: IconComponent; // e.g., a Lucide icon component 42 | 43 | // Optional schema defining editable properties for this node type 44 | propertySchema?: PropertyDefinition[]; 45 | } -------------------------------------------------------------------------------- /karta_svelte/src/lib/components/ThemeEditor.svelte: -------------------------------------------------------------------------------- 1 | 31 | 32 |
33 |

Theme Colors

34 |
35 | {#each themeColors as color} 36 |
37 | 38 | 43 |
44 | {/each} 45 |
46 |
47 | 53 |
54 |
55 | -------------------------------------------------------------------------------- /karta_server/src/server/search_endpoints.rs: -------------------------------------------------------------------------------- 1 | use axum::{extract::{State, Query}, http::StatusCode, Json}; 2 | use serde::{Deserialize, Serialize}; 3 | use uuid::Uuid; 4 | 5 | use super::AppState; 6 | 7 | #[derive(Serialize, Deserialize, Debug, Clone)] 8 | pub struct SearchResult { 9 | pub id: Option, // UUID if indexed in database 10 | pub path: String, // Full path from vault root 11 | pub ntype: String, // Node type (File, Directory, etc.) 12 | pub is_indexed: bool, // Whether it exists in database 13 | pub score: f64, // Fuzzy match score (0.0-1.0) 14 | pub match_indices: Option>, // Character indices that matched the query 15 | } 16 | 17 | #[derive(Deserialize)] 18 | pub struct SearchQuery { 19 | pub q: String, // Search query 20 | #[serde(default = "default_limit")] 21 | pub limit: usize, // Max results (default 100 for infinite scroll) 22 | #[serde(default = "default_min_score")] 23 | pub min_score: f64, // Filter low-quality matches (default 0.1) 24 | } 25 | 26 | #[derive(Serialize, Deserialize)] 27 | pub struct SearchResponse { 28 | pub results: Vec, 29 | pub total_found: usize, // How many matched (before limit) 30 | pub truncated: bool, // Whether results were limited 31 | pub query: String, 32 | pub took_ms: u64, 33 | } 34 | 35 | fn default_limit() -> usize { 36 | 100 37 | } 38 | 39 | fn default_min_score() -> f64 { 40 | 0.1 41 | } 42 | 43 | pub async fn search_nodes( 44 | State(app_state): State, 45 | Query(query): Query, 46 | ) -> Result, StatusCode> { 47 | // Validate input 48 | if query.q.trim().is_empty() { 49 | return Ok(Json(SearchResponse { 50 | results: vec![], 51 | total_found: 0, 52 | truncated: false, 53 | query: query.q, 54 | took_ms: 0, 55 | })); 56 | } 57 | 58 | let service = app_state.service.read().unwrap(); 59 | match service.search_nodes(&query.q, query.limit, query.min_score) { 60 | Ok(response) => Ok(Json(response)), 61 | Err(_) => Err(StatusCode::INTERNAL_SERVER_ERROR), 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /karta_svelte/src/lib/util/PersistenceService.ts: -------------------------------------------------------------------------------- 1 | import type { AssetData, Context, ContextBundle, DataNode, EdgeDeletionPayload, KartaEdge, KartaEdgeCreationPayload, NodeId, StorableContext, DeleteNodesResponse, SearchQuery, SearchResponse } from "$lib/types/types"; 2 | 3 | export interface PersistenceService { 4 | // Node methods 5 | saveNode(node: DataNode): Promise; 6 | getNode(nodeId: string): Promise; 7 | deleteNodes(nodeHandles: string[]): Promise; // Can be paths or UUIDs 8 | getNodes(): Promise; 9 | checkNameExists(name: string): Promise; 10 | getDataNodesByIds(nodeIds: NodeId[]): Promise>; 11 | getAllNodePaths(): Promise; 12 | getDataNodeByPath(path: string): Promise; 13 | 14 | // Edge methods 15 | createEdges(edges: KartaEdgeCreationPayload[]): Promise; 16 | getEdge(edgeId: string): Promise; 17 | getEdges(): Promise; 18 | deleteEdges(payload: EdgeDeletionPayload[]): Promise; 19 | reconnectEdge(old_from: NodeId, old_to: NodeId, new_from: NodeId, new_to: NodeId, new_from_path: string, new_to_path: string): Promise; 20 | loadEdges(): Promise; 21 | getEdgesByNodeIds(nodeIds: NodeId[]): Promise>; 22 | 23 | // Context methods 24 | // saveContext still takes the in-memory Context (with Tweens) 25 | saveContext(context: Context): Promise; 26 | // getContext returns the StorableContext read from DB 27 | getContext(contextId: NodeId): Promise; 28 | getAllContextIds(): Promise; 29 | deleteContext(contextId: NodeId): Promise; 30 | getAllContextPaths(): Promise>; 31 | loadContextBundle(identifier: string): Promise; 32 | 33 | // Asset methods 34 | saveAsset(assetId: string, assetData: AssetData): Promise; 35 | getAsset(assetId: string): Promise; 36 | deleteAsset(assetId: string): Promise; 37 | getAssetObjectUrl(assetId: string): Promise; 38 | 39 | // Search methods 40 | searchNodes(query: SearchQuery): Promise; 41 | } -------------------------------------------------------------------------------- /karta_svelte/src/lib/components/ConfirmationDialog.svelte: -------------------------------------------------------------------------------- 1 | 27 | 28 | {#if $isConfirmationDialogOpen} 29 | 30 | 63 | {/if} 64 | 65 | -------------------------------------------------------------------------------- /karta_svelte/src/routes/+layout.svelte: -------------------------------------------------------------------------------- 1 | 74 | 75 | {@render children()} 76 | 77 | 78 | 79 | -------------------------------------------------------------------------------- /karta_svelte/src/lib/tauri/server.ts: -------------------------------------------------------------------------------- 1 | import { invoke } from '@tauri-apps/api/core'; 2 | import { API_BASE } from '$lib/apiBase'; 3 | 4 | export interface VaultInfo { 5 | path: string; 6 | exists: boolean; 7 | has_karta_dir: boolean; 8 | } 9 | 10 | export interface ServerManager { 11 | startServer(vaultPath: string): Promise; 12 | stopServer(): Promise; 13 | checkServerStatus(): Promise; 14 | pollForServerReady(maxAttempts?: number, intervalMs?: number): Promise; 15 | getAvailableVaults(): Promise; 16 | selectVaultDirectory(): Promise; 17 | addVaultToConfig(vaultPath: string): Promise; 18 | } 19 | 20 | export const serverManager: ServerManager = { 21 | async startServer(vaultPath: string): Promise { 22 | return await invoke('start_server', { vaultPath }); 23 | }, 24 | 25 | async stopServer(): Promise { 26 | return await invoke('stop_server'); 27 | }, 28 | 29 | async checkServerStatus(): Promise { 30 | // Prefer browser-side fetch to avoid blocking native event loop on Linux 31 | const controller = new AbortController(); 32 | const timeout = setTimeout(() => controller.abort(), 1000); 33 | try { 34 | const res = await fetch(`${API_BASE}/`, { method: 'GET', signal: controller.signal, cache: 'no-store' }); 35 | return res.ok; 36 | } catch { 37 | return false; 38 | } finally { 39 | clearTimeout(timeout); 40 | } 41 | }, 42 | 43 | async pollForServerReady(maxAttempts = 30, intervalMs = 500): Promise { 44 | for (let i = 0; i < maxAttempts; i++) { 45 | const isReady = await this.checkServerStatus(); 46 | if (isReady) { 47 | return; 48 | } 49 | 50 | // Wait before next attempt 51 | await new Promise(resolve => setTimeout(resolve, intervalMs)); 52 | } 53 | 54 | throw new Error(`Server failed to start within ${(maxAttempts * intervalMs) / 1000} seconds`); 55 | }, 56 | 57 | async getAvailableVaults(): Promise { 58 | return await invoke('get_available_vaults'); 59 | }, 60 | 61 | async selectVaultDirectory(): Promise { 62 | return await invoke('select_vault_directory'); 63 | }, 64 | 65 | async addVaultToConfig(vaultPath: string): Promise { 66 | return await invoke('add_vault_to_config', { vaultPath }); 67 | } 68 | }; 69 | -------------------------------------------------------------------------------- /karta_svelte/src/lib/karta/SettingsStore.ts: -------------------------------------------------------------------------------- 1 | import { writable } from 'svelte/store'; 2 | import type { KartaSettings, ColorTheme } from '$lib/types/types'; 3 | import { ServerAdapter } from '$lib/util/ServerAdapter'; 4 | 5 | const adapter = new ServerAdapter(); 6 | 7 | // Default settings 8 | const defaultSettings: KartaSettings = { 9 | version: 0.1, 10 | savelastViewedContextPath: true, 11 | lastViewedContextPath: null, 12 | colorTheme: { 13 | 'viewport-bg': '#000000', 14 | 'panel-bg': '#431d1f', 15 | 'focal-hl': '#f4902dff', 16 | 'panel-hl': '#741f2fff', 17 | 'text-color': '#f0f0f0', 18 | 'contrast-color': '#60a5fa', 19 | 'connection-color': '#505050' 20 | }, 21 | edgeFilters: { 22 | containsEdges: 'always', 23 | normalEdges: 'always' 24 | } 25 | }; 26 | 27 | // Create the writable store, initialized with defaults 28 | const { subscribe, set, update } = writable(defaultSettings); 29 | 30 | // Function to load settings from the server 31 | async function loadSettings() { 32 | try { 33 | const serverSettings = await adapter.getSettings(); 34 | if (serverSettings) { 35 | // Deep merge loaded settings with defaults to ensure all keys exist 36 | const mergedSettings = { 37 | ...defaultSettings, 38 | ...serverSettings, 39 | colorTheme: { ...defaultSettings.colorTheme, ...serverSettings.colorTheme }, 40 | edgeFilters: { ...defaultSettings.edgeFilters, ...serverSettings.edgeFilters } 41 | }; 42 | set(mergedSettings); 43 | } else { 44 | // No settings found on server, save the defaults 45 | await adapter.saveSettings(defaultSettings); 46 | set(defaultSettings); 47 | } 48 | } catch (error) { 49 | console.error('Error loading Karta settings from server:', error); 50 | // Fallback to default settings if loading fails 51 | set(defaultSettings); 52 | } 53 | } 54 | 55 | // Function to update settings and save to the server 56 | export async function updateSettings(newSettings: Partial) { 57 | let updatedSettings: KartaSettings | undefined; 58 | update((currentSettings) => { 59 | updatedSettings = { ...currentSettings, ...newSettings }; 60 | return updatedSettings; 61 | }); 62 | 63 | if (updatedSettings) { 64 | try { 65 | await adapter.saveSettings(updatedSettings); 66 | } catch (error) { 67 | console.error('Error saving Karta settings to server:', error); 68 | } 69 | } 70 | } 71 | 72 | // Export the store interface 73 | export const settings = { 74 | subscribe, 75 | loadSettings, // Expose load function to be called on app init 76 | updateSettings, // Expose update function 77 | defaultSettings // Expose default settings for reset functionality 78 | }; -------------------------------------------------------------------------------- /karta_svelte/src/lib/node_types/GenericNode.svelte: -------------------------------------------------------------------------------- 1 | 7 | 35 | 36 | 55 | 56 |
63 | 64 | 65 |
66 | 67 | -------------------------------------------------------------------------------- /karta_svelte/src/lib/node_types/DirectoryNode.svelte: -------------------------------------------------------------------------------- 1 | 7 | 35 | 36 | 56 | 57 | 58 |
66 | 67 |
68 | 69 | -------------------------------------------------------------------------------- /karta_svelte/src/routes/+page.svelte: -------------------------------------------------------------------------------- 1 | 58 | 59 | {#if isTauriApp} 60 | 61 | {/if} 62 | 63 | {#if isServerReady} 64 |
65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 |
74 |
75 | {/if} 76 | -------------------------------------------------------------------------------- /karta_svelte/src/lib/karta/ColorPickerStore.ts: -------------------------------------------------------------------------------- 1 | import { writable, get } from 'svelte/store'; 2 | import { settings, updateSettings } from '$lib/karta/SettingsStore'; 3 | import type { ColorTheme } from '$lib/types/types'; 4 | 5 | type ColorPickerState = { 6 | isOpen: boolean; 7 | initialColor: string; 8 | currentColor: string; 9 | position: { x: number; y: number }; 10 | onUpdate: (color: string) => void; 11 | onClose?: (finalColor: string) => void; 12 | }; 13 | 14 | const initialState: ColorPickerState = { 15 | isOpen: false, 16 | initialColor: '#ffffff', 17 | currentColor: '#ffffff', 18 | position: { x: 0, y: 0 }, 19 | onUpdate: () => {}, 20 | onClose: undefined 21 | }; 22 | 23 | const { subscribe, update } = writable(initialState); 24 | 25 | function open(initialColor: string, event: MouseEvent, onUpdate: (color: string) => void, onClose?: (finalColor: string) => void) { 26 | const rect = (event.currentTarget as HTMLElement).getBoundingClientRect(); 27 | 28 | // Calculate position with viewport bounds 29 | const viewportWidth = window.innerWidth; 30 | const viewportHeight = window.innerHeight; 31 | const pickerWidth = 280; // Approximate width of color picker 32 | const pickerHeight = 350; // Approximate height of color picker 33 | 34 | let x = rect.left; 35 | let y = rect.bottom + 5; 36 | 37 | // Keep within right boundary 38 | if (x + pickerWidth > viewportWidth) { 39 | x = viewportWidth - pickerWidth - 10; 40 | } 41 | 42 | // Keep within left boundary 43 | if (x < 10) { 44 | x = 10; 45 | } 46 | 47 | // Check if picker would go below viewport 48 | if (y + pickerHeight > viewportHeight) { 49 | // Position above the button instead 50 | y = rect.top - pickerHeight - 5; 51 | 52 | // If still outside viewport, position at top 53 | if (y < 10) { 54 | y = 10; 55 | } 56 | } 57 | 58 | update((state) => ({ 59 | ...state, 60 | isOpen: true, 61 | initialColor, 62 | currentColor: initialColor, 63 | position: { x, y }, 64 | onUpdate, 65 | onClose 66 | })); 67 | } 68 | 69 | let currentState: ColorPickerState = initialState; 70 | // Track state for internal use 71 | const storeInstance = { subscribe, update }; 72 | storeInstance.subscribe(state => { 73 | currentState = state; 74 | }); 75 | 76 | function updateColor(color: string) { 77 | update((state) => ({ 78 | ...state, 79 | currentColor: color 80 | })); 81 | 82 | // Call the immediate update callback for visual feedback 83 | if (currentState.onUpdate) { 84 | currentState.onUpdate(color); 85 | } 86 | } 87 | 88 | function close() { 89 | if (currentState.onClose) { 90 | currentState.onClose(currentState.currentColor); 91 | } 92 | update((state) => ({ ...state, isOpen: false, onClose: undefined })); 93 | } 94 | 95 | export const colorPickerStore = { 96 | subscribe, 97 | open, 98 | close, 99 | updateColor 100 | }; -------------------------------------------------------------------------------- /karta_server/src/server/write_endpoints_tests/test_helpers.rs: -------------------------------------------------------------------------------- 1 | use super::*; 2 | use crate::server::write_endpoints::{save_context, create_node, delete_nodes, update_node, rename_node, move_nodes}; 3 | use crate::{ 4 | context::context::Context, 5 | elements::{node::DataNode, view_node::ViewNode}, 6 | graph_traits::graph_node::GraphNodes, 7 | server::{karta_service::KartaService, AppState}, 8 | utils::utils::KartaServiceTestContext, 9 | }; 10 | use axum::{ 11 | body::Body, 12 | http::{self, Request, StatusCode}, 13 | Router, 14 | }; 15 | use tower::ServiceExt; 16 | 17 | // This setup is proven to work from context_endpoints.rs 18 | pub fn setup_test_environment(test_name: &str) -> (Router, KartaServiceTestContext) { 19 | let test_ctx = KartaServiceTestContext::new(test_name); 20 | let app_state = AppState { 21 | service: test_ctx.service_arc.clone(), 22 | tx: tokio::sync::broadcast::channel(1).0, 23 | }; 24 | let router = Router::new() 25 | .route( 26 | "/ctx/{*id}", 27 | axum::routing::get(crate::server::context_endpoints::open_context_from_fs_path), 28 | ) 29 | .route("/api/ctx/{id}", axum::routing::put(save_context)) 30 | .route("/api/nodes", axum::routing::post(create_node).delete(delete_nodes)) 31 | .route("/api/nodes/{id}", axum::routing::put(update_node)) 32 | .route("/api/nodes/rename", axum::routing::post(rename_node)) 33 | .route("/api/nodes/move", axum::routing::post(move_nodes)) 34 | .with_state(app_state); 35 | (router, test_ctx) 36 | } 37 | 38 | // Helper for POST requests 39 | pub async fn execute_post_request(router: Router, uri: &str, body: String) -> http::Response { 40 | router 41 | .oneshot( 42 | Request::builder() 43 | .method(http::Method::POST) 44 | .uri(uri) 45 | .header(http::header::CONTENT_TYPE, "application/json") 46 | .body(Body::from(body)) 47 | .unwrap(), 48 | ) 49 | .await 50 | .unwrap() 51 | } 52 | 53 | // Helper for PUT requests 54 | pub async fn execute_put_request(router: Router, uri: &str, body: String) -> http::Response { 55 | router 56 | .oneshot( 57 | Request::builder() 58 | .method(http::Method::PUT) 59 | .uri(uri) 60 | .header(http::header::CONTENT_TYPE, "application/json") 61 | .body(Body::from(body)) 62 | .unwrap(), 63 | ) 64 | .await 65 | .unwrap() 66 | } 67 | 68 | // Helper for DELETE requests 69 | pub async fn execute_delete_request(router: Router, uri: &str, body: String) -> http::Response { 70 | router 71 | .oneshot( 72 | Request::builder() 73 | .method(http::Method::DELETE) 74 | .uri(uri) 75 | .header(http::header::CONTENT_TYPE, "application/json") 76 | .body(Body::from(body)) 77 | .unwrap(), 78 | ) 79 | .await 80 | .unwrap() 81 | } 82 | -------------------------------------------------------------------------------- /karta_server/src/elements/nodetype.rs: -------------------------------------------------------------------------------- 1 | use agdb::{DbError, DbValue}; 2 | 3 | use crate::elements::node; 4 | 5 | // Some of the structs and enums in this file are currently not used. 6 | // Determining a sound architecture for node types is difficult and 7 | // not urgent quite yet. 8 | 9 | pub const KARTA_VERSION: &str = "0.1.0"; 10 | 11 | 12 | // pub const ARCHETYPES: [&str; 5] = ["", "vault", "attributes", "nodetypes", "settings"]; 13 | pub const ARCHETYPES: [&str; 2] = ["", "vault"]; 14 | 15 | #[derive(Debug, Clone, Hash, Eq, PartialEq, serde::Serialize, serde::Deserialize)] 16 | pub struct NodeTypeId { 17 | type_path: String, 18 | version: String, 19 | } 20 | 21 | impl NodeTypeId { 22 | pub fn to_string(&self) -> String { 23 | format!("{}@{}", self.type_path, self.version) 24 | } 25 | 26 | pub fn new(type_path: &str) -> Self { 27 | Self { 28 | type_path: type_path.to_string(), 29 | version: KARTA_VERSION.to_string(), 30 | } 31 | } 32 | 33 | pub fn root_type() -> Self { 34 | Self { 35 | type_path: "core/root".to_string(), 36 | version: KARTA_VERSION.to_string(), 37 | } 38 | } 39 | 40 | pub fn archetype_type() -> Self { 41 | Self { 42 | type_path: "core/archetype".to_string(), 43 | version: KARTA_VERSION.to_string(), 44 | } 45 | } 46 | 47 | pub fn dir_type() -> Self { 48 | Self { 49 | type_path: "core/fs/dir".to_string(), 50 | version: KARTA_VERSION.to_string(), 51 | } 52 | } 53 | 54 | /// Generic file type. 55 | pub fn file_type() -> Self { 56 | Self { 57 | type_path: "core/fs/file".to_string(), 58 | version: KARTA_VERSION.to_string(), 59 | } 60 | } 61 | 62 | pub fn image_type() -> Self { 63 | Self { 64 | type_path: "core/image".to_string(), 65 | version: KARTA_VERSION.to_string(), 66 | } 67 | } 68 | 69 | pub fn virtual_generic() -> Self { 70 | Self { 71 | type_path: "core/virtual_generic".to_string(), 72 | version: KARTA_VERSION.to_string(), 73 | } 74 | } 75 | 76 | /// Check if this is the root node specifically 77 | pub fn is_root_node(&self) -> bool { 78 | self.type_path == "core/root" 79 | } 80 | 81 | } 82 | 83 | impl TryFrom for NodeTypeId { 84 | type Error = DbError; 85 | 86 | fn try_from(value: DbValue) -> Result { 87 | let type_str = value.string()?; 88 | let parts: Vec<&str> = type_str.split('@').collect(); 89 | if parts.len() != 2 { 90 | return Err(DbError::from("Invalid NodeTypeId format")); 91 | } 92 | 93 | Ok(NodeTypeId { 94 | type_path: parts[0].to_string(), 95 | version: parts[1].to_string(), 96 | }) 97 | } 98 | } 99 | 100 | impl From for DbValue { 101 | fn from(type_id: NodeTypeId) -> Self { 102 | type_id.to_string().into() 103 | } 104 | } -------------------------------------------------------------------------------- /karta_svelte/static/tutorial-sections.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "title": "Welcome to Karta", 4 | "content": "Welcome to Karta! This is your personal graph-based workspace for organizing files, ideas, projects, and creative work.\n\n**Everything stays on your computer** - Karta runs locally with its own server, so your data is private and secure.\n\nUse the mouse wheel or pinch to **zoom**, and drag with the middle mouse button or two fingers on touchpad to **pan** around the canvas." 5 | }, 6 | { 7 | "title": "The Overview", 8 | "content": "Karta can be thought of as a canvas file browser. It has access to the folders and files inside any vault you create, and turns them into **Nodes**.\n\nYou can move, scale, and connect Nodes together as you see fit. Connected Nodes will always appear together, so you can navigate your files by the associations you define, and not just their location in your file system.\n\nIn addition to files and folders, you can create **Virtual Nodes**. For now this is mostly for text notes, which you can create anywhere to further organise thoughts and ideas." 9 | }, 10 | { 11 | "title": "Understanding Contexts", 12 | "content": "In Karta, you're always viewing the graph from the perspective some node - this is called a **Context**, and that node is called that Context's **Focal Node**. You'll recognise it by its thick bright outline.\n\nEvery node has a Context, which contains all the nodes that are connected to it. The same Nodes can have different arrangements in different Contexts. You can think of it like having the same files in multiple places, but with no duplication.\n\nYou can **Enter** a node's context by right-clicking and selecting \"Enter Context\", which makes that node the new focal point and shows its related nodes." 13 | }, 14 | { 15 | "title": "Creating Your First Nodes", 16 | "content": "Start building your graph by creating Nodes in your current Context:\n\n**Right-click** on empty space to open the context menu and create:\n- **Text Nodes** for notes, ideas, and written content\n- **Generic Nodes** for simple labeled items\n- **Directory Nodes** which create actual folders in that location\n- **File Nodes** create empty files in that location\n\nEach Node's content is unique, but you can display the same Node in multiple Contexts with different visual styles. You can use the search to bring in any Node into the current Context." 17 | }, 18 | { 19 | "title": "Connecting Ideas with Edges", 20 | "content": "**Edges** (the lines between nodes) explicitly connect related ideas.\n\n**Create edges** by:\n1. Selecting one or more Nodes\n2. Dragging from the handle at the bottom\n\nEdges exist in the content layer, so they're visible across all Contexts. Use them to say \"these ideas belong together\"." 21 | }, 22 | { 23 | "title": "Next Steps", 24 | "content": "Those are the basics! Karta is still extremely barebones, but what's there should convey the concept and be somewhat useful. It has been in prototyping and concepting stages for a long time (too long), and now it's time to hear what you think about it!\n\nQuestions, feedback, and feature ideas can all be directed to karta@teodosin.com. All thoughts are greatly appreciated!" 25 | } 26 | ] 27 | 28 | -------------------------------------------------------------------------------- /karta_svelte/static/tutorial-sections.md: -------------------------------------------------------------------------------- 1 | # Tutorial Sections 2 | 3 | ## Section 1: Welcome to Karta 4 | 5 | Welcome to Karta! This is your personal graph-based workspace for organizing files, ideas, projects, and creative work. 6 | 7 | **Everything stays on your computer** - Karta runs locally with its own server, so your data is private and secure. 8 | 9 | Use the mouse wheel or pinch to **zoom**, and drag with the middle mouse button or two fingers on touchpad to **pan** around the canvas. 10 | 11 | --- 12 | 13 | ## Section 2: The Overview 14 | 15 | Karta can be thought of as a canvas file browser. It has access to the folders and files inside any vault you create, and turns them into **Nodes**. 16 | 17 | You can move, scale, and connect Nodes together as you see fit. Connected Nodes will always appear together, so you can navigate your files by the associations you define, and not just their location in your file system. 18 | 19 | In addition to files and folders, you can create **Virtual Nodes**. For now this is mostly for text notes, which you can create anywhere to further organise thoughts and ideas. 20 | 21 | --- 22 | 23 | ## Section 3: Understanding Contexts 24 | 25 | In Karta, you're always viewing the graph from the perspective some node - this is called a **Context**, and that node is called that Context's **Focal Node**. You'll recognise it by its thick bright outline. 26 | 27 | Every node has a Context, which contains all the nodes that are connected to it. The same Nodes can have different arrangements in different Contexts. You can think of it like having the same files in multiple places, but with no duplication. 28 | 29 | You can **Enter** a node's context by right-clicking and selecting "Enter Context", which makes that node the new focal point and shows its related nodes. 30 | 31 | --- 32 | 33 | ## Section 4: Creating Your First Nodes 34 | 35 | Start building your graph by creating Nodes in your current Context: 36 | 37 | **Right-click** on empty space to open the context menu and create: 38 | - **Text Nodes** for notes, ideas, and written content 39 | - **Generic Nodes** for simple labeled items 40 | - **Directory Nodes** which create actual folders in that location 41 | - **File Nodes** create empty files in that location 42 | 43 | Each Node's content is unique, but you can display the same Node in multiple Contexts with different visual styles. You can use the search to bring in any Node into the current Context. 44 | 45 | --- 46 | 47 | ## Section 5: Connecting Ideas with Edges 48 | 49 | **Edges** (the lines between nodes) explicitly connect related ideas. 50 | 51 | **Create edges** by: 52 | 1. Selecting one or more Nodes 53 | 2. Dragging from the handle at the bottom 54 | 55 | Edges exist in the content layer, so they're visible across all Contexts. Use them to say "these ideas belong together". 56 | 57 | --- 58 | 59 | ## Section 6: Next Steps 60 | 61 | Those are the basics! Karta is still extremely barebones, but what's there should convey the concept and be somewhat useful. It has been in prototyping and concepting stages for a long time (too long), and now it's time to hear what you think about it! 62 | 63 | Questions, feedback, and feature ideas can all be directed to karta@teodosin.com. All thoughts are greatly appreciated! 64 | 65 | 66 | -------------------------------------------------------------------------------- /karta_svelte/src/lib/node_types/FileNode.svelte: -------------------------------------------------------------------------------- 1 | 7 | 35 | 36 | 65 | 66 |
73 |
74 | 75 | {fileExtension} 76 | 77 |
78 |
79 | 80 | -------------------------------------------------------------------------------- /karta_server/README.md: -------------------------------------------------------------------------------- 1 | # Karta Server 2 | 3 | The Rust backend for Karta, handling graph database operations and file system integration. 4 | 5 | ## What it does 6 | 7 | This server provides a REST API for managing the graph database that powers Karta. It handles: 8 | 9 | - **Graph operations** - Creating, updating, and querying nodes and edges 10 | - **File system integration** - Syncing filesystem changes with graph data 11 | - **Asset management** - Handling image and file assets 12 | - **Context management** - Managing different views/contexts of the same data 13 | 14 | The server uses [AGDB](https://github.com/agnesoft/agdb) as the graph database backend, which provides efficient graph storage and querying. 15 | 16 | ## Architecture 17 | 18 | **Core components:** 19 | - `src/server/` - REST API endpoints and routing 20 | - `src/graph_agdb/` - Database layer and AGDB integration 21 | - `src/fs_reader/` - File system integration and watching 22 | - `src/context/` - Context management (RON file handling) 23 | - `src/elements/` - Core data structures and graph elements 24 | 25 | **Key concepts:** 26 | - **DataNodes** - Represent files/folders in the filesystem or virtual nodes 27 | - **Edges** - Connections between nodes with typed relationships 28 | - **Contexts** - Different spatial arrangements of the same nodes 29 | - **ViewNodes** - Position and visual state of nodes within contexts 30 | 31 | ## API Overview 32 | 33 | **Core endpoints:** 34 | - `GET /` - Vault information 35 | - `GET /api/asset/{path}` - File assets 36 | - `GET /api/paths` - File system paths 37 | - `GET /api/search` - Search nodes 38 | - `GET /api/contexts` - Available contexts 39 | 40 | **Node operations:** 41 | - `POST /api/nodes` - Create new node 42 | - `DELETE /api/nodes` - Delete nodes 43 | - `PUT /api/nodes/{id}` - Update node 44 | - `GET /api/nodes/{id}` - Get node by ID 45 | - `GET /api/nodes/by-path/{path}` - Get node by file path 46 | 47 | **Edge operations:** 48 | - `POST /api/edges` - Create edges 49 | - `DELETE /api/edges` - Delete edges 50 | - `PATCH /api/edges` - Reconnect edge 51 | 52 | **Context operations:** 53 | - `PUT /api/ctx/{id}` - Save context 54 | - `GET /ctx/{id}` - Open context from filesystem path 55 | 56 | ## Development 57 | 58 | **Prerequisites:** 59 | - Rust (latest stable) 60 | 61 | **Running:** 62 | ```bash 63 | cargo run 64 | ``` 65 | 66 | The server starts on `http://localhost:7370` by default. It will prompt you to type in a path for your vault. You can use tab to autocomplete, double click to show all available folders. Once you've created some vaults, you can quickly initialise them without typing in the whole path each time. 67 | 68 | **Configuration:** 69 | Currently uses hardcoded paths and settings. Configuration system is planned but not implemented yet. 70 | 71 | ## Current limitations 72 | 73 | - No authentication/authorization 74 | - Hardcoded database location 75 | - Limited error handling in some areas 76 | - File watching could be more robust 77 | - No configuration system yet 78 | 79 | This is development-focused right now - it assumes a single local user and doesn't handle multi-user scenarios or security concerns. 80 | 81 | ## Integration 82 | 83 | The server is designed to work with the Karta Svelte client, but could potentially be used by other applications that need graph-based file organization. 84 | 85 | Eventually this will be automatically initialised as a separate process by the Tauri app instead of having to be run individually. 86 | -------------------------------------------------------------------------------- /karta_svelte/src/lib/node_types/RootNode.svelte: -------------------------------------------------------------------------------- 1 | 7 | 39 | 40 | 62 | 63 |
71 | 72 | 73 |
74 | 75 | -------------------------------------------------------------------------------- /karta_server/src/server/edge_endpoints.rs: -------------------------------------------------------------------------------- 1 | use axum::{extract::State, http::StatusCode, response::Json}; 2 | use serde::{Deserialize, Serialize}; 3 | use std::{collections::HashMap, str::FromStr}; 4 | use uuid::Uuid; 5 | 6 | use crate::{prelude::Edge, server::{karta_service::KartaService, AppState}}; 7 | 8 | #[derive(Serialize, Deserialize, Debug, Clone)] 9 | pub struct CreateEdgePayload { 10 | pub id: String, 11 | pub source: String, 12 | pub target: String, 13 | pub attributes: HashMap, 14 | pub source_path: String, 15 | pub target_path: String, 16 | } 17 | 18 | #[axum::debug_handler] 19 | pub async fn create_edges( 20 | State(state): State, 21 | Json(payload): Json>, 22 | ) -> Result>, StatusCode> { 23 | 24 | dbg!(&payload); 25 | let mut service = state.service.write().unwrap(); 26 | 27 | match service.create_edges(payload) { 28 | Ok(created_edges) => Ok(Json(created_edges)), 29 | Err(_) => Err(StatusCode::INTERNAL_SERVER_ERROR), 30 | } 31 | } 32 | 33 | #[derive(Serialize, Deserialize, Debug)] 34 | pub struct ReconnectEdgePayload { 35 | pub old_from: Uuid, 36 | pub old_to: Uuid, 37 | pub new_from: Uuid, 38 | pub new_to: Uuid, 39 | pub new_from_path: String, 40 | pub new_to_path: String, 41 | } 42 | 43 | #[axum::debug_handler] 44 | pub async fn reconnect_edge( 45 | State(state): State, 46 | Json(payload): Json, 47 | ) -> Result, StatusCode> { 48 | let mut service = state.service.write().unwrap(); 49 | 50 | println!("[RECONNECT_EDGE] Attempting edge reconnection:"); 51 | println!("[RECONNECT_EDGE] Old: {} -> {}", payload.old_from, payload.old_to); 52 | println!("[RECONNECT_EDGE] New: {} -> {}", payload.new_from, payload.new_to); 53 | println!("[RECONNECT_EDGE] Paths: {} -> {}", payload.new_from_path, payload.new_to_path); 54 | 55 | match service.reconnect_edge(&payload.old_from, &payload.old_to, &payload.new_from, &payload.new_to, &payload.new_from_path, &payload.new_to_path) { 56 | Ok(edge) => { 57 | println!("[RECONNECT_EDGE] Success: {:?}", edge); 58 | Ok(Json(edge)) 59 | }, 60 | Err(e) => { 61 | println!("[RECONNECT_EDGE] Error: {}", e); 62 | if e.to_string() 63 | .contains("Reconnection of 'contains' edges is not allowed") 64 | { 65 | println!("[RECONNECT_EDGE] Returning 400 - Contains edge reconnection not allowed"); 66 | Err(StatusCode::BAD_REQUEST) 67 | } else { 68 | println!("[RECONNECT_EDGE] Returning 500 - Internal server error"); 69 | Err(StatusCode::INTERNAL_SERVER_ERROR) 70 | } 71 | } 72 | } 73 | } 74 | 75 | #[derive(Serialize, Deserialize, Debug)] 76 | pub struct DeleteEdgePayload { 77 | pub source: Uuid, 78 | pub target: Uuid, 79 | } 80 | 81 | #[axum::debug_handler] 82 | pub async fn delete_edges( 83 | State(state): State, 84 | Json(payload): Json>, 85 | ) -> Result { 86 | let mut service = state.service.write().unwrap(); 87 | 88 | match service.delete_edges(payload) { 89 | Ok(_) => Ok(StatusCode::NO_CONTENT), 90 | Err(e) => { 91 | if e.to_string().contains("Deletion of 'contains' edges is not allowed.") { 92 | Err(StatusCode::BAD_REQUEST) 93 | } else { 94 | Err(StatusCode::INTERNAL_SERVER_ERROR) 95 | } 96 | } 97 | } 98 | } -------------------------------------------------------------------------------- /karta_svelte/README.md: -------------------------------------------------------------------------------- 1 | # Karta Client 2 | 3 | The desktop application frontend for Karta, built with SvelteKit and Tauri. 4 | 5 | ## What it does 6 | 7 | This is the user interface for Karta - the part you actually interact with. It provides: 8 | 9 | - **Graph visualization** - Interactive canvas for viewing and manipulating nodes 10 | - **Context switching** - Navigate between different views of your data 11 | - **File integration** - Drag and drop files, create nodes from filesystem 12 | - **Node editing** - Create, edit, and connect different types of nodes 13 | - **Server communication** - Syncs with the Rust backend 14 | 15 | ## Architecture 16 | 17 | **Key directories:** 18 | - `src/lib/components/` - Reusable UI components 19 | - `src/lib/karta/` - Core stores and state management 20 | - `src/lib/node_types/` - Different node type implementations 21 | - `src/lib/interaction/` - Mouse/keyboard interaction handling 22 | - `src/lib/util/` - Utilities and adapters 23 | - `src-tauri/` - Tauri desktop app configuration 24 | 25 | **State management:** 26 | - Uses Svelte stores for reactive state 27 | - Separate stores for nodes, edges, contexts, viewport, etc. 28 | - Persistence layer that connects to the Rust backend server 29 | 30 | ## Key concepts 31 | 32 | **Nodes:** Represent files, folders, or virtual content 33 | **Edges:** Connections between nodes with different relationship types 34 | **Contexts:** Different spatial arrangements of the same nodes 35 | **Viewport:** Pan/zoom state and visual transforms 36 | 37 | The UI is built around the idea of contextual navigation - you're always viewing the graph from some focal point, and nodes are positioned relative to that context. 38 | 39 | ## Development 40 | 41 | **Prerequisites:** 42 | - Node.js 18+ 43 | - Rust (for Tauri) 44 | 45 | **Running in development:** 46 | ```bash 47 | # Install dependencies 48 | npm install 49 | 50 | # Start development server (web mode) 51 | npm run dev 52 | 53 | # Start Tauri development (desktop mode) 54 | # Note: Tauri npm scripts not set up yet, use cargo directly: 55 | cd src-tauri 56 | cargo tauri dev 57 | ``` 58 | 59 | **Building:** 60 | ```bash 61 | # Build web version 62 | npm run build 63 | 64 | # Build Tauri desktop app 65 | cd src-tauri 66 | cargo tauri build 67 | ``` 68 | 69 | ## Current state 70 | 71 | **What works well:** 72 | - Basic graph visualization and interaction 73 | - Context switching with smooth transitions 74 | - File node creation and editing 75 | - Server integration with graph database 76 | 77 | **Known issues:** 78 | - Performance with large numbers of nodes/images 79 | - Some UI elements need polish 80 | - Error handling could be more robust 81 | - Mobile/touch interaction is limited 82 | 83 | ## Configuration 84 | 85 | The app connects to the Rust backend server running on localhost:7370. There's legacy IndexedDB adapter code that will be removed in a future version. 86 | 87 | **Current setup:** 88 | - Uses ServerAdapter to connect to the backend 89 | - Requires the karta_server to be running separately 90 | - Configuration options are minimal and mostly hardcoded 91 | 92 | ## Tauri integration 93 | 94 | This is designed as a desktop application using Tauri. The web version works for development, but the intended experience is as a native desktop app that will eventually bundle the server component. 95 | 96 | Current Tauri features: 97 | - Native file system access 98 | - Desktop notifications 99 | - Window management 100 | 101 | Planned Tauri features: 102 | - Bundled server process 103 | - Better file watching 104 | - System tray integration 105 | -------------------------------------------------------------------------------- /karta_svelte/src/lib/components/ContextMenu.svelte: -------------------------------------------------------------------------------- 1 | 67 | 68 | {#if position} 69 | 98 | {/if} 99 | 100 | -------------------------------------------------------------------------------- /karta_svelte/src/lib/components/Toolbar.svelte: -------------------------------------------------------------------------------- 1 | 20 | 21 | 22 |
23 | 24 | {#each [ 25 | { tool: 'move', label: 'Move Tool', icon: MousePointer2, instance: MoveTool }, 26 | { tool: 'connect', label: 'Connect Tool', icon: Workflow, instance: ConnectTool }, 27 | { tool: 'context', label: 'Context Tool', icon: Focus, instance: ContextTool } // Removed trailing comma 28 | ] as item (item.tool)} 29 | {#if item.tool !== 'connect'} 30 |
31 | 49 |
50 | {/if} 51 | {/each} 52 | 53 | 54 |
55 | 56 | 57 | 58 | 59 | 60 |
61 | 62 | 63 |
64 | 78 |
79 | 80 | 81 |
82 | -------------------------------------------------------------------------------- /karta_svelte/src/lib/components/FilterMenuDropdown.svelte: -------------------------------------------------------------------------------- 1 | 54 | 55 | 56 |
61 |
62 |

Edge Visibility

63 | 64 | 65 |
66 |
67 | Folder Connections 68 |
69 |
70 | {#each visibilityOptions as option} 71 | 81 | {/each} 82 |
83 |
84 | 85 | 86 |
87 | 88 | 89 |
90 |
91 | Normal Edges 92 |
93 |
94 | {#each visibilityOptions as option} 95 | 105 | {/each} 106 |
107 |
108 |
109 |
110 | -------------------------------------------------------------------------------- /karta_svelte/src/lib/components/CreateNodeMenu.svelte: -------------------------------------------------------------------------------- 1 | 86 | 87 | 88 | 89 | {#if position} 90 | 91 | 115 | {/if} -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Karta 2 | 3 | ![Karta Logo](docs/karta_logo.png) 4 | 5 | A node-based file organization tool for creative projects. 6 | 7 | > [!IMPORTANT] 8 | > This project is in active development and not ready for production use. It's functional but still rough around the edges. The current focus is getting the core features stable before expanding functionality. 9 | 10 | ## What is this? 11 | 12 | Karta turns your file system into a spatial graph. Instead of just folders and files, you get nodes and connections. You can arrange your project files on a canvas, create arbitrary links between them, and view your work from different "contexts" - basically different perspectives that show the same files arranged differently. 13 | 14 | It's designed for complex creative projects where you need to see relationships between many different files and ideas. Think game development, world-building, writing projects with lots of interconnected parts. 15 | 16 | ## Current state 17 | 18 | **What works:** 19 | - Basic node-based file visualization 20 | - Creating connections between files 21 | - Context switching (viewing the same network from different focal points) 22 | - Server integration with graph database 23 | - File system integration 24 | 25 | **What doesn't work yet:** 26 | - Packaged desktop app (currently requires running server and client separately) 27 | - Export/sharing functionality 28 | - Custom node types 29 | - Smooth performance with large vaults and images 30 | 31 | ## Licensing (work in progress) 32 | 33 | The licensing situation is being sorted out to enable both open source development and commercial use of created content. Currently everything is private while we figure this out, but the plan is: 34 | 35 | - **Editor**: GPL - keeps the editing tools open source 36 | - **Runtime**: MIT - allows commercial use of exported networks 37 | 38 | Both will be part of this repository but serve different purposes. If you're interested in contributing or using this, please reach out so we can discuss the licensing before you invest time. 39 | 40 | ## Architecture 41 | 42 | This is a monorepo with two main parts: 43 | 44 | **`karta_server/`** - Rust backend 45 | - Uses [agdb](https://github.com/agnesoft/agdb) graph database for storage 46 | - Handles the core graph operations and file system integration 47 | - See [karta_server/README.md](./karta_server/README.md) 48 | 49 | **`karta_svelte/`** - Desktop application (Tauri + SvelteKit) 50 | - SvelteKit frontend with Tauri wrapper for desktop functionality 51 | - Currently works with the separate server, but the plan is to bundle the server into the Tauri app 52 | - For now you need to run both server and client separately during development 53 | - See [karta_svelte/README.md](./karta_svelte/README.md) 54 | 55 | The long-term plan includes splitting functionality into editor and runtime components to enable embedding Karta networks in other applications, while keeping both in this repository. 56 | 57 | ## Development setup 58 | 59 | Prerequisites: Rust, Node.js 18+, and your package manager of choice. 60 | 61 | ```bash 62 | # Server (in one terminal) 63 | cd karta_server 64 | cargo run 65 | 66 | # Client (in another terminal) 67 | cd karta_svelte 68 | npm install 69 | npm run dev 70 | ``` 71 | 72 | See the individual READMEs for more detailed setup instructions. 73 | 74 | ## Contributing 75 | 76 | It's too early to ask for contributions, but feedback and discussion are very welcome. The project is still finding its direction and having conversations about what this should become is valuable. 77 | 78 | If you want to try it out, expect rough edges and incomplete features. But if something clicks for you or you see potential, I'd love to hear about it. You can send your questions, feedback and feature ideas directly to karta@teodosin.com. 79 | 80 | ## Background 81 | 82 | The motivation is pretty simple: creative projects are interconnected webs of ideas and files, but our tools treat them as isolated pieces in folder hierarchies. This makes it hard to see the relationships and patterns that actually matter for the work. 83 | 84 | Karta tries to make those connections visible and interactive. Whether that's actually useful remains to be seen, but it's worth exploring. 85 | 86 | --- 87 | 88 | *Built for creative work that doesn't fit neatly into folders.* 89 | 90 | 91 | -------------------------------------------------------------------------------- /karta_server/src/server/settings.rs: -------------------------------------------------------------------------------- 1 | use axum::{extract::State, http::StatusCode, response::{IntoResponse, Json}}; 2 | use serde::{Deserialize, Serialize}; 3 | use std::fs; 4 | use std::path::PathBuf; 5 | 6 | use crate::prelude::NodePath; 7 | 8 | use super::AppState; 9 | 10 | #[derive(Serialize, Deserialize, Debug, Clone)] 11 | #[serde(rename_all = "camelCase")] 12 | pub struct ColorTheme { 13 | pub viewport_bg: String, 14 | pub panel_bg: String, 15 | pub focal_hl: String, 16 | pub panel_hl: String, 17 | pub text_color: String, 18 | pub contrast_color: String, 19 | pub connection_color: String, 20 | } 21 | 22 | impl Default for ColorTheme { 23 | fn default() -> Self { 24 | ColorTheme { 25 | viewport_bg: "#000000".to_string(), 26 | panel_bg: "#431d1f".to_string(), 27 | focal_hl: "#f4902dff".to_string(), 28 | panel_hl: "#741f2fff".to_string(), 29 | text_color: "#f0f0f0".to_string(), 30 | contrast_color: "#60a5fa".to_string(), 31 | connection_color: "#505050".to_string(), 32 | } 33 | } 34 | } 35 | 36 | #[derive(Serialize, Deserialize, Debug, Clone)] 37 | #[serde(rename_all = "camelCase")] 38 | pub struct EdgeFilterSettings { 39 | pub contains_edges: String, 40 | pub normal_edges: String, 41 | } 42 | 43 | impl Default for EdgeFilterSettings { 44 | fn default() -> Self { 45 | EdgeFilterSettings { 46 | contains_edges: "always".to_string(), 47 | normal_edges: "always".to_string(), 48 | } 49 | } 50 | } 51 | 52 | #[derive(Serialize, Deserialize, Debug, Clone)] 53 | #[serde(rename_all = "camelCase")] 54 | pub struct KartaSettings { 55 | pub version: f32, 56 | pub save_last_viewed_context: bool, 57 | pub last_viewed_context_path: Option, 58 | pub vault_path: Option, 59 | pub color_theme: ColorTheme, 60 | pub edge_filters: EdgeFilterSettings, 61 | } 62 | 63 | impl Default for KartaSettings { 64 | fn default() -> Self { 65 | KartaSettings { 66 | version: 0.1, 67 | save_last_viewed_context: true, 68 | last_viewed_context_path: None, 69 | vault_path: None, 70 | color_theme: ColorTheme::default(), 71 | edge_filters: EdgeFilterSettings::default(), 72 | } 73 | } 74 | } 75 | 76 | fn get_settings_path(vault_path: &str) -> PathBuf { 77 | let mut path = PathBuf::from(vault_path); 78 | path.push(".karta"); 79 | path.push("settings.json"); 80 | path 81 | } 82 | 83 | pub fn load_settings(vault_path: &str) -> KartaSettings { 84 | let settings_path = get_settings_path(vault_path); 85 | if settings_path.exists() { 86 | let content = fs::read_to_string(settings_path).unwrap_or_default(); 87 | serde_json::from_str(&content).unwrap_or_else(|_| KartaSettings::default()) 88 | } else { 89 | KartaSettings::default() 90 | } 91 | } 92 | 93 | pub fn save_settings(vault_path: &str, settings: &KartaSettings) -> Result<(), std::io::Error> { 94 | let settings_path = get_settings_path(vault_path); 95 | let parent_dir = settings_path.parent().unwrap(); 96 | fs::create_dir_all(parent_dir)?; 97 | let content = serde_json::to_string_pretty(settings).unwrap(); 98 | fs::write(settings_path, content) 99 | } 100 | 101 | pub async fn get_settings_handler( 102 | State(state): State, 103 | ) -> Json { 104 | let service = state.service.read().unwrap(); 105 | let vault_path = service.vault_fs_path().to_str().unwrap(); 106 | Json(load_settings(vault_path)) 107 | } 108 | 109 | pub async fn update_settings_handler( 110 | State(state): State, 111 | Json(payload): Json, 112 | ) -> impl IntoResponse { 113 | let service = state.service.read().unwrap(); 114 | let vault_path = service.vault_fs_path().to_str().unwrap(); 115 | match save_settings(vault_path, &payload) { 116 | Ok(_) => (StatusCode::OK, Json(payload)).into_response(), 117 | Err(e) => ( 118 | StatusCode::INTERNAL_SERVER_ERROR, 119 | format!("Failed to save settings: {}", e), 120 | ) 121 | .into_response(), 122 | } 123 | } -------------------------------------------------------------------------------- /karta_server/src/graph_agdb/graph_core.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | error::Error, 3 | path::{self, PathBuf}, 4 | }; 5 | 6 | use agdb::QueryBuilder; 7 | 8 | use crate::{ 9 | elements::{node::ROOT_UUID, nodetype::ARCHETYPES}, 10 | graph_traits::{self, graph_core::GraphCore, graph_node::GraphNodes}, 11 | prelude::{DataNode, GraphEdge, NodePath, NodeTypeId, StoragePath}, 12 | }; 13 | 14 | use super::GraphAgdb; 15 | 16 | /// Implementation block for the Graph struct itself. 17 | /// Includes constructors and utility functions. 18 | impl GraphCore for GraphAgdb { 19 | fn storage_path(&self) -> PathBuf { 20 | self.storage_path.clone() 21 | } 22 | 23 | fn vault_dirpath(&self) -> PathBuf { 24 | let path = self.vault_fs_path.clone(); 25 | // println!("root_path: {:?}", path); 26 | path 27 | } 28 | 29 | fn root_nodepath(&self) -> NodePath { 30 | NodePath::root() 31 | } 32 | 33 | /// Gets the name of the root directory without the full path 34 | fn root_name(&self) -> String { 35 | self.vault_fs_path 36 | .file_name() 37 | .unwrap() 38 | .to_str() 39 | .unwrap() 40 | .to_string() 41 | } 42 | 43 | /// Constructor. Panics if the db cannot be created. 44 | /// 45 | /// Takes the desired root directory of the graph as a parameter and the name for the db. 46 | /// The name of the root directory will become the vault of the graph, 47 | /// as first child of the root node. 48 | /// 49 | /// Creates the db at the storage_path, or initialises the db if it already exists there. 50 | /// 51 | /// TODO: Add error handling. 52 | fn new(name: &str, vault_fs_path: PathBuf, storage_dir: PathBuf) -> Self { 53 | if !storage_dir.exists() { 54 | std::fs::create_dir_all(&storage_dir).expect("Failed to create storage path"); 55 | } 56 | let db_path = storage_dir.join(format!("{}.agdb", name)); 57 | 58 | // Check if the database already exists 59 | let open_existing = db_path.exists(); 60 | 61 | let db = agdb::Db::new(db_path.to_str().unwrap()).expect("Failed to create new db"); 62 | 63 | let mut giraphe = GraphAgdb { 64 | name: name.to_string(), 65 | db, 66 | vault_fs_path: vault_fs_path.into(), 67 | storage_path: storage_dir, 68 | }; 69 | 70 | // Indexes for faster lookup based on attributes 71 | giraphe.db.exec_mut(QueryBuilder::insert().index("uuid").query()); 72 | giraphe.db.exec_mut(QueryBuilder::insert().index("ntype").query()); 73 | giraphe.db.exec_mut(QueryBuilder::insert().index("path").query()); 74 | 75 | if !open_existing { 76 | let archetypes = ARCHETYPES; 77 | 78 | archetypes.iter().for_each(|atype| { 79 | let atype_path = NodePath::atype(*atype); 80 | let ntype = if atype_path == NodePath::root() { 81 | NodeTypeId::root_type() 82 | } else if atype_path == NodePath::vault() { 83 | NodeTypeId::dir_type() 84 | } else { 85 | NodeTypeId::archetype_type() 86 | }; 87 | 88 | let node = DataNode::new(&atype_path, ntype); 89 | let node_uuid = node.uuid(); 90 | 91 | giraphe.insert_nodes(vec![node]); 92 | 93 | 94 | if atype_path != NodePath::root() { 95 | let root_to_atype_edge = 96 | crate::prelude::Edge::new_cont(ROOT_UUID, node_uuid); 97 | 98 | giraphe.insert_edges(vec![root_to_atype_edge]); 99 | } 100 | }); 101 | } 102 | 103 | return giraphe; 104 | } 105 | 106 | fn get_all_aliases(&self) -> Vec { 107 | let all = self.db().exec(&QueryBuilder::select().aliases().query()); 108 | match all { 109 | Ok(aliases) => { 110 | let all: Vec = aliases 111 | .elements 112 | .iter() 113 | .map(|alias| alias.values[0].value.to_string()) 114 | .collect(); 115 | 116 | all 117 | } 118 | Err(err) => { 119 | // println!("Error: {}", err); 120 | vec![] 121 | } 122 | } 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /karta_server/src/fs_reader/mod.rs: -------------------------------------------------------------------------------- 1 | use std::{error::Error, ops::Index, path::{Path, PathBuf}}; 2 | 3 | use crate::prelude::{DataNode, NodePath, NodeTypeId}; 4 | 5 | fn get_node_type_from_path(path: &Path) -> NodeTypeId { 6 | if path.is_dir() { 7 | return NodeTypeId::dir_type(); 8 | } 9 | 10 | if path.is_file() { 11 | return match path.extension().and_then(|s| s.to_str()) { 12 | Some("png") | Some("jpg") | Some("jpeg") | Some("gif") => NodeTypeId::image_type(), 13 | _ => NodeTypeId::file_type(), 14 | }; 15 | } 16 | 17 | // Default fallback, though in practice the calling logic should prevent this. 18 | NodeTypeId::file_type() 19 | } 20 | 21 | pub fn destructure_single_path( 22 | vault_root_path: &PathBuf, 23 | path_to_destructure: &NodePath, 24 | ) -> Result> { 25 | let path_to_destructure = path_to_destructure.full(vault_root_path); 26 | if !path_to_destructure.exists() { 27 | return Err(format!("Path does not exist: {:?}", path_to_destructure).into()); 28 | } 29 | 30 | let node_path = NodePath::from_dir_path(vault_root_path, &path_to_destructure); 31 | let ntype = get_node_type_from_path(&path_to_destructure); 32 | let node = DataNode::new(&node_path, ntype); 33 | Ok(node) 34 | } 35 | 36 | pub fn destructure_file_path( 37 | vault_root_path: &PathBuf, // New parameter: the root of the Karta vault 38 | path_to_destructure: &PathBuf, // This is the absolute path of the item to destructure 39 | include_self: bool, 40 | ) -> Result, Box> { 41 | if !path_to_destructure.exists() { 42 | return Err(format!("Path to destructure does not exist: {:?}", path_to_destructure).into()); 43 | } 44 | let mut nodes: Vec = Vec::new(); 45 | 46 | if path_to_destructure.is_file() && include_self { 47 | // Create NodePath relative to vault_root_path 48 | let node_path = NodePath::from_dir_path(vault_root_path, path_to_destructure); 49 | let ntype = get_node_type_from_path(path_to_destructure); 50 | let node = DataNode::new(&node_path, ntype); 51 | nodes.push(node); 52 | } 53 | 54 | if path_to_destructure.is_dir() { 55 | // If include_self is true for a directory, we might want to add the directory itself. 56 | // However, the current KartaService logic creates the focal directory node separately. 57 | // This function, as per its usage in KartaService, primarily returns children for a dir. 58 | // If include_self for a dir meant to include the dir itself, that logic would be here. 59 | // For now, it only lists children. 60 | 61 | let dir_entries = std::fs::read_dir(path_to_destructure)?; 62 | 63 | for entry in dir_entries { 64 | let entry = entry?; 65 | let absolute_entry_path = entry.path(); // This is an absolute path 66 | 67 | // Ignore .karta directory or files 68 | if absolute_entry_path.file_name().map_or(false, |name| name == ".karta") { 69 | continue; 70 | } 71 | 72 | // Create NodePath relative to vault_root_path 73 | let node_path = NodePath::from_dir_path(vault_root_path, &absolute_entry_path); 74 | let ntype: NodeTypeId; 75 | 76 | if absolute_entry_path.is_dir() { 77 | ntype = NodeTypeId::dir_type(); 78 | } else if absolute_entry_path.is_file() { 79 | ntype = get_node_type_from_path(&absolute_entry_path); 80 | } else { continue; } 81 | 82 | let node = DataNode::new(&node_path, ntype); 83 | nodes.push(node); 84 | } 85 | } 86 | Ok(nodes) 87 | } 88 | 89 | pub fn get_all_paths(vault_root_path: &PathBuf) -> Result, Box> { 90 | let mut paths = Vec::new(); 91 | let mut walker = walkdir::WalkDir::new(vault_root_path).into_iter(); 92 | 93 | while let Some(entry) = walker.next() { 94 | let entry = entry?; 95 | let path = entry.path(); 96 | 97 | if path.file_name().map_or(false, |name| name == ".karta") { 98 | walker.skip_current_dir(); 99 | continue; 100 | } 101 | 102 | if path.is_dir() || path.is_file() { 103 | let node_path = NodePath::from_dir_path(vault_root_path, &path.to_path_buf()); 104 | paths.push(node_path.alias().to_string()); 105 | } 106 | } 107 | 108 | Ok(paths) 109 | } 110 | -------------------------------------------------------------------------------- /website/architecture.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Karta Architecture History" 3 | date: 2025-08-03 4 | status: draft 5 | description: "Historical architecture document from Karta's early development - preserved for context" 6 | tags: ["architecture", "history", "development"] 7 | --- 8 | 9 | 18 | 19 | # Karta Architecture (Historical) 20 | 21 | This document outlines some of the architectural decisions made during Karta's early development as well as open questions that were being considered at the time. It is not exhaustive and much of it is now outdated. 22 | 23 | ### Background 24 | 25 | #### Previous prototype 26 | The first prototype of Karta was made with the Godot engine and its scripting language GDscript. Godot has a lot of features that made it simple to get started and get something working quickly. It worked well, but with its shaky foundation, the prototype quickly became unwieldy and started breaking at odd places, and I didn't have the knowledge or the tools to debug it. Spaghetti. But the experience was encouraging, so I started to look at my options for continuing the experiment. 27 | 28 | #### Rust 29 | I landed on Rust after weighing it against C++ and deciding that I didn't want to have to deal with manual memory management and runtime errors. I want clear feedback up front on whether my code will work or not. Godot's deep inheritance trees also left a sour impression on me, so I was further lulled in to Rust by its composition features. 30 | 31 | #### Bevy 32 | From Godot I was left with an optimistic view of using game engines for native applications. I tried Bevy, using it to load a folder of files as circles in a 2D space. The ECS system clicked, and I imagined it would be a very intuitive and ergonomic way to manage nodes and edges in Karta. The ECS has the downside of handwaving away some of Rusts safety guarantees, but so far the tradeoff has been completely worth it. Bevy's community has also proven highly helpful and knowledgeable. Karta will be able to leverage future improvements to Bevy. 33 | 34 | ------------------------------------------ 35 | 36 | ## Overview 37 | 38 | As of writing this, Karta is undergoing a refactoring to better prepare it for new features and use cases. It is being split into a few different crates and repositories for maintainability and extendability reasons. Easier to upgrade or swap out parts in case the project pivots. 39 | 40 | ### Crate structure 41 | 42 | #### Indexing - karta_server 43 | 44 | The core of Karta and its concept relies on an efficient and effective indexing of files into a graph structure. The library for this should be decoupled from the main Karta application so that it could eventually be accessed and modified by other applications as well. The separation also allows for the db to be run on a local or cloud server in addition to the local database file. 45 | 46 | Preferably, the solution would fulfill these requirements: 47 | * Scalable, O(1) lookup times always 48 | * Local first 49 | * Support for virtual nodes (nodes that don't reference a file or directory) 50 | * Support for arbitrary attributes on nodes and edges 51 | 52 | The crate developed for this purpose is [karta_server](https://github.com/teodosin/karta_server), which is essentially Karta's wrapper around [Agnesoft Graph Database](https://github.com/agnesoft/agdb). 53 | 54 | #### Bevy Plugin - bevy_karta_client 55 | 56 | Since the main Karta application is developed using Bevy, there needs to be a connective layer between the user interface and graphical elements being coded in Bevy and the database backend. [bevy_karta_client](https://github.com/teodosin/bevy_karta_client) is essentially the high-level backend of Karta, responsible for communication with the database. 57 | 58 | bevy_karta_client loads in nodes and edges from the database and turns them into Entities in Bevy's ECS. 59 | 60 | #### User Interface - bevy_karta_ui 61 | 62 | [bevy_karta_ui](https://github.com/teodosin/bevy_karta_ui). 63 | 64 | ### Open questions 65 | Some things I'm not currently dealing with but am imagining will be important in the future. Refer to VISION.md for the features that will want these technical challenges addressed. 66 | 67 | * How to tackle the serialization of non-unicode paths, to support moving of a vault from different operating systems? 68 | * How will the evaluation of the node graph for real-time composition work, and how will it be made performant? 69 | * How would the exporting of specific networks work? How to minimize the size of the exports and runtimes? 70 | * How to export a runtime to the web? 71 | * How should plugin and custom node support be implemented? 72 | 73 | -------------------------------------------------------------------------------- /karta_svelte/src/lib/tools/ConnectTool.ts: -------------------------------------------------------------------------------- 1 | import type { Tool, NodeId } from '$lib/types/types'; 2 | import { get } from 'svelte/store'; 3 | import { 4 | startConnectionProcess, 5 | updateTempLinePosition, 6 | finishConnectionProcess, 7 | cancelConnectionProcess, 8 | isConnecting 9 | } from '$lib/karta/ToolStore'; 10 | import { screenToCanvasCoordinates } from '$lib/karta/ViewportStore'; 11 | 12 | 13 | 14 | 15 | 16 | export class ConnectTool implements Tool { 17 | readonly name = 'connect'; 18 | 19 | // Bound methods for event listeners - Use PointerEvent 20 | private boundHandlePointerMove: (event: PointerEvent) => void; 21 | private boundHandlePointerUp: (event: PointerEvent) => void; 22 | 23 | constructor() { 24 | this.boundHandlePointerMove = this.handlePointerMove.bind(this); 25 | this.boundHandlePointerUp = this.handlePointerUp.bind(this); 26 | } 27 | 28 | activate() { 29 | document.body.style.cursor = 'crosshair'; // Set cursor on activate 30 | } 31 | 32 | deactivate() { 33 | if (get(isConnecting)) { 34 | cancelConnectionProcess(); // Cancel if connection was in progress 35 | } 36 | this.removeWindowListeners(); 37 | document.body.style.cursor = 'default'; // Reset cursor on deactivate 38 | } 39 | 40 | // Replaces onNodeMouseDown 41 | onPointerDown(event: PointerEvent, targetElement: EventTarget | null): void { 42 | if (event.button !== 0 || !(targetElement instanceof HTMLElement)) return; 43 | 44 | // Check if the target is a node element 45 | const nodeEl = targetElement.closest('.node-wrapper') as HTMLElement | null; // Use the new wrapper class 46 | if (!nodeEl || !nodeEl.dataset.id) return; // Exit if not a node 47 | 48 | const nodeId = nodeEl.dataset.id; 49 | startConnectionProcess([nodeId]); // KartaStore handles the state 50 | this.addWindowListeners(); // Add listeners to track pointer movement and release 51 | } 52 | 53 | // Replaces handleWindowMouseMove - Use PointerEvent 54 | private handlePointerMove(event: PointerEvent): void { 55 | if (!get(isConnecting)) return; 56 | 57 | // Need container rect for coordinate conversion 58 | const containerEl = document.querySelector('.w-full.h-screen.overflow-hidden') as HTMLElement; // Adjust selector if needed 59 | if (!containerEl) { 60 | console.error("Viewport container not found for coordinate conversion."); 61 | return; 62 | } 63 | const containerRect = containerEl.getBoundingClientRect(); 64 | const { x: mouseCanvasX, y: mouseCanvasY } = screenToCanvasCoordinates(event.clientX, event.clientY, containerRect); 65 | updateTempLinePosition(mouseCanvasX, mouseCanvasY); 66 | } 67 | 68 | // Replaces handleWindowMouseUp - Use PointerEvent 69 | private handlePointerUp(event: PointerEvent): void { 70 | if (!get(isConnecting) || event.button !== 0) return; 71 | 72 | let targetNodeId: NodeId | null = null; 73 | // Use event.target for pointerup, as it reflects the element where the pointer was released 74 | let currentElement: HTMLElement | null = event.target as HTMLElement; 75 | 76 | // Traverse up DOM to find a node element with data-id 77 | while (currentElement) { 78 | if (currentElement.dataset?.id && currentElement.classList.contains('node-wrapper')) { // Use the new wrapper class 79 | targetNodeId = currentElement.dataset.id; 80 | break; // Found it 81 | } 82 | // Stop if we hit the canvas container or body 83 | const viewportContainer = document.querySelector('.w-full.h-screen.overflow-hidden'); 84 | if (currentElement === document.body || currentElement === viewportContainer) { 85 | break; 86 | } 87 | currentElement = currentElement.parentElement; 88 | } 89 | 90 | finishConnectionProcess(targetNodeId); // KartaStore handles state reset and edge creation 91 | this.removeWindowListeners(); 92 | } 93 | 94 | // --- Removed Obsolete Methods --- 95 | // onCanvasClick, onCanvasMouseDown, getNodeCursorStyle, getCanvasCursorStyle 96 | // onWindowMouseMove, onWindowMouseUp 97 | 98 | // Optional: Implement updateCursor if needed 99 | updateCursor(): void { 100 | document.body.style.cursor = 'crosshair'; // Ensure cursor stays correct 101 | } 102 | 103 | // --- Helper methods for listeners - Use Pointer Events --- 104 | private addWindowListeners() { 105 | window.addEventListener('pointermove', this.boundHandlePointerMove); 106 | window.addEventListener('pointerup', this.boundHandlePointerUp, { once: true }); 107 | } 108 | 109 | private removeWindowListeners() { 110 | window.removeEventListener('pointermove', this.boundHandlePointerMove); 111 | window.removeEventListener('pointerup', this.boundHandlePointerUp); 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /karta_svelte/src/lib/components/TutorialModal.svelte: -------------------------------------------------------------------------------- 1 | 64 | 65 | 66 | 67 | {#if $isTutorialOpen} 68 | 159 | {/if} 160 | -------------------------------------------------------------------------------- /karta_svelte/src/lib/node_types/registry.ts: -------------------------------------------------------------------------------- 1 | // --- Karta Runtime Component --- 2 | // This file is planned for inclusion in the MIT-licensed `karta_runtime` package. 3 | // It dynamically loads and registers available node type components and definitions. 4 | // This is essential for both the editor and the runtime. 5 | 6 | import type { SvelteComponent } from 'svelte'; 7 | import type { NodeTypeDefinition, NodeTypeComponent, IconComponent } from './types'; // Include IconComponent 8 | import type { TweenableNodeState } from '$lib/types/types'; 9 | 10 | // Use Vite's glob import to eagerly load all Svelte component modules in this directory 11 | // Import the entire module to access both default (component) and named (nodeTypeDef) exports 12 | const modules = import.meta.glob('./*.svelte', { // Use 'any' for module type 13 | eager: true 14 | }); 15 | 16 | // Define the structure for the registry map 17 | // Key: ntype (string), Value: Combined object with definition and component 18 | interface RegistryEntry { 19 | // Store definition without component ref, as component is handled separately 20 | definition: Omit; 21 | component: NodeTypeComponent; 22 | } 23 | export const nodeTypeRegistry: Record = {}; 24 | 25 | // Populate the registry dynamically 26 | for (const path in modules) { 27 | const module = modules[path]; 28 | // Access the named export 'nodeTypeDef' 29 | const definition = module.nodeTypeDef as Omit; 30 | // Access the default export (the component) 31 | const component = module.default as NodeTypeComponent; 32 | 33 | if (definition && definition.ntype && component) { 34 | nodeTypeRegistry[definition.ntype] = { 35 | definition: definition, 36 | component: component 37 | }; 38 | } else { 39 | // Add more specific warnings 40 | let warnings = []; 41 | if (!module) warnings.push("module not loaded"); 42 | if (!definition) warnings.push("nodeTypeDef export missing"); 43 | else if (!definition.ntype) warnings.push("ntype missing in nodeTypeDef"); 44 | if (!component) warnings.push("default export (component) missing"); 45 | console.warn(`[Registry] Failed to load node type from path: ${path}. Issues: ${warnings.join(', ')}.`); 46 | } 47 | } 48 | 49 | // --- Registry Helper Functions --- 50 | 51 | // Get the full definition object (excluding component) for a type 52 | export function getNodeTypeDef(ntype: string): Omit | undefined { 53 | return nodeTypeRegistry[ntype]?.definition; 54 | } 55 | 56 | // Function to get the component constructor for a type 57 | export function getNodeComponent(ntype: string): NodeTypeComponent | undefined { 58 | const entry = nodeTypeRegistry[ntype]; 59 | // Fallback to generic component if type not found or component missing 60 | return entry?.component || nodeTypeRegistry['core/generic']?.component; 61 | } 62 | 63 | // Function to get default attributes for a type 64 | export function getDefaultAttributesForType(ntype: string): Record { 65 | const definition = nodeTypeRegistry[ntype]?.definition; 66 | let typeSpecificAttributes: Record = { name: `New ${ntype}` }; 67 | 68 | if (definition?.getDefaultAttributes) { 69 | const baseName = definition.displayName || ntype.charAt(0).toUpperCase() + ntype.slice(1); 70 | typeSpecificAttributes = definition.getDefaultAttributes(baseName); 71 | } else { 72 | console.warn(`getDefaultAttributes function missing for ntype: ${ntype}, using basic name default.`); 73 | } 74 | 75 | // Define common view defaults 76 | const defaultViewNodeState = getDefaultViewNodeStateForType(ntype); // Already handles fallback 77 | const commonViewDefaults: Record = { 78 | view_width: defaultViewNodeState.width, 79 | view_height: defaultViewNodeState.height, 80 | view_isNameVisible: ntype !== 'core/root' // True for all except root 81 | }; 82 | 83 | // Merge common defaults with type-specific attributes 84 | // Type-specific attributes can override common ones if there's a name clash (e.g. if a type *really* needed to define its own view_width) 85 | return { ...commonViewDefaults, ...typeSpecificAttributes }; 86 | } 87 | 88 | // Function to get default view state for a type 89 | export function getDefaultViewNodeStateForType(ntype: string): Omit { 90 | const definition = nodeTypeRegistry[ntype]?.definition; 91 | if (definition?.getDefaultViewNodeState) { 92 | return definition.getDefaultViewNodeState(); 93 | } 94 | // Fallback to generic defaults 95 | console.warn(`getDefaultViewNodeState function missing for ntype: ${ntype}, using fallback.`); 96 | return { width: 100, height: 100, scale: 1, rotation: 0 }; 97 | } 98 | 99 | // Function to get a list of available types for UI (excluding root) 100 | export function getAvailableNodeTypesForMenu(): { ntype: string; displayName: string; icon?: IconComponent }[] { 101 | return Object.values(nodeTypeRegistry) 102 | .filter(entry => entry.definition.ntype !== 'core/root') // Exclude root type 103 | .map(entry => ({ 104 | ntype: entry.definition.ntype, 105 | displayName: entry.definition.displayName || entry.definition.ntype, 106 | icon: entry.definition.icon // Include icon if defined 107 | })) 108 | .sort((a, b) => a.displayName.localeCompare(b.displayName)); // Sort alphabetically 109 | } -------------------------------------------------------------------------------- /karta_svelte/src/lib/components/ServerSetupModal.svelte: -------------------------------------------------------------------------------- 1 | 89 | 90 | 91 | {#if currentStep !== 'ready' && currentStep !== 'vault-selection'} 92 |
93 |
97 |
98 | 99 |

100 | Karta Server 101 |

102 |
103 | 104 | {#if currentStep === 'checking'} 105 |
106 |
107 |

108 | Checking server status... 109 |

110 |
111 | 112 | {:else if currentStep === 'starting-server'} 113 |
114 |
115 |

116 | Starting server... 117 |

118 |

119 | Vault: {selectedVaultPath} 120 |

121 |
122 | 123 | {:else if currentStep === 'error'} 124 |
125 | 126 |

127 | Server Error 128 |

129 |

130 | {errorMessage} 131 |

132 |
133 | 139 | {#if selectedVaultPath} 140 | 147 | {/if} 148 |
149 |
150 | 151 | {/if} 152 |
153 |
154 | {/if} 155 | 156 | 157 | 162 | 163 | 168 | -------------------------------------------------------------------------------- /karta_svelte/src/lib/karta/ViewportStore.ts: -------------------------------------------------------------------------------- 1 | import { writable, get, derived } from 'svelte/store'; 2 | import { Tween } from 'svelte/motion'; 3 | import { cubicInOut } from 'svelte/easing'; 4 | import type { ViewportSettings, AbsoluteTransform } from '../types/types'; 5 | import { currentContextId, currentViewNodes } from './ContextStore'; 6 | import { storeLogger } from '$lib/debug'; 7 | 8 | 9 | 10 | const DEFAULT_FOCAL_TRANSFORM: AbsoluteTransform = { x: 0, y: 0, scale: 1 }; 11 | const DEFAULT_VIEWPORT_SETTINGS: ViewportSettings = { scale: 1, posX: 0, posY: 0 }; 12 | const VIEWPORT_TWEEN_DURATION = 1000; 13 | 14 | 15 | export const viewTransform = new Tween( 16 | { ...DEFAULT_VIEWPORT_SETTINGS }, 17 | { duration: VIEWPORT_TWEEN_DURATION, easing: cubicInOut } 18 | ); 19 | 20 | 21 | export const viewportWidth = writable(0); 22 | export const viewportHeight = writable(0); 23 | 24 | 25 | 26 | 27 | 28 | // Viewport Actions 29 | /** Centers the viewport on a specific canvas coordinate, maintaining current scale. */ 30 | export function centerViewOnCanvasPoint(canvasX: number, canvasY: number) { 31 | 32 | const viewportEl = document.getElementById('viewport'); 33 | 34 | if (!viewportEl) { 35 | console.error("[centerViewOnCanvasPoint] Viewport element not found."); 36 | return; 37 | } 38 | 39 | const targetScale = 1; 40 | const targetPosX = -canvasX * targetScale; 41 | const targetPosY = -canvasY * targetScale; 42 | 43 | const newTransform = { 44 | scale: targetScale, 45 | posX: targetPosX, 46 | posY: targetPosY 47 | }; 48 | 49 | viewTransform.set(newTransform, { duration: VIEWPORT_TWEEN_DURATION }); 50 | } 51 | 52 | 53 | 54 | 55 | 56 | /** Centers the viewport on the current focal node. */ 57 | export function centerOnFocalNode() { 58 | 59 | const focalNodeId = get(currentContextId); 60 | const focalViewNode = get(currentViewNodes).get(focalNodeId); 61 | 62 | if (focalViewNode) { 63 | const nodeState = focalViewNode.state.current; 64 | const centerX = nodeState.x; 65 | const centerY = nodeState.y; 66 | 67 | centerViewOnCanvasPoint(centerX, centerY); 68 | 69 | } else { 70 | storeLogger.warn(`Cannot center on focal node: ViewNode ${focalNodeId} not found in current context.`); 71 | } 72 | } 73 | 74 | 75 | 76 | 77 | 78 | /** Calculates the bounding box of all nodes in the current context and adjusts the viewport to frame them. */ 79 | export function frameContext() { 80 | 81 | const viewportEl = document.getElementById('viewport'); 82 | 83 | if (!viewportEl) { 84 | console.error("[frameContext] Viewport element not found."); 85 | return; 86 | } 87 | const rect = viewportEl.getBoundingClientRect(); 88 | const nodesInContext = get(currentViewNodes); 89 | 90 | let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity; 91 | 92 | nodesInContext.forEach(viewNode => { 93 | 94 | const state = viewNode.state.current; 95 | const nodeLeft = state.x - (state.width / 2) * state.scale; 96 | const nodeRight = state.x + (state.width / 2) * state.scale; 97 | const nodeTop = state.y - (state.height / 2) * state.scale; 98 | const nodeBottom = state.y + (state.height / 2) * state.scale; 99 | 100 | minX = Math.min(minX, nodeLeft); 101 | minY = Math.min(minY, nodeTop); 102 | maxX = Math.max(maxX, nodeRight); 103 | maxY = Math.max(maxY, nodeBottom); 104 | }); 105 | 106 | const boundsWidth = maxX - minX; 107 | const boundsHeight = maxY - minY; 108 | const boundsCenterX = minX + boundsWidth / 2; 109 | const boundsCenterY = minY + boundsHeight / 2; 110 | 111 | // Fallback: center on the first node found (or the focal node if it exists) 112 | if (boundsWidth <= 0 || boundsHeight <= 0) { 113 | const firstNode = nodesInContext.values().next().value; 114 | if (firstNode) { 115 | const state = firstNode.state.current; 116 | centerViewOnCanvasPoint(state.x + state.width / 2, state.y + state.height / 2); 117 | } else { 118 | viewTransform.set({ ...DEFAULT_VIEWPORT_SETTINGS }, { duration: 0 }); // Reset if truly empty 119 | } 120 | return; 121 | } 122 | 123 | const padding = 0.1; 124 | const scaleX = rect.width / (boundsWidth * (1 + padding)); 125 | const scaleY = rect.height / (boundsHeight * (1 + padding)); 126 | 127 | const targetScale = Math.min(scaleX, scaleY, 2); 128 | const targetPosX = -boundsCenterX * targetScale; 129 | const targetPosY = -boundsCenterY * targetScale; 130 | 131 | const newTransform = { scale: targetScale, posX: targetPosX, posY: targetPosY }; 132 | viewTransform.set(newTransform, { duration: 0 }); 133 | } 134 | 135 | 136 | 137 | 138 | 139 | /** Converts screen coordinates to canvas coordinates based on the current viewport transform. */ 140 | export function screenToCanvasCoordinates(screenX: number, screenY: number, containerRect: DOMRect): { x: number; y: number } { 141 | 142 | const currentTransform = viewTransform.target; 143 | const vw = get(viewportWidth); 144 | const vh = get(viewportHeight); 145 | 146 | // This is the inverse of the transformation applied in Viewport.svelte 147 | // It accounts for the pan (posX, posY), zoom (scale), and the viewport center offset (vw/2, vh/2) 148 | const canvasX = (screenX - containerRect.left - currentTransform.posX - vw / 2) / currentTransform.scale; 149 | const canvasY = (screenY - containerRect.top - currentTransform.posY - vh / 2) / currentTransform.scale; 150 | 151 | return { x: canvasX, y: canvasY }; 152 | } 153 | 154 | 155 | 156 | 157 | 158 | export { DEFAULT_FOCAL_TRANSFORM, DEFAULT_VIEWPORT_SETTINGS, VIEWPORT_TWEEN_DURATION }; -------------------------------------------------------------------------------- /karta_server/src/server/write_endpoints_tests/test_context.rs: -------------------------------------------------------------------------------- 1 | use super::*; 2 | use super::test_helpers::*; 3 | 4 | #[tokio::test] 5 | async fn test_save_context_creates_file() { 6 | let (router, test_ctx) = setup_test_environment("save_creates_file"); 7 | 8 | // Arrange 9 | let focal_uuid = test_ctx.with_service_mut(|s| { 10 | s.data_mut().insert_nodes(vec![DataNode::new(&"vault/test_dir".into(), NodeTypeId::dir_type())]); 11 | s.open_context_from_path("vault/test_dir".into()).unwrap().2.focal() 12 | }); 13 | 14 | let initial_context = test_ctx.with_service(|s| s.open_context_from_path("vault/test_dir".into()).unwrap().2); 15 | let view_node_to_modify = initial_context.viewnodes().get(0).unwrap().clone(); 16 | let modified_view_node = view_node_to_modify.positioned(123.0, 456.0); 17 | let context_payload = Context::with_viewnodes(focal_uuid, vec![modified_view_node.clone()]); 18 | let payload_json = serde_json::to_string(&context_payload).unwrap(); 19 | 20 | // Act 21 | let response = 22 | execute_put_request(router, &format!("/api/ctx/{}", focal_uuid), payload_json).await; 23 | 24 | // Assert 25 | assert_eq!(response.status(), StatusCode::OK); 26 | let saved_context = test_ctx 27 | .with_service(|s| s.view().get_context_file(focal_uuid)) 28 | .unwrap(); 29 | assert_eq!(saved_context.viewnodes().len(), 1); 30 | assert_eq!(saved_context.viewnodes()[0].relX, 123.0); 31 | } 32 | 33 | #[tokio::test] 34 | async fn test_save_empty_context_deletes_file() { 35 | let (router, test_ctx) = setup_test_environment("save_empty_deletes"); 36 | 37 | // Arrange: Create a directory and save a context for it first. 38 | test_ctx.create_dir_in_vault("dir_to_delete").unwrap(); 39 | // Manually insert the node to ensure it's indexed before we try to save its context by UUID. 40 | let focal_uuid = test_ctx.with_service_mut(|s| { 41 | let node_to_insert = DataNode::new(&"vault/dir_to_delete".into(), NodeTypeId::dir_type()); 42 | s.data_mut().insert_nodes(vec![node_to_insert]); 43 | s.open_context_from_path("vault/dir_to_delete".into()).unwrap().2.focal() 44 | }); 45 | let initial_context = test_ctx.with_service(|s| s.open_context_from_path("vault/dir_to_delete".into()).unwrap().2); 46 | let view_node = initial_context.viewnodes().get(0).unwrap().clone(); 47 | let initial_payload = Context::with_viewnodes(focal_uuid, vec![view_node]); 48 | let initial_payload_json = serde_json::to_string(&initial_payload).unwrap(); 49 | execute_put_request( 50 | router.clone(), 51 | &format!("/api/ctx/{}", focal_uuid), 52 | initial_payload_json, 53 | ) 54 | .await; 55 | assert!(test_ctx 56 | .with_service(|s| s.view().get_context_file(focal_uuid)) 57 | .is_ok()); 58 | 59 | // Arrange: Create an empty payload. 60 | let empty_payload = Context::with_viewnodes(focal_uuid, vec![]); 61 | let empty_payload_json = serde_json::to_string(&empty_payload).unwrap(); 62 | 63 | // Act 64 | let response = execute_put_request( 65 | router, 66 | &format!("/api/ctx/{}", focal_uuid), 67 | empty_payload_json, 68 | ) 69 | .await; 70 | 71 | // Assert 72 | assert_eq!(response.status(), StatusCode::OK); 73 | assert!(test_ctx 74 | .with_service(|s| s.view().get_context_file(focal_uuid)) 75 | .is_err()); 76 | } 77 | 78 | #[tokio::test] 79 | async fn test_reload_context_merges_saved_and_default_nodes() { 80 | let (router, test_ctx) = setup_test_environment("reload_merges"); 81 | 82 | // Arrange: FS setup and index the nodes to simulate modification before saving. 83 | let (initial_nodes, _, initial_context) = test_ctx.with_service_mut(|s| { 84 | s.data_mut().insert_nodes(vec![ 85 | DataNode::new(&"vault/test_dir".into(), NodeTypeId::dir_type()), 86 | DataNode::new(&"vault/test_dir/A.txt".into(), NodeTypeId::file_type()), 87 | DataNode::new(&"vault/test_dir/B.txt".into(), NodeTypeId::file_type()), 88 | ]); 89 | s.open_context_from_path("vault/test_dir".into()).unwrap() 90 | }); 91 | let focal_uuid = initial_context.focal(); 92 | let node_b_data = initial_nodes.iter().find(|n| n.path().name() == "B.txt").expect("Node B data not found"); 93 | let node_b_view = initial_context.viewnodes().iter().find(|vn| vn.uuid == node_b_data.uuid()).expect("Node B view not found"); 94 | 95 | // Arrange: Save a modified position for node B. 96 | let modified_node_b = node_b_view.clone().positioned(500.0, 500.0); 97 | let save_payload = Context::with_viewnodes(focal_uuid, vec![modified_node_b]); 98 | let save_payload_json = serde_json::to_string(&save_payload).unwrap(); 99 | execute_put_request( 100 | router.clone(), 101 | &format!("/api/ctx/{}", focal_uuid), 102 | save_payload_json, 103 | ) 104 | .await; 105 | 106 | // Act: Reload the context. 107 | let response = router 108 | .oneshot(Request::builder().uri("/ctx/vault/test_dir").body(Body::empty()).unwrap()) 109 | .await 110 | .unwrap(); 111 | let body = axum::body::to_bytes(response.into_body(), usize::MAX).await.unwrap(); 112 | let reloaded_bundle: (Vec, Vec, Context) = 113 | serde_json::from_slice(&body).unwrap(); 114 | let reloaded_context = reloaded_bundle.2; 115 | 116 | // Assert 117 | let node_a_data = initial_nodes.iter().find(|n| n.path().name() == "A.txt").unwrap(); 118 | let reloaded_a = reloaded_context.viewnodes().iter().find(|vn| vn.uuid == node_a_data.uuid()).unwrap(); 119 | let reloaded_b = reloaded_context.viewnodes().iter().find(|vn| vn.uuid == node_b_data.uuid()).unwrap(); 120 | 121 | assert_eq!(reloaded_b.relX, 500.0, "Node B should have saved X pos"); 122 | assert_ne!(reloaded_a.relX, 0.0, "Node A should have default X pos"); 123 | assert_ne!(reloaded_a.relX, reloaded_b.relX, "A and B should have different X positions"); 124 | } 125 | -------------------------------------------------------------------------------- /karta_server/src/graph_traits/graph_edge.rs: -------------------------------------------------------------------------------- 1 | use std::error::Error; 2 | 3 | use uuid::Uuid; 4 | 5 | use super::{edge::Edge}; 6 | 7 | 8 | pub trait GraphEdge { 9 | fn get_edge_strict( 10 | &self, 11 | from: &Uuid, 12 | to: &Uuid 13 | ) -> Result>; 14 | 15 | fn insert_edges(&mut self, edges: Vec); 16 | 17 | fn get_edges_between_nodes(&self, nodes: &[Uuid]) -> Result, Box>; 18 | 19 | fn delete_edges(&mut self, edges: &[(Uuid, Uuid)]) -> Result<(), Box>; 20 | 21 | fn reconnect_edge( 22 | &mut self, 23 | old_from: &Uuid, 24 | old_to: &Uuid, 25 | new_from: &Uuid, 26 | new_to: &Uuid, 27 | ) -> Result>; 28 | 29 | /// Reparent a node by moving its contains edge from old parent to new parent 30 | /// This is specifically for moving nodes in the hierarchy and bypasses the 31 | /// normal restrictions on contains edge manipulation 32 | fn reparent_node( 33 | &mut self, 34 | node_uuid: &Uuid, 35 | old_parent_uuid: &Uuid, 36 | new_parent_uuid: &Uuid, 37 | ) -> Result<(), Box>; 38 | } 39 | 40 | #[cfg(test)] 41 | mod tests { 42 | use crate::{ 43 | elements::{node::DataNode, node_path::NodePath, edge::Edge, nodetype::NodeTypeId}, 44 | graph_agdb::GraphAgdb, 45 | graph_traits::{graph_edge::GraphEdge, graph_node::GraphNodes}, 46 | utils::utils::KartaServiceTestContext, 47 | }; 48 | 49 | #[test] 50 | fn test_delete_edge() { 51 | let mut ctx = KartaServiceTestContext::new("test_delete_edge"); 52 | let node1 = DataNode::new(&NodePath::from("node1"), NodeTypeId::new("core/text")); 53 | let node2 = DataNode::new(&NodePath::from("node2"), NodeTypeId::new("core/text")); 54 | let edge = Edge::new(node1.uuid(), node2.uuid()); 55 | 56 | ctx.with_service_mut(|s| { 57 | s.data_mut().insert_nodes(vec![node1.clone(), node2.clone()]); 58 | s.data_mut().insert_edges(vec![edge.clone()]); 59 | }); 60 | 61 | let edge_result = ctx.with_service(|s| s.data().get_edge_strict(&node1.uuid(), &node2.uuid())); 62 | assert!(edge_result.is_ok()); 63 | 64 | let result = ctx.with_service_mut(|s| s.data_mut().delete_edges(&[(node1.uuid(), node2.uuid())])); 65 | assert!(result.is_ok()); 66 | 67 | let edge_result = ctx.with_service(|s| s.data().get_edge_strict(&node1.uuid(), &node2.uuid())); 68 | assert!(edge_result.is_err()); 69 | } 70 | 71 | #[test] 72 | fn test_reconnect_edge() { 73 | let mut ctx = KartaServiceTestContext::new("test_reconnect_edge"); 74 | let node1 = DataNode::new(&NodePath::from("node1"), NodeTypeId::new("core/text")); 75 | let node2 = DataNode::new(&NodePath::from("node2"), NodeTypeId::new("core/text")); 76 | let node3 = DataNode::new(&NodePath::from("node3"), NodeTypeId::new("core/text")); 77 | let edge = Edge::new(node1.uuid(), node2.uuid()); 78 | 79 | ctx.with_service_mut(|s| { 80 | s.data_mut().insert_nodes(vec![node1.clone(), node2.clone(), node3.clone()]); 81 | s.data_mut().insert_edges(vec![edge.clone()]); 82 | }); 83 | 84 | let initial_edge = 85 | ctx.with_service(|s| s.data().get_edge_strict(&node1.uuid(), &node2.uuid())); 86 | assert!(initial_edge.is_ok()); 87 | 88 | let result = ctx.with_service_mut(|s| { 89 | s.data_mut() 90 | .reconnect_edge(&node1.uuid(), &node2.uuid(), &node1.uuid(), &node3.uuid()) 91 | }); 92 | 93 | assert!(result.is_ok()); 94 | 95 | let old_edge = 96 | ctx.with_service(|s| s.data().get_edge_strict(&node1.uuid(), &node2.uuid())); 97 | assert!(old_edge.is_err()); 98 | 99 | let new_edge = 100 | ctx.with_service(|s| s.data().get_edge_strict(&node1.uuid(), &node3.uuid())); 101 | assert!(new_edge.is_ok()); 102 | } 103 | 104 | #[test] 105 | fn test_reconnect_edge_to_root() { 106 | // Test reconnecting the "to" end to the root 107 | let mut ctx_to = KartaServiceTestContext::new("test_reconnect_edge_to_root_to"); 108 | let root_node_to = ctx_to.with_service(|s| s.data().open_node(&crate::elements::node_path::NodeHandle::Path(NodePath::root()))).unwrap(); 109 | let node1_to = DataNode::new(&NodePath::from("node1"), NodeTypeId::new("core/text")); 110 | let node2_to = DataNode::new(&NodePath::from("node2"), NodeTypeId::new("core/text")); 111 | let edge_to = Edge::new(node1_to.uuid(), node2_to.uuid()); 112 | 113 | ctx_to.with_service_mut(|s| { 114 | s.data_mut().insert_nodes(vec![node1_to.clone(), node2_to.clone()]); 115 | s.data_mut().insert_edges(vec![edge_to.clone()]); 116 | }); 117 | 118 | let result_to = ctx_to.with_service_mut(|s| { 119 | s.data_mut() 120 | .reconnect_edge(&node1_to.uuid(), &node2_to.uuid(), &node1_to.uuid(), &root_node_to.uuid()) 121 | }); 122 | 123 | assert!(result_to.is_ok()); 124 | assert!(ctx_to.with_service(|s| s.data().get_edge_strict(&node1_to.uuid(), &node2_to.uuid())).is_err()); 125 | assert!(ctx_to.with_service(|s| s.data().get_edge_strict(&node1_to.uuid(), &root_node_to.uuid())).is_ok()); 126 | 127 | // Test reconnecting the "from" end to the root 128 | let mut ctx_from = KartaServiceTestContext::new("test_reconnect_edge_to_root_from"); 129 | let root_node_from = ctx_from.with_service(|s| s.data().open_node(&crate::elements::node_path::NodeHandle::Path(NodePath::root()))).unwrap(); 130 | let node1_from = DataNode::new(&NodePath::from("node1"), NodeTypeId::new("core/text")); 131 | let node2_from = DataNode::new(&NodePath::from("node2"), NodeTypeId::new("core/text")); 132 | let edge_from = Edge::new(node1_from.uuid(), node2_from.uuid()); 133 | 134 | ctx_from.with_service_mut(|s| { 135 | s.data_mut().insert_nodes(vec![node1_from.clone(), node2_from.clone()]); 136 | s.data_mut().insert_edges(vec![edge_from.clone()]); 137 | }); 138 | 139 | let result_from = ctx_from.with_service_mut(|s| { 140 | s.data_mut() 141 | .reconnect_edge(&node1_from.uuid(), &node2_from.uuid(), &root_node_from.uuid(), &node2_from.uuid()) 142 | }); 143 | 144 | assert!(result_from.is_ok()); 145 | assert!(ctx_from.with_service(|s| s.data().get_edge_strict(&node1_from.uuid(), &node2_from.uuid())).is_err()); 146 | assert!(ctx_from.with_service(|s| s.data().get_edge_strict(&root_node_from.uuid(), &node2_from.uuid())).is_ok()); 147 | } 148 | } 149 | --------------------------------------------------------------------------------