# Zync > Zync is a Tauri v2 desktop SSH client — a terminal emulator, SFTP file manager, port-forward tunnel manager, and plugin platform — built with React + TypeScript (frontend) and Rust (backend). Targets Linux (AppImage), macOS, and Windows. ## Full References - [AGENTS.md](AGENTS.md): Complete architecture reference — directory map, all IPC commands, patterns, conventions, plugin authoring guide, dev commands - [CLAUDE.md](CLAUDE.md): Concise session primer — critical rules, key file locations, store slices, IPC event table - [README.md](README.md): User-facing project overview - [CHANGELOG.md](CHANGELOG.md): Version history - [CONTRIBUTING.md](CONTRIBUTING.md): Contributor guide - [PLUGIN_CATALOG.md](PLUGIN_CATALOG.md): Available plugins and themes --- ## Critical Rules — Read Before Changing Anything 1. **Never call `window.__TAURI__` directly.** All backend calls go through `src/lib/tauri-ipc.ts` which exposes an Electron-compatible `window.ipcRenderer` shim. 2. **Never call `app.path().app_data_dir()` in Rust.** Always use `get_data_dir(app)` — it respects the user's custom data path setting. 3. **Never destroy XTerm terminal instances by React unmounting.** Only call `destroyTerminalInstance(termId)` exported from `Terminal.tsx`. 4. **New Rust commands must be registered** in `src-tauri/src/lib.rs` inside `.invoke_handler(tauri::generate_handler![...])` — adding the function to `commands.rs` alone is not enough. 5. **Optimistic UI in `tunnelSlice` and `snippetsSlice` is intentional** — state updates before the IPC call, reverts on error. Do not remove this pattern. 6. **`fs:delete` uses `ssh exec rm -rf` and `fs:copy` uses `ssh exec cp -r` as fast paths** before falling back to SFTP. Do not replace with pure SFTP. --- ## Tech Stack | Layer | Technology | |---|---| | Frontend | React 18, TypeScript, Vite | | Styling | Tailwind CSS, Framer Motion | | State | Zustand — 10 slices composed in `src/store/useAppStore.ts` | | Terminal | XTerm.js + FitAddon + SearchAddon + WebLinksAddon | | Code editor | CodeMirror 6 (`@uiw/react-codemirror`) | | Charts | Recharts | | Drag & drop | `@dnd-kit/core`, `@dnd-kit/sortable` | | Command palette | `cmdk` | | Backend | Rust, Tauri v2 | | SSH | `russh` + `russh_sftp` | | Local PTY | `portable_pty` | | Async | `tokio` | | IPC shim | `src/lib/tauri-ipc.ts` — wraps Tauri v2 behind Electron-compatible `window.ipcRenderer` | --- ## Architecture Patterns ### IPC Shim All frontend↔backend calls use `ipcRenderer.invoke(channel, payload)` and `ipcRenderer.on(channel, cb)`. Never use `window.__TAURI__` directly. ### Zustand Slices 10 slices composed into a single `useAppStore`. Each slice can read the full store via `get()`. Slices: `connection`, `terminal`, `fileSystem`, `tunnel`, `settings`, `snippets`, `transfer`, `toast`, `ui`, `update`. ### Terminal Instance Cache `Terminal.tsx` keeps a **module-level** `Map` called `terminalCache`. Preserves XTerm DOM on React unmount (e.g. tab switches). Only `destroyTerminalInstance(id)` from `Terminal.tsx` should remove entries. ### Settings Storage (Two Locations) - Bootstrap: `app_data_dir/settings.json` (always accessible) - Active: `custom_dataPath/settings.json` (if user set custom path) `get_data_dir(app)` in Rust resolves the effective path. Never hardcode app data dir. --- ## IPC Quick Reference (most-used) ### Frontend → Backend (`ipc.invoke`) | Channel | What it does | |---|---| | `ssh_connect` | Connect SSH, init SFTP, detect OS | | `ssh_disconnect` | Close PTYs + remove connection handle | | `ssh_exec` / `ssh:exec` | Run command on remote (or local shell) | | `terminal_create` | Create local or remote PTY session | | `connections_get` | Load connections + folders | | `connections_save` | Save connections + folders | | `fs:list` | List directory (local or SFTP) | | `fs:readFile` / `fs:writeFile` | Read / write file contents | | `fs:delete` | Delete (tries `rm -rf` first, then SFTP) | | `fs:copy` | Copy (tries `cp -r` first, then SFTP) | | `sftp_put` / `sftp_get` | Upload / download with progress events | | `tunnel:start` / `tunnel:stop` | Start / stop port forward | | `tunnel:save` / `tunnel:delete` | Upsert / remove tunnel config | | `plugins:load` | Scan and load all plugins | | `plugins_install` | Download + extract plugin zip | | `plugins_uninstall` | Remove plugin directory | | `settings_get` / `settings_set` | Load / save app settings | | `update:check` / `update:install` | Check + install app update | ### Frontend → Backend (fire-and-forget `ipc.send`) | Channel | Payload | |---|---| | `terminal:write` | `{ termId, data: string }` | | `terminal:resize` | `{ termId, cols, rows }` | | `terminal:kill` | `{ termId }` | ### Backend → Frontend (Tauri events) | Event | When | |---|---| | `terminal-output-{termId}` | PTY has output bytes | | `terminal-exit-{termId}` | PTY process exited | | `transfer-progress` | SFTP transfer in progress | | `transfer-success` / `transfer-error` | SFTP transfer done / failed | | `tunnel:status-change` | Tunnel started / stopped / errored | | `app:request-close` | Window close button clicked | | `update:available` / `update:downloaded` | Update lifecycle | --- ## Plugin System Plugins run in two sandboxed contexts and communicate through the `zync` global API. ### Background Plugins (Web Worker) Loaded as blob URL by `src/context/PluginContext.tsx`. No visible UI. Good for commands, SSH automation, status bar widgets. ```js zync.on('ready', function() { // Command palette (Cmd+K / Ctrl+K) zync.commands.register('my-plugin.run', 'My Plugin: Run', async function() { const out = await zync.ssh.exec('uptime'); zync.ui.notify({ type: 'info', body: out }); }); // Status bar widget zync.statusBar.set('my-plugin', '⚡ active'); // Toast: success | error | info | warning zync.ui.notify({ type: 'success', body: 'Done!' }); // Confirm dialog → Promise const ok = await zync.ui.confirm({ title: 'Sure?', message: '...', variant: 'danger' }); // SSH exec → Promise const out = await zync.ssh.exec('df -h'); // Write to active terminal zync.terminal.send('ls -la\n'); // Open new terminal tab zync.terminal.newTab({ command: 'htop\n' }); // Register full HTML panel (iframe) zync.panel.register('my-plugin.panel', 'My Panel', '...'); zync.logger.log('[my-plugin] ready'); }); ``` ### Panel Plugins (Sandboxed iframe) Rendered by `src/components/plugins/PluginPanel.tsx` with `sandbox="allow-scripts allow-same-origin"`. `window.zync` shim injected into HTML head. Same API as worker plugins, bridged via `postMessage`. ### manifest.json — Plugin ```json { "id": "com.yourname.plugin.my-plugin", "name": "My Plugin", "version": "1.0.0", "description": "Short description.", "author": "Your Name", "main": "main.js", "permissions": ["terminal", "statusBar", "ui", "panel", "ssh"] } ``` ### manifest.json — Theme ```json { "id": "com.yourname.theme.my-theme", "name": "My Theme", "version": "1.0.0", "style": "theme.css", "mode": "dark", "preview_bg": "#0f111a", "preview_accent": "#6366f1" } ``` ### Packaging ```bash # manifest.json must be at the root of the zip (not inside a subfolder) zip -r my-plugin.zip manifest.json main.js ``` ### Publishing to Official Marketplace 1. Fork `github.com/gajendraxdev/zync-extensions` 2. Add plugin files under `plugins//` 3. Host the `.zip` at a permanent public URL (GitHub Releases recommended) 4. Add entry to `marketplace.json` with `id`, `name`, `version`, `description`, `author`, `type`, `downloadUrl` 5. Open a Pull Request — once merged it appears in-app for all users ### Self-Hosted Registry Anyone can host their own `marketplace.json` at any HTTPS URL and add it in Zync under **Settings → Extensions**. Two supported formats: - Flat array: `[ { ...plugin }, ... ]` - Object with keys: `{ "plugins": [...], "themes": [...] }` Each entry needs at minimum: `id`, `name`, `version`, `downloadUrl`. --- ## Key File Locations ### Frontend Entry Points - [src/main.tsx](src/main.tsx): React app entry point - [src/App.tsx](src/App.tsx): Root component - [src/index.css](src/index.css): Global styles ### IPC & Utilities - [src/lib/tauri-ipc.ts](src/lib/tauri-ipc.ts): IPC shim — all backend calls go through here - [src/lib/utils.ts](src/lib/utils.ts): `cn()`, `formatBytes()`, `formatDate()`, `debounce()` - [src/lib/keyboard.ts](src/lib/keyboard.ts): Keyboard shortcut parsing - [src/lib/shortcuts.ts](src/lib/shortcuts.ts): Shortcut definitions - [src/lib/tunnelPresets.ts](src/lib/tunnelPresets.ts): Built-in tunnel presets ### State (Zustand) - [src/store/useAppStore.ts](src/store/useAppStore.ts): Composes all slices - [src/store/connectionSlice.ts](src/store/connectionSlice.ts): SSH connections, tabs, folders - [src/store/terminalSlice.ts](src/store/terminalSlice.ts): Terminal instances per connection - [src/store/fileSystemSlice.ts](src/store/fileSystemSlice.ts): File manager navigation and operations - [src/store/tunnelSlice.ts](src/store/tunnelSlice.ts): Port forwarding tunnel configs - [src/store/settingsSlice.ts](src/store/settingsSlice.ts): App settings — theme, terminal, keybindings - [src/store/snippetsSlice.ts](src/store/snippetsSlice.ts): Saved commands - [src/store/transferSlice.ts](src/store/transferSlice.ts): SFTP upload/download queue - [src/store/toastSlice.ts](src/store/toastSlice.ts): Toast notifications - [src/store/uiSlice.ts](src/store/uiSlice.ts): Global confirm dialog - [src/store/updateSlice.ts](src/store/updateSlice.ts): App update status ### Hooks - [src/hooks/useFileSystemEvents.ts](src/hooks/useFileSystemEvents.ts): FS change event listener - [src/hooks/useTransferEvents.ts](src/hooks/useTransferEvents.ts): Transfer progress/success/error listener - [src/hooks/useTauriFileDrop.ts](src/hooks/useTauriFileDrop.ts): Native file drop handler - [src/hooks/useWindowDrag.ts](src/hooks/useWindowDrag.ts): Custom window drag region ### Plugin System - [src/context/PluginContext.tsx](src/context/PluginContext.tsx): Web Worker-based plugin runtime - [src/components/plugins/PluginPanel.tsx](src/components/plugins/PluginPanel.tsx): Sandboxed iframe panel renderer ### Layout & Shell - [src/components/layout/MainLayout.tsx](src/components/layout/MainLayout.tsx): Root layout — view routing, modal orchestration, data init - [src/components/layout/Sidebar.tsx](src/components/layout/Sidebar.tsx): DnD connection/tab list with folders - [src/components/layout/CombinedTabBar.tsx](src/components/layout/CombinedTabBar.tsx): Draggable tab bar - [src/components/layout/CommandPalette.tsx](src/components/layout/CommandPalette.tsx): cmdk-powered command search - [src/components/layout/StatusBar.tsx](src/components/layout/StatusBar.tsx): Bottom status bar ### Core Views - [src/components/Terminal.tsx](src/components/Terminal.tsx): XTerm.js terminal — module-level terminalCache, FitAddon, IPC bridge - [src/components/FileManager.tsx](src/components/FileManager.tsx): SFTP file manager — context menu, drag-drop, search - [src/components/FileEditor.tsx](src/components/FileEditor.tsx): CodeMirror 6 editor with auto language detection - [src/components/dashboard/Dashboard.tsx](src/components/dashboard/Dashboard.tsx): SSH system stats dashboard (Recharts) ### Modals - [src/components/modals/AddConnectionModal.tsx](src/components/modals/AddConnectionModal.tsx): SSH connection form (new/edit) - [src/components/modals/AddTunnelModal.tsx](src/components/modals/AddTunnelModal.tsx): Tunnel form with animated SVG flow visualizer - [src/components/modals/ImportSshModal.tsx](src/components/modals/ImportSshModal.tsx): Import from ~/.ssh/config - [src/components/modals/ImportSSHCommandModal.tsx](src/components/modals/ImportSSHCommandModal.tsx): Parse SSH -L/-R flags into tunnels ### Settings - [src/components/settings/SettingsModal.tsx](src/components/settings/SettingsModal.tsx): 7-tab settings panel - [src/components/settings/Marketplace.tsx](src/components/settings/Marketplace.tsx): Plugin marketplace — fetches registry, install/uninstall ### Tunnels & Snippets - [src/components/tunnel/GlobalTunnelList.tsx](src/components/tunnel/GlobalTunnelList.tsx): Port forwarding manager with port conflict detection - [src/components/snippets/SnippetsManager.tsx](src/components/snippets/SnippetsManager.tsx): Saved commands manager ### UI Primitives - [src/components/ui/Button.tsx](src/components/ui/Button.tsx): Button with variants - [src/components/ui/Modal.tsx](src/components/ui/Modal.tsx): Portal-based modal wrapper - [src/components/ui/ContextMenu.tsx](src/components/ui/ContextMenu.tsx): Right-click context menu with boundary detection - [src/components/ui/GlobalConfirmDialog.tsx](src/components/ui/GlobalConfirmDialog.tsx): Radix confirm dialog - [src/components/ui/Toast.tsx](src/components/ui/Toast.tsx): Toast notification container - [src/components/icons/OSIcon.tsx](src/components/icons/OSIcon.tsx): OS/service logo resolver (20+ mappings) ### Backend — Rust - [src-tauri/src/lib.rs](src-tauri/src/lib.rs): AppState, Tauri app builder, command registrations - [src-tauri/src/commands.rs](src-tauri/src/commands.rs): All ~40 Tauri command handlers (~2200 lines) - [src-tauri/src/ssh.rs](src-tauri/src/ssh.rs): SshManager — recursive jump host support + virtual SSH agent - [src-tauri/src/pty.rs](src-tauri/src/pty.rs): PtyManager — local and remote PTY sessions - [src-tauri/src/tunnel.rs](src-tauri/src/tunnel.rs): TunnelManager — local/remote port forwarding - [src-tauri/src/fs.rs](src-tauri/src/fs.rs): FileSystem — local + SFTP with 4MB chunked streaming - [src-tauri/src/plugins.rs](src-tauri/src/plugins.rs): PluginScanner + 11 built-in themes - [src-tauri/src/ssh_config.rs](src-tauri/src/ssh_config.rs): Two-pass ~/.ssh/config parser with ProxyJump resolution - [src-tauri/src/types.rs](src-tauri/src/types.rs): Shared Rust types (ConnectionConfig, TunnelConfig, AuthMethod, etc.) ### Build & Config - [package.json](package.json): Frontend dependencies and scripts - [src-tauri/Cargo.toml](src-tauri/Cargo.toml): Rust dependencies - [vite.config.ts](vite.config.ts): Vite config - [tsconfig.json](tsconfig.json): TypeScript config - [src-tauri/tauri.conf.json](src-tauri/tauri.conf.json): Tauri app config (window, permissions, bundle) --- ## Data Files (on disk) All persisted in `get_data_dir()` (default: OS app data dir, overridable in Settings): | File | Contents | |---|---| | `settings.json` | App settings (`AppSettings` type) | | `connections.json` | `{ connections: Connection[], folders: Folder[] }` | | `tunnels_data.json` | `{ [connectionId]: TunnelConfig[] }` | | `snippets.json` | `{ snippets: Snippet[] }` | | `plugins.json` | `{ enabled_plugins: { [id]: bool } }` | | `keys/` | Internalized SSH private key files | | `plugins/` | Installed plugin directories (`{id}/manifest.json`, `main.js`, etc.) | --- ## What NOT to Do - **Never call `window.__TAURI__` directly** — always use `ipcRenderer` from `src/lib/tauri-ipc.ts` - **Never import from `node_modules` in plugin `main.js`** — plugins run in a Web Worker with no module system; use vanilla JS only - **Never mutate Zustand store directly** — always use the action functions - **Never use `app.path().app_data_dir()` in Rust** — use `get_data_dir(app)` to respect custom data path - **Never destroy `terminalCache` entries by React unmounting** — only `destroyTerminalInstance(id)` from `Terminal.tsx` - **Never skip registering a new Tauri command** in `lib.rs` `generate_handler![]`