From 6ac9ba60c3d45aee1c46549d5837094cb4d518da Mon Sep 17 00:00:00 2001 From: Vuong <3168632+vuon9@users.noreply.github.com> Date: Sun, 1 Mar 2026 14:21:21 +0700 Subject: [PATCH 1/3] chore: update Go version to 1.25.0 in release workflow --- .github/workflows/release.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index f7d0c0f..72e4b72 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -42,7 +42,7 @@ jobs: - name: Setup Go uses: actions/setup-go@v5 with: - go-version: "1.24.0" + go-version: "1.25.0" check-latest: true - name: Setup Bun From be7eb5e5dbce1b3a0a665cce31eb78dd9c5a390e Mon Sep 17 00:00:00 2001 From: Vuong <3168632+vuon9@users.noreply.github.com> Date: Sun, 1 Mar 2026 23:54:02 +0700 Subject: [PATCH 2/3] feat: multi features - command palette - system tray icon - code highlighting - WIP: close as minimum - simplify readme --- .entire/.gitignore | 4 + README.md | 143 +----- docs/BUILD.md | 53 +++ .../2025-03-01-syntax-highlighting-design.md | 334 ++++++++++++++ .../wailsapp/wails/v3/internal/eventcreate.js | 4 + .../wailsapp/wails/v3/internal/eventdata.d.ts | 4 + frontend/bun.lock | 79 ++++ frontend/package.json | 17 +- frontend/src/App.jsx | 35 +- frontend/src/components/CommandPalette.css | 158 +++++++ frontend/src/components/CommandPalette.jsx | 425 ++++++++++++++++++ frontend/src/components/SettingsModal.css | 39 ++ frontend/src/components/SettingsModal.jsx | 104 +++++ frontend/src/components/Sidebar.jsx | 2 +- frontend/src/components/TitleBar.jsx | 71 +-- frontend/src/components/ToolUI.jsx | 2 +- frontend/src/components/inputs/CodeEditor.jsx | 282 ++++++++++++ .../src/components/inputs/EditorToggle.jsx | 39 ++ .../src/components/inputs/HighlightedCode.jsx | 246 ++++++++++ .../inputs/carbonCodeMirrorTheme.js | 258 +++++++++++ frontend/src/components/inputs/index.js | 3 + .../src/components/inputs/sqlHighlighter.js | 71 +++ .../src/generated/http/settingsService.ts | 17 + .../generated/http/windowControlsService.ts | 38 ++ frontend/src/generated/index.ts | 2 + frontend/src/generated/wails/index.ts | 4 +- .../src/generated/wails/settingsService.ts | 16 + .../generated/wails/windowControlsService.ts | 40 ++ frontend/src/index.scss | 32 ++ frontend/src/pages/CodeFormatter/index.jsx | 127 +++++- frontend/src/pages/DataGenerator/index.jsx | 32 +- frontend/src/pages/TextConverter/index.jsx | 33 +- internal/codeformatter/css_selector_test.go | 186 ++++++++ internal/codeformatter/service.go | 297 ++++++++++-- internal/codeformatter/xpath_filter_test.go | 222 +++++++++ internal/settings/settings.go | 90 ++++ main.go | 105 ++++- service/settings.go | 63 +++ service/window.go | 66 +++ 39 files changed, 3521 insertions(+), 222 deletions(-) create mode 100644 .entire/.gitignore create mode 100644 docs/BUILD.md create mode 100644 docs/plans/2025-03-01-syntax-highlighting-design.md create mode 100644 frontend/src/components/CommandPalette.css create mode 100644 frontend/src/components/CommandPalette.jsx create mode 100644 frontend/src/components/SettingsModal.css create mode 100644 frontend/src/components/SettingsModal.jsx create mode 100644 frontend/src/components/inputs/CodeEditor.jsx create mode 100644 frontend/src/components/inputs/EditorToggle.jsx create mode 100644 frontend/src/components/inputs/HighlightedCode.jsx create mode 100644 frontend/src/components/inputs/carbonCodeMirrorTheme.js create mode 100644 frontend/src/components/inputs/sqlHighlighter.js create mode 100644 frontend/src/generated/http/settingsService.ts create mode 100644 frontend/src/generated/http/windowControlsService.ts create mode 100644 frontend/src/generated/wails/settingsService.ts create mode 100644 frontend/src/generated/wails/windowControlsService.ts create mode 100644 internal/codeformatter/css_selector_test.go create mode 100644 internal/codeformatter/xpath_filter_test.go create mode 100644 internal/settings/settings.go create mode 100644 service/settings.go create mode 100644 service/window.go diff --git a/.entire/.gitignore b/.entire/.gitignore new file mode 100644 index 0000000..2cffdef --- /dev/null +++ b/.entire/.gitignore @@ -0,0 +1,4 @@ +tmp/ +settings.local.json +metadata/ +logs/ diff --git a/README.md b/README.md index 38d9a12..6f84219 100644 --- a/README.md +++ b/README.md @@ -1,149 +1,42 @@ # DevToolbox -[![Tests & Build](https://github.com/vuon9/devtoolbox/actions/workflows/ci.yml/badge.svg)](https://github.com/vuon9/devtoolbox/actions/workflows/ci.yml) -[![Wails Build](https://github.com/vuon9/devtoolbox/actions/workflows/release.yml/badge.svg)](https://github.com/vuon9/devtoolbox/actions/workflows/release.yml) -[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) -[![Go Version](https://img.shields.io/badge/Go-1.25+-00ADD8?style=flat&logo=go&logoColor=white)](https://go.dev) +> [Download for macOS](https://github.com/vuon9/devtoolbox/releases) Β· [Windows](https://github.com/vuon9/devtoolbox/releases) Β· [Linux](https://github.com/vuon9/devtoolbox/releases) -Essential software development tools for everyday tasks. +A single app for 45+ common development tasks. Works offline, zero setup. -image +Base64, JWT, JSON formatting, hashing, encoding, escaping, color conversion, regex testing, cron parsing, diff checking, Unix time conversion, barcode generation, mock data, and 30+ more. +DevToolbox interface -## Features +No browser tabs. No data sent to servers. Just open and use. -### **Browser Support** +## Desktop or Browser -DevToolbox now works in both desktop and browser modes: - -- **Desktop**: Native Wails application with native performance (default) -- **Browser**: Access via `http://localhost:8081` when the desktop app is running - -The frontend automatically detects the environment and uses the appropriate API (Wails runtime for desktop, HTTP for browser). See [docs/BROWSER_MODE.md](docs/BROWSER_MODE.md) for details. - -### **Text Based Converter** (Unified Tool) -The central hub with 45+ algorithms across 5 categories: - -| Category | Algorithms | -|----------|------------| -| **πŸ” Encrypt / Decrypt** | AES, AES-GCM, DES, Triple DES, ChaCha20, Salsa20, XOR, RC4 | -| **πŸ”€ Encode / Decode** | Base64, Base64URL, Base32, Base58, Base16 (Hex), URL, HTML Entities, Binary, Morse Code, ROT13, ROT47, Quoted-Printable | -| **βœ‚οΈ Escape / Unescape** | String Literal, Unicode/Hex, HTML/XML, URL, Regex | -| **πŸ”„ Convert** | JSON ↔ YAML, JSON ↔ XML, JSON ↔ CSV, YAML ↔ TOML, CSV ↔ TSV, Properties ↔ JSON, INI ↔ JSON, Key-Value ↔ Query String, Number Bases, Case Swapping, Color Codes | -| **#️⃣ Hash** | MD5, SHA-1, SHA-224, SHA-256, SHA-384, SHA-512, SHA-3, BLAKE2b, BLAKE3, RIPEMD-160, bcrypt, scrypt, Argon2, HMAC, CRC32, Adler-32, MurmurHash3, xxHash, FNV-1a | - -**Special Features:** -- **"All Hashes" view** - Compute all 19 hash algorithms at once with copy buttons for each -- **Quick Action Tags** - Save frequently used conversions for instant access -- **Base64 Image Preview** - Automatically displays base64 images in output pane -- **Smart key/IV detection** - Automatically shows configuration pane when needed -- **Auto-run mode** - Results update instantly as you type -- **Horizontal/Vertical layout toggle** - Customize the workspace layout - -### **Other Tools** - -| Tool | Description | -|------|-------------| -| **JWT Debugger** | Decode and verify JWT tokens with header/payload inspection | -| **Barcode / QR Code Generator** | Create QR codes and 1D barcodes (EAN-13, EAN-8, Code 128, Code 39) with preview and download | -| **Data Generator** | Generate mock data with templates using Faker library (UUID, ULID, Random String, Lorem Ipsum, User Profiles, API responses, SQL inserts, and more) | -| **Code Formatter** | Format and minify JSON, XML, HTML, SQL, CSS, and JavaScript with advanced filtering support (jq for JSON, XPath for XML, CSS selectors for HTML) | -| **Color Converter** | Pick colors with eyedropper and generate code snippets for 11+ programming languages (CSS, Swift, .NET, Java, Android, Obj-C, Flutter, Unity, React Native, OpenGL, SVG) | -| **RegExp Tester** | Test regular expressions with real-time matching | -| **Unix Time Converter** | Convert between Unix timestamps and human-readable dates | -| **String Utilities** | Sort/Dedupe lines, Case conversion (camelCase, snake_case, etc.), String Inspector | -| **Cron Job Parser** | Parse and explain cron expressions | -| **Text Diff Checker** | Compare two text blocks and highlight differences | -| **Number Converter** | Convert between Decimal, Hex, Octal, and Binary | +- **Desktop:** Native app (default). Fast, works offline. +- **Browser:** Access at `http://localhost:8081` when desktop app is running. ## Installation -### Download Pre-built Binaries -Download the latest release for your platform from the [Releases](https://github.com/vuon9/devtoolbox/releases) page. - -**Supported Platforms:** -- Windows (x64) -- macOS (Intel & Apple Silicon) -- Linux (x64) - -### Build from Source - -**Prerequisites:** -- Bun (>= 1.0) - Required for frontend dependencies -- Go (>= 1.22) -- Wails CLI: `go install github.com/wailsapp/wails/v2/cmd/wails@latest` +### macOS -**Build Steps:** ```bash -# Clone the repository -git clone https://github.com/your-org/devtoolbox.git -cd devtoolbox - -# Install dependencies and build -wails build - -# Or run in development mode -wails dev +# Download from releases, then: +xattr -cr /Applications/devtoolbox.app # First run only ``` -## Installation - -Download the latest release for your platform from the [Releases](https://github.com/vuon9/devtoolbox/releases) page. - -### macOS - -⚠️ **Note:** The macOS build is not signed with an Apple Developer certificate (requires $99/year). You may see a security warning when opening the app. - -**To bypass the security warning:** - -1. Download the `devtoolbox-macos.dmg` file -2. Open the DMG and drag the app to your Applications folder -3. **First time only:** Open Terminal and run: - ```bash - xattr -cr /Applications/devtoolbox.app - ``` - Or alternatively: - - Go to **System Settings** β†’ **Privacy & Security** - - Scroll down to the "Security" section - - Click **"Open Anyway"** next to the message about "devtoolbox" - - Click **"Open"** in the dialog that appears - -4. The app will now open normally - ### Windows -1. Download `devtoolbox-windows.exe` -2. Run the executable -3. If Windows Defender shows a warning, click **"More info"** β†’ **"Run anyway"** +Download `devtoolbox-windows.exe` from releases and run. ### Linux -1. Download `devtoolbox-linux.tar.gz` -2. Extract: `tar -xzf devtoolbox-linux.tar.gz` -3. Run: `./devtoolbox` - -## Key Features - -βœ… **Works Offline** - All tools run locally, no internet connection required -βœ… **Dark/Light Themes** - Switch between themes or use system preference -βœ… **Pin Tools** - Pin frequently used tools to the top of the sidebar -βœ… **Keyboard Shortcuts** - `Cmd/Ctrl + B` to toggle sidebar -βœ… **Copy to Clipboard** - One-click copy buttons on all output fields -βœ… **Auto-run** - See results instantly as you type (can be disabled) -βœ… **Responsive Layout** - Horizontal or vertical split panes - -## UI Design +```bash +tar -xzf devtoolbox-linux.tar.gz +./devtoolbox +``` -Built with **Carbon Design System** for a consistent, professional look: -- Clean, modern interface -- Accessible components -- Consistent spacing and typography -- Monospace fonts for code/data +Build from source: [docs/BUILD.md](docs/BUILD.md) ## License -MIT License - free to use, modify, and distribute. - ---- - -*Built with ❀️ using Go, React, and Wails.* +MIT diff --git a/docs/BUILD.md b/docs/BUILD.md new file mode 100644 index 0000000..f6844b9 --- /dev/null +++ b/docs/BUILD.md @@ -0,0 +1,53 @@ +# Build from Source + +## Prerequisites + +- Go 1.25+ +- Bun 1.0+ +- Wails CLI: `go install github.com/wailsapp/wails/v2/cmd/wails@latest` + +## Quick Build + +```bash +# Clone +git clone https://github.com/vuon9/devtoolbox.git +cd devtoolbox + +# Build +wails build + +# Or run in development mode +wails dev +``` + +## Development + +```bash +# Frontend only (hot reload) +cd frontend && bun dev + +# Backend only +go run . + +# Both (separate terminals) +wails dev # Terminal 1 +cd frontend && bun dev # Terminal 2 +``` + +## Output + +Built binaries are in `build/bin/`: +- `devtoolbox` (Linux/macOS) +- `devtoolbox.exe` (Windows) + +## Troubleshooting + +**Frontend build fails:** +```bash +cd frontend && bun install +``` + +**Wails not found:** +```bash +go install github.com/wailsapp/wails/v2/cmd/wails@latest +``` diff --git a/docs/plans/2025-03-01-syntax-highlighting-design.md b/docs/plans/2025-03-01-syntax-highlighting-design.md new file mode 100644 index 0000000..8af2261 --- /dev/null +++ b/docs/plans/2025-03-01-syntax-highlighting-design.md @@ -0,0 +1,334 @@ +# Syntax Highlighting Design + +**Date:** 2025-03-01 +**Status:** Ready for Implementation +**Scope:** Add syntax highlighting to code display and editing components across the devtoolbox + +--- + +## Overview + +Add syntax highlighting capabilities to the devtoolbox using CodeMirror 6, with per-tool toggle persistence and Carbon Design System theming. + +**Key Principles:** +- Highlighting ON by default (opt-out) +- Per-tool persistence (each tool remembers its own setting) +- Lazy loading of language modules for performance +- Graceful fallback to plain TextArea when disabled or on error + +--- + +## Goals + +1. Replace plain text areas with syntax-highlighted code editors in CodeFormatter +2. Update CodeSnippetsPanel to use highlighted display for all 11 language tabs +3. Add toggle controls for enabling/disabling highlighting per tool +4. Maintain Carbon Design System visual consistency +5. Support 8 languages initially: JSON, JavaScript, HTML, XML, CSS, SQL, Swift, Java + +--- + +## Architecture + +### Components + +Three new components in `/frontend/src/components/inputs/`: + +#### 1. CodeEditor.jsx +Editable code editor with CodeMirror integration. + +```jsx + {}} // Optional callback + readOnly={false} // Edit or view-only mode + highlight={true} // Controlled by tool toggle + placeholder="Paste code..." + className="optional-class" +/> +``` + +**Responsibilities:** +- Load CodeMirror dynamically when highlight=true +- Apply Carbon dark theme (g100) to editor +- Manage editor lifecycle (create/destroy on toggle) +- Fall back to native TextArea when highlight=false + +#### 2. HighlightedCode.jsx +Read-only code display for snippets and output panes. + +```jsx + +``` + +**Replaces:** `
` tags in CodeSnippetsPanel and read-only TextArea in ToolPane
+
+#### 3. EditorToggle.jsx
+Toggle button for ToolControls section.
+
+```jsx
+
+```
+
+**UI:**
+- Icon button with Code icon
+- Tooltip: "Syntax highlighting: On/Off"
+- Persists to localStorage immediately on toggle
+
+### State Management
+
+Each tool manages its own highlighting state:
+
+```javascript
+// localStorage key pattern
+`${toolKey}-editor-highlight`: 'true' | 'false'
+
+// Examples
+'codeFormatter-editor-highlight': 'true'
+'colorConverter-editor-highlight': 'false'
+```
+
+**Default:** ON for new users (opt-out model)
+
+**Persistence:** Immediate write to localStorage on toggle
+
+### Lazy Loading Strategy
+
+Language modules load on-demand via dynamic imports:
+
+```javascript
+const loadLanguage = async (lang) => {
+  const languageModules = {
+    json: () => import('@codemirror/lang-json'),
+    javascript: () => import('@codemirror/lang-javascript'),
+    typescript: () => import('@codemirror/lang-javascript'), // TS uses JS grammar
+    html: () => import('@codemirror/lang-html'),
+    xml: () => import('@codemirror/lang-xml'),
+    css: () => import('@codemirror/lang-css'),
+    sql: () => import('@codemirror/lang-sql'),
+    swift: () => import('@codemirror/legacy-modes/mode/swift'),
+    java: () => import('@codemirror/lang-java'),
+  };
+  
+  const loader = languageModules[lang];
+  if (!loader) return null;
+  
+  const module = await loader();
+  return module[lang === 'swift' ? 'swift' : `${lang}Language`];
+};
+```
+
+**Caching:** Modules cached after first load (browser cache + memory)
+
+### Tools to Enhance
+
+1. **CodeFormatter** (`/pages/CodeFormatter/index.jsx`)
+   - Add EditorToggle to ToolControls
+   - Replace input ToolPane with CodeEditor
+   - Replace output ToolPane with CodeEditor (readOnly)
+
+2. **ColorConverter** (`/pages/ColorConverter/components/CodeSnippetsPanel.jsx`)
+   - Replace `
` with HighlightedCode
+   - All 11 language tabs benefit automatically
+
+3. **JWTDebugger** (if exists)
+   - Add EditorToggle
+   - Highlight header/payload JSON display
+
+4. **TextDiffChecker** (if exists)
+   - Add EditorToggle
+   - Highlight diff output
+
+---
+
+## Data Flow
+
+```
+User loads tool
+    ↓
+Load persisted preference from localStorage (default: true)
+    ↓
+highlight=true?
+    β”œβ”€ YES β†’ Dynamically import CodeMirror + language module
+    β”‚        ↓
+    β”‚        Mount CodeMirror with Carbon theme
+    β”‚        ↓
+    β”‚        Render highlighted editor
+    β”‚
+    └─ NO  β†’ Render native TextArea (fallback)
+    
+User toggles EditorToggle
+    ↓
+Update state + persist to localStorage
+    ↓
+Re-render: mount/unmount CodeMirror accordingly
+```
+
+---
+
+## Error Handling
+
+### CodeMirror Load Failure
+- **Cause:** Network error, CDN unreachable
+- **Behavior:** Fall back to TextArea, show subtle warning icon with tooltip
+- **User message:** "Syntax highlighting unavailable"
+
+### Unsupported Language
+- **Cause:** Language prop not in supported list
+- **Behavior:** Render as plain text (no highlighting)
+- **Dev:** Console warning in development mode
+
+### Large Files (>1MB)
+- **Detection:** Check content length on value change
+- **Behavior:** Auto-disable highlighting, show info banner
+- **User message:** "Large file - highlighting disabled for performance"
+- **Override:** User can manually re-enable via toggle
+
+### Touch Devices
+- **Detection:** `window.matchMedia('(pointer: coarse)')`
+- **Behavior:** Default to OFF on mobile/tablet (better UX with native input)
+- **Override:** User can still enable if desired
+
+### localStorage Corruption
+- **Behavior:** Parse error caught, reset to default (ON)
+- **User impact:** None, silent recovery
+
+### Memory Cleanup
+- **Implementation:** `useEffect` cleanup function destroys CodeMirror instance
+- **Trigger:** Component unmount or toggle OFF
+
+---
+
+## Styling (Carbon Integration)
+
+CodeMirror theme maps to Carbon tokens:
+
+```javascript
+const carbonDarkTheme = EditorView.theme({
+  '&': {
+    backgroundColor: 'var(--cds-field)',
+    color: 'var(--cds-text-primary)',
+    fontFamily: "'IBM Plex Mono', monospace",
+    fontSize: '0.875rem',
+  },
+  '.cm-content': {
+    caretColor: 'var(--cds-focus)',
+  },
+  '.cm-cursor': {
+    borderLeftColor: 'var(--cds-focus)',
+  },
+  '.cm-selectionBackground': {
+    backgroundColor: 'var(--cds-highlight)',
+  },
+  // Syntax colors - subtle, accessible
+  '.cm-keyword': { color: 'var(--cds-text-primary)' },
+  '.cm-string': { color: 'var(--cds-support-success)' },
+  '.cm-number': { color: 'var(--cds-support-info)' },
+  '.cm-comment': { color: 'var(--cds-text-secondary)' },
+  // ... etc
+});
+```
+
+**Visual consistency:**
+- Matches Carbon g100 dark theme
+- Uses IBM Plex Mono for code
+- Respects `--cds-*` CSS custom properties
+
+---
+
+## Dependencies
+
+### New Packages
+```json
+{
+  "@codemirror/commands": "^6.0.0",
+  "@codemirror/lang-css": "^6.0.0",
+  "@codemirror/lang-html": "^6.0.0",
+  "@codemirror/lang-java": "^6.0.0",
+  "@codemirror/lang-javascript": "^6.0.0",
+  "@codemirror/lang-json": "^6.0.0",
+  "@codemirror/lang-sql": "^6.0.0",
+  "@codemirror/lang-xml": "^6.0.0",
+  "@codemirror/language": "^6.0.0",
+  "@codemirror/legacy-modes": "^6.0.0", // For Swift
+  "@codemirror/state": "^6.0.0",
+  "@codemirror/view": "^6.0.0",
+  "codemirror": "^6.0.0"
+}
+```
+
+**Estimated bundle impact:** ~200-300KB for all 8 languages (tree-shaken, loaded on demand)
+
+---
+
+## Testing Checklist
+
+### Manual Testing
+- [ ] Toggle persists across refresh for each tool
+- [ ] Language switching works (CodeFormatter JSON→XML)
+- [ ] Large file (>1MB) auto-disables highlighting
+- [ ] Copy button works in CodeEditor/HighlightedCode
+- [ ] Mobile defaults to OFF
+- [ ] Network failure shows graceful fallback
+
+### Component Tests
+- [ ] CodeEditor renders TextArea when highlight=false
+- [ ] CodeEditor loads CodeMirror when highlight=true
+- [ ] EditorToggle calls onToggle and persists
+- [ ] HighlightedCode displays with correct language
+
+### Bundle Analysis
+- [ ] Verify lazy loading (CodeMirror not in main chunk)
+- [ ] Target: <300KB size increase
+
+### Accessibility
+- [ ] Toggle has aria-label
+- [ ] Contrast ratios meet WCAG 2.1 AA
+- [ ] Keyboard navigation works
+
+---
+
+## Migration Path
+
+1. **Phase 1:** Create CodeEditor, HighlightedCode, EditorToggle components
+2. **Phase 2:** Update CodeFormatter (input + output panes)
+3. **Phase 3:** Update CodeSnippetsPanel (all language tabs)
+4. **Phase 4:** Add to JWTDebugger and TextDiffChecker
+5. **Phase 5:** Remove legacy ToolPane usage in favor of CodeEditor
+
+---
+
+## Open Questions
+
+1. Should we add TypeScript support explicitly or rely on JavaScript grammar?
+2. Do we want line numbers as a separate toggle or always on/off?
+3. Should the large file threshold be configurable?
+
+---
+
+## Appendix: File Locations
+
+**New files:**
+- `/frontend/src/components/inputs/CodeEditor.jsx`
+- `/frontend/src/components/inputs/HighlightedCode.jsx`
+- `/frontend/src/components/inputs/EditorToggle.jsx`
+
+**Modified files:**
+- `/frontend/src/components/inputs/index.js` - Add exports
+- `/frontend/src/pages/CodeFormatter/index.jsx` - Add EditorToggle, use CodeEditor
+- `/frontend/src/pages/ColorConverter/components/CodeSnippetsPanel.jsx` - Use HighlightedCode
+- `/frontend/package.json` - Add CodeMirror dependencies
+
+**Theme file (optional):**
+- `/frontend/src/components/inputs/carbonCodeMirrorTheme.js` - Extracted theme constants
diff --git a/frontend/bindings/github.com/wailsapp/wails/v3/internal/eventcreate.js b/frontend/bindings/github.com/wailsapp/wails/v3/internal/eventcreate.js
index 458def9..c61a5b2 100644
--- a/frontend/bindings/github.com/wailsapp/wails/v3/internal/eventcreate.js
+++ b/frontend/bindings/github.com/wailsapp/wails/v3/internal/eventcreate.js
@@ -8,7 +8,11 @@ import { Create as $Create } from "@wailsio/runtime";
 
 function configure() {
     Object.freeze(Object.assign($Create.Events, {
+        "settings:changed": $$createType0,
     }));
 }
 
+// Private type creation functions
+const $$createType0 = $Create.Map($Create.Any, $Create.Any);
+
 configure();
diff --git a/frontend/bindings/github.com/wailsapp/wails/v3/internal/eventdata.d.ts b/frontend/bindings/github.com/wailsapp/wails/v3/internal/eventdata.d.ts
index 793ee3e..b1e8586 100644
--- a/frontend/bindings/github.com/wailsapp/wails/v3/internal/eventdata.d.ts
+++ b/frontend/bindings/github.com/wailsapp/wails/v3/internal/eventdata.d.ts
@@ -8,7 +8,11 @@ import type { Events } from "@wailsio/runtime";
 declare module "@wailsio/runtime" {
     namespace Events {
         interface CustomEvents {
+            "app:quit": string;
+            "command-palette:open": string;
+            "settings:changed": { [_ in string]?: any };
             "time": string;
+            "window:toggle": string;
         }
     }
 }
diff --git a/frontend/bun.lock b/frontend/bun.lock
index fa39023..ce3bbd1 100644
--- a/frontend/bun.lock
+++ b/frontend/bun.lock
@@ -7,7 +7,22 @@
         "@carbon/icons-react": "^11.76.0",
         "@carbon/react": "^1.102.0",
         "@carbon/styles": "^1.101.0",
+        "@codemirror/commands": "^6.10.2",
+        "@codemirror/lang-css": "^6.3.1",
+        "@codemirror/lang-html": "^6.4.11",
+        "@codemirror/lang-java": "^6.0.2",
+        "@codemirror/lang-javascript": "^6.2.4",
+        "@codemirror/lang-json": "^6.0.2",
+        "@codemirror/lang-sql": "^6.10.0",
+        "@codemirror/lang-xml": "^6.1.0",
+        "@codemirror/language": "^6.12.2",
+        "@codemirror/legacy-modes": "^6.5.2",
+        "@codemirror/rangeset": "^0.19.9",
+        "@codemirror/state": "^6.5.4",
+        "@codemirror/view": "^6.39.15",
+        "@lezer/highlight": "^1.2.3",
         "@wailsio/runtime": "^3.0.0-alpha.79",
+        "codemirror": "^6.0.2",
         "cronstrue": "^3.9.0",
         "diff": "^8.0.2",
         "js-beautify": "^1.15.4",
@@ -99,6 +114,40 @@
 
     "@carbon/utilities": ["@carbon/utilities@0.16.0", "", { "dependencies": { "@ibm/telemetry-js": "^1.6.1", "@internationalized/number": "^3.6.1" } }, "sha512-lEYSOpD8Dfh9NJ00uNSORcJ2Bl4+UugM11110SBpbFyes4bWSQkSoRLqoIfV2f1WwFBVDyGXV08O3FaWUgQk3w=="],
 
+    "@codemirror/autocomplete": ["@codemirror/autocomplete@6.20.0", "", { "dependencies": { "@codemirror/language": "^6.0.0", "@codemirror/state": "^6.0.0", "@codemirror/view": "^6.17.0", "@lezer/common": "^1.0.0" } }, "sha512-bOwvTOIJcG5FVo5gUUupiwYh8MioPLQ4UcqbcRf7UQ98X90tCa9E1kZ3Z7tqwpZxYyOvh1YTYbmZE9RTfTp5hg=="],
+
+    "@codemirror/commands": ["@codemirror/commands@6.10.2", "", { "dependencies": { "@codemirror/language": "^6.0.0", "@codemirror/state": "^6.4.0", "@codemirror/view": "^6.27.0", "@lezer/common": "^1.1.0" } }, "sha512-vvX1fsih9HledO1c9zdotZYUZnE4xV0m6i3m25s5DIfXofuprk6cRcLUZvSk3CASUbwjQX21tOGbkY2BH8TpnQ=="],
+
+    "@codemirror/lang-css": ["@codemirror/lang-css@6.3.1", "", { "dependencies": { "@codemirror/autocomplete": "^6.0.0", "@codemirror/language": "^6.0.0", "@codemirror/state": "^6.0.0", "@lezer/common": "^1.0.2", "@lezer/css": "^1.1.7" } }, "sha512-kr5fwBGiGtmz6l0LSJIbno9QrifNMUusivHbnA1H6Dmqy4HZFte3UAICix1VuKo0lMPKQr2rqB+0BkKi/S3Ejg=="],
+
+    "@codemirror/lang-html": ["@codemirror/lang-html@6.4.11", "", { "dependencies": { "@codemirror/autocomplete": "^6.0.0", "@codemirror/lang-css": "^6.0.0", "@codemirror/lang-javascript": "^6.0.0", "@codemirror/language": "^6.4.0", "@codemirror/state": "^6.0.0", "@codemirror/view": "^6.17.0", "@lezer/common": "^1.0.0", "@lezer/css": "^1.1.0", "@lezer/html": "^1.3.12" } }, "sha512-9NsXp7Nwp891pQchI7gPdTwBuSuT3K65NGTHWHNJ55HjYcHLllr0rbIZNdOzas9ztc1EUVBlHou85FFZS4BNnw=="],
+
+    "@codemirror/lang-java": ["@codemirror/lang-java@6.0.2", "", { "dependencies": { "@codemirror/language": "^6.0.0", "@lezer/java": "^1.0.0" } }, "sha512-m5Nt1mQ/cznJY7tMfQTJchmrjdjQ71IDs+55d1GAa8DGaB8JXWsVCkVT284C3RTASaY43YknrK2X3hPO/J3MOQ=="],
+
+    "@codemirror/lang-javascript": ["@codemirror/lang-javascript@6.2.4", "", { "dependencies": { "@codemirror/autocomplete": "^6.0.0", "@codemirror/language": "^6.6.0", "@codemirror/lint": "^6.0.0", "@codemirror/state": "^6.0.0", "@codemirror/view": "^6.17.0", "@lezer/common": "^1.0.0", "@lezer/javascript": "^1.0.0" } }, "sha512-0WVmhp1QOqZ4Rt6GlVGwKJN3KW7Xh4H2q8ZZNGZaP6lRdxXJzmjm4FqvmOojVj6khWJHIb9sp7U/72W7xQgqAA=="],
+
+    "@codemirror/lang-json": ["@codemirror/lang-json@6.0.2", "", { "dependencies": { "@codemirror/language": "^6.0.0", "@lezer/json": "^1.0.0" } }, "sha512-x2OtO+AvwEHrEwR0FyyPtfDUiloG3rnVTSZV1W8UteaLL8/MajQd8DpvUb2YVzC+/T18aSDv0H9mu+xw0EStoQ=="],
+
+    "@codemirror/lang-sql": ["@codemirror/lang-sql@6.10.0", "", { "dependencies": { "@codemirror/autocomplete": "^6.0.0", "@codemirror/language": "^6.0.0", "@codemirror/state": "^6.0.0", "@lezer/common": "^1.2.0", "@lezer/highlight": "^1.0.0", "@lezer/lr": "^1.0.0" } }, "sha512-6ayPkEd/yRw0XKBx5uAiToSgGECo/GY2NoJIHXIIQh1EVwLuKoU8BP/qK0qH5NLXAbtJRLuT73hx7P9X34iO4w=="],
+
+    "@codemirror/lang-xml": ["@codemirror/lang-xml@6.1.0", "", { "dependencies": { "@codemirror/autocomplete": "^6.0.0", "@codemirror/language": "^6.4.0", "@codemirror/state": "^6.0.0", "@codemirror/view": "^6.0.0", "@lezer/common": "^1.0.0", "@lezer/xml": "^1.0.0" } }, "sha512-3z0blhicHLfwi2UgkZYRPioSgVTo9PV5GP5ducFH6FaHy0IAJRg+ixj5gTR1gnT/glAIC8xv4w2VL1LoZfs+Jg=="],
+
+    "@codemirror/language": ["@codemirror/language@6.12.2", "", { "dependencies": { "@codemirror/state": "^6.0.0", "@codemirror/view": "^6.23.0", "@lezer/common": "^1.5.0", "@lezer/highlight": "^1.0.0", "@lezer/lr": "^1.0.0", "style-mod": "^4.0.0" } }, "sha512-jEPmz2nGGDxhRTg3lTpzmIyGKxz3Gp3SJES4b0nAuE5SWQoKdT5GoQ69cwMmFd+wvFUhYirtDTr0/DRHpQAyWg=="],
+
+    "@codemirror/legacy-modes": ["@codemirror/legacy-modes@6.5.2", "", { "dependencies": { "@codemirror/language": "^6.0.0" } }, "sha512-/jJbwSTazlQEDOQw2FJ8LEEKVS72pU0lx6oM54kGpL8t/NJ2Jda3CZ4pcltiKTdqYSRk3ug1B3pil1gsjA6+8Q=="],
+
+    "@codemirror/lint": ["@codemirror/lint@6.9.4", "", { "dependencies": { "@codemirror/state": "^6.0.0", "@codemirror/view": "^6.35.0", "crelt": "^1.0.5" } }, "sha512-ABc9vJ8DEmvOWuH26P3i8FpMWPQkduD9Rvba5iwb6O3hxASgclm3T3krGo8NASXkHCidz6b++LWlzWIUfEPSWw=="],
+
+    "@codemirror/rangeset": ["@codemirror/rangeset@0.19.9", "", { "dependencies": { "@codemirror/state": "^0.19.0" } }, "sha512-V8YUuOvK+ew87Xem+71nKcqu1SXd5QROMRLMS/ljT5/3MCxtgrRie1Cvild0G/Z2f1fpWxzX78V0U4jjXBorBQ=="],
+
+    "@codemirror/search": ["@codemirror/search@6.6.0", "", { "dependencies": { "@codemirror/state": "^6.0.0", "@codemirror/view": "^6.37.0", "crelt": "^1.0.5" } }, "sha512-koFuNXcDvyyotWcgOnZGmY7LZqEOXZaaxD/j6n18TCLx2/9HieZJ5H6hs1g8FiRxBD0DNfs0nXn17g872RmYdw=="],
+
+    "@codemirror/state": ["@codemirror/state@6.5.4", "", { "dependencies": { "@marijn/find-cluster-break": "^1.0.0" } }, "sha512-8y7xqG/hpB53l25CIoit9/ngxdfoG+fx+V3SHBrinnhOtLvKHRyAJJuHzkWrR4YXXLX8eXBsejgAAxHUOdW1yw=="],
+
+    "@codemirror/text": ["@codemirror/text@0.19.6", "", {}, "sha512-T9jnREMIygx+TPC1bOuepz18maGq/92q2a+n4qTqObKwvNMg+8cMTslb8yxeEDEq7S3kpgGWxgO1UWbQRij0dA=="],
+
+    "@codemirror/view": ["@codemirror/view@6.39.15", "", { "dependencies": { "@codemirror/state": "^6.5.0", "crelt": "^1.0.6", "style-mod": "^4.1.0", "w3c-keyname": "^2.2.4" } }, "sha512-aCWjgweIIXLBHh7bY6cACvXuyrZ0xGafjQ2VInjp4RM4gMfscK5uESiNdrH0pE+e1lZr2B4ONGsjchl2KsKZzg=="],
+
     "@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.21.5", "", { "os": "aix", "cpu": "ppc64" }, "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ=="],
 
     "@esbuild/android-arm": ["@esbuild/android-arm@0.21.5", "", { "os": "android", "cpu": "arm" }, "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg=="],
@@ -189,6 +238,26 @@
 
     "@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="],
 
+    "@lezer/common": ["@lezer/common@1.5.1", "", {}, "sha512-6YRVG9vBkaY7p1IVxL4s44n5nUnaNnGM2/AckNgYOnxTG2kWh1vR8BMxPseWPjRNpb5VtXnMpeYAEAADoRV1Iw=="],
+
+    "@lezer/css": ["@lezer/css@1.3.1", "", { "dependencies": { "@lezer/common": "^1.2.0", "@lezer/highlight": "^1.0.0", "@lezer/lr": "^1.3.0" } }, "sha512-PYAKeUVBo3HFThruRyp/iK91SwiZJnzXh8QzkQlwijB5y+N5iB28+iLk78o2zmKqqV0uolNhCwFqB8LA7b0Svg=="],
+
+    "@lezer/highlight": ["@lezer/highlight@1.2.3", "", { "dependencies": { "@lezer/common": "^1.3.0" } }, "sha512-qXdH7UqTvGfdVBINrgKhDsVTJTxactNNxLk7+UMwZhU13lMHaOBlJe9Vqp907ya56Y3+ed2tlqzys7jDkTmW0g=="],
+
+    "@lezer/html": ["@lezer/html@1.3.13", "", { "dependencies": { "@lezer/common": "^1.2.0", "@lezer/highlight": "^1.0.0", "@lezer/lr": "^1.0.0" } }, "sha512-oI7n6NJml729m7pjm9lvLvmXbdoMoi2f+1pwSDJkl9d68zGr7a9Btz8NdHTGQZtW2DA25ybeuv/SyDb9D5tseg=="],
+
+    "@lezer/java": ["@lezer/java@1.1.3", "", { "dependencies": { "@lezer/common": "^1.2.0", "@lezer/highlight": "^1.0.0", "@lezer/lr": "^1.0.0" } }, "sha512-yHquUfujwg6Yu4Fd1GNHCvidIvJwi/1Xu2DaKl/pfWIA2c1oXkVvawH3NyXhCaFx4OdlYBVX5wvz2f7Aoa/4Xw=="],
+
+    "@lezer/javascript": ["@lezer/javascript@1.5.4", "", { "dependencies": { "@lezer/common": "^1.2.0", "@lezer/highlight": "^1.1.3", "@lezer/lr": "^1.3.0" } }, "sha512-vvYx3MhWqeZtGPwDStM2dwgljd5smolYD2lR2UyFcHfxbBQebqx8yjmFmxtJ/E6nN6u1D9srOiVWm3Rb4tmcUA=="],
+
+    "@lezer/json": ["@lezer/json@1.0.3", "", { "dependencies": { "@lezer/common": "^1.2.0", "@lezer/highlight": "^1.0.0", "@lezer/lr": "^1.0.0" } }, "sha512-BP9KzdF9Y35PDpv04r0VeSTKDeox5vVr3efE7eBbx3r4s3oNLfunchejZhjArmeieBH+nVOpgIiBJpEAv8ilqQ=="],
+
+    "@lezer/lr": ["@lezer/lr@1.4.8", "", { "dependencies": { "@lezer/common": "^1.0.0" } }, "sha512-bPWa0Pgx69ylNlMlPvBPryqeLYQjyJjqPx+Aupm5zydLIF3NE+6MMLT8Yi23Bd9cif9VS00aUebn+6fDIGBcDA=="],
+
+    "@lezer/xml": ["@lezer/xml@1.0.6", "", { "dependencies": { "@lezer/common": "^1.2.0", "@lezer/highlight": "^1.0.0", "@lezer/lr": "^1.0.0" } }, "sha512-CdDwirL0OEaStFue/66ZmFSeppuL6Dwjlk8qk153mSQwiSH/Dlri4GNymrNWnUmPl2Um7QfV1FO9KFUyX3Twww=="],
+
+    "@marijn/find-cluster-break": ["@marijn/find-cluster-break@1.0.2", "", {}, "sha512-l0h88YhZFyKdXIFNfSWpyjStDjGHwZ/U7iobcK1cQQD8sejsONdQtTVU+1wVN1PBw40PiiHB1vA5S7VTfQiP9g=="],
+
     "@one-ini/wasm": ["@one-ini/wasm@0.1.1", "", {}, "sha512-XuySG1E38YScSJoMlqovLru4KTUNSjgVTIjyh7qMX6aNN5HY5Ct5LhRJdxO79JtTzKfzV/bnWpz+zquYrISsvw=="],
 
     "@parcel/watcher": ["@parcel/watcher@2.5.6", "", { "dependencies": { "detect-libc": "^2.0.3", "is-glob": "^4.0.3", "node-addon-api": "^7.0.0", "picomatch": "^4.0.3" }, "optionalDependencies": { "@parcel/watcher-android-arm64": "2.5.6", "@parcel/watcher-darwin-arm64": "2.5.6", "@parcel/watcher-darwin-x64": "2.5.6", "@parcel/watcher-freebsd-x64": "2.5.6", "@parcel/watcher-linux-arm-glibc": "2.5.6", "@parcel/watcher-linux-arm-musl": "2.5.6", "@parcel/watcher-linux-arm64-glibc": "2.5.6", "@parcel/watcher-linux-arm64-musl": "2.5.6", "@parcel/watcher-linux-x64-glibc": "2.5.6", "@parcel/watcher-linux-x64-musl": "2.5.6", "@parcel/watcher-win32-arm64": "2.5.6", "@parcel/watcher-win32-ia32": "2.5.6", "@parcel/watcher-win32-x64": "2.5.6" } }, "sha512-tmmZ3lQxAe/k/+rNnXQRawJ4NjxO2hqiOLTHvWchtGZULp4RyFeh6aU4XdOYBFe2KE1oShQTv4AblOs2iOrNnQ=="],
@@ -323,6 +392,8 @@
 
     "cliui": ["cliui@6.0.0", "", { "dependencies": { "string-width": "^4.2.0", "strip-ansi": "^6.0.0", "wrap-ansi": "^6.2.0" } }, "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ=="],
 
+    "codemirror": ["codemirror@6.0.2", "", { "dependencies": { "@codemirror/autocomplete": "^6.0.0", "@codemirror/commands": "^6.0.0", "@codemirror/language": "^6.0.0", "@codemirror/lint": "^6.0.0", "@codemirror/search": "^6.0.0", "@codemirror/state": "^6.0.0", "@codemirror/view": "^6.0.0" } }, "sha512-VhydHotNW5w1UGK0Qj96BwSk/Zqbp9WbnyK2W/eVMv4QyF41INRGpjUhFJY7/uDNuudSc33a/PKr4iDqRduvHw=="],
+
     "color": ["color@4.2.3", "", { "dependencies": { "color-convert": "^2.0.1", "color-string": "^1.9.0" } }, "sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A=="],
 
     "color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="],
@@ -341,6 +412,8 @@
 
     "copy-to-clipboard": ["copy-to-clipboard@3.3.3", "", { "dependencies": { "toggle-selection": "^1.0.6" } }, "sha512-2KV8NhB5JqC3ky0r9PMCAZKbUHSwtEo4CwCs0KXgruG43gX5PMqDEBbVU4OUzw2MuAWUfsuFmWvEKG5QRfSnJA=="],
 
+    "crelt": ["crelt@1.0.6", "", {}, "sha512-VQ2MBenTq1fWZUH9DJNGti7kKv6EeAuYr3cLwxUWhIu1baTaXh4Ib5W2CqHVqib4/MqbYGJqiL3Zb8GJZr3l4g=="],
+
     "cronstrue": ["cronstrue@3.9.0", "", { "bin": { "cronstrue": "bin/cli.js" } }, "sha512-T3S35zmD0Ai2B4ko6+mEM+k9C6tipe2nB9RLiGT6QL2Wn0Vsn2cCZAC8Oeuf4CaE00GZWVdpYitbpWCNlIWqdA=="],
 
     "cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="],
@@ -539,6 +612,8 @@
 
     "strip-ansi-cjs": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="],
 
+    "style-mod": ["style-mod@4.1.3", "", {}, "sha512-i/n8VsZydrugj3Iuzll8+x/00GH2vnYsk1eomD8QiRrSAeW6ItbCQDtfXCeJHd0iwiNagqjQkvpvREEPtW3IoQ=="],
+
     "tabbable": ["tabbable@6.4.0", "", {}, "sha512-05PUHKSNE8ou2dwIxTngl4EzcnsCDZGJ/iCLtDflR/SHB/ny14rXc+qU5P4mG9JkusiV7EivzY9Mhm55AzAvCg=="],
 
     "toggle-selection": ["toggle-selection@1.0.6", "", {}, "sha512-BiZS+C1OS8g/q2RRbJmy59xpyghNBqrr6k5L/uKBGRsTfxmu3ffiRnd8mlGPUVayg8pvfi5urfnu8TU7DVOkLQ=="],
@@ -551,6 +626,8 @@
 
     "vite": ["vite@5.4.21", "", { "dependencies": { "esbuild": "^0.21.3", "postcss": "^8.4.43", "rollup": "^4.20.0" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^18.0.0 || >=20.0.0", "less": "*", "lightningcss": "^1.21.0", "sass": "*", "sass-embedded": "*", "stylus": "*", "sugarss": "*", "terser": "^5.4.0" }, "optionalPeers": ["@types/node", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser"], "bin": { "vite": "bin/vite.js" } }, "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw=="],
 
+    "w3c-keyname": ["w3c-keyname@2.2.8", "", {}, "sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ=="],
+
     "which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="],
 
     "which-module": ["which-module@2.0.1", "", {}, "sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ=="],
@@ -571,6 +648,8 @@
 
     "yargs-parser": ["yargs-parser@18.1.3", "", { "dependencies": { "camelcase": "^5.0.0", "decamelize": "^1.2.0" } }, "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ=="],
 
+    "@codemirror/rangeset/@codemirror/state": ["@codemirror/state@0.19.9", "", { "dependencies": { "@codemirror/text": "^0.19.0" } }, "sha512-psOzDolKTZkx4CgUqhBQ8T8gBc0xN5z4gzed109aF6x7D7umpDRoimacI/O6d9UGuyl4eYuDCZmDFr2Rq7aGOw=="],
+
     "@isaacs/cliui/string-width": ["string-width@5.1.2", "", { "dependencies": { "eastasianwidth": "^0.2.0", "emoji-regex": "^9.2.2", "strip-ansi": "^7.0.1" } }, "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA=="],
 
     "@isaacs/cliui/strip-ansi": ["strip-ansi@7.1.2", "", { "dependencies": { "ansi-regex": "^6.0.1" } }, "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA=="],
diff --git a/frontend/package.json b/frontend/package.json
index 80d0706..acdc47e 100644
--- a/frontend/package.json
+++ b/frontend/package.json
@@ -15,7 +15,22 @@
     "@carbon/icons-react": "^11.76.0",
     "@carbon/react": "^1.102.0",
     "@carbon/styles": "^1.101.0",
+    "@codemirror/commands": "^6.10.2",
+    "@codemirror/lang-css": "^6.3.1",
+    "@codemirror/lang-html": "^6.4.11",
+    "@codemirror/lang-java": "^6.0.2",
+    "@codemirror/lang-javascript": "^6.2.4",
+    "@codemirror/lang-json": "^6.0.2",
+    "@codemirror/lang-sql": "^6.10.0",
+    "@codemirror/lang-xml": "^6.1.0",
+    "@codemirror/language": "^6.12.2",
+    "@codemirror/legacy-modes": "^6.5.2",
+    "@codemirror/rangeset": "^0.19.9",
+    "@codemirror/state": "^6.5.4",
+    "@codemirror/view": "^6.39.15",
+    "@lezer/highlight": "^1.2.3",
     "@wailsio/runtime": "^3.0.0-alpha.79",
+    "codemirror": "^6.0.2",
     "cronstrue": "^3.9.0",
     "diff": "^8.0.2",
     "js-beautify": "^1.15.4",
@@ -26,8 +41,8 @@
     "php-serialize": "^5.1.3",
     "qrcode": "^1.5.4",
     "react": "^18.2.0",
-    "react-router-dom": "^6.29.0",
     "react-dom": "^18.2.0",
+    "react-router-dom": "^6.29.0",
     "sass": "^1.96.0",
     "sql-formatter": "^15.6.11",
     "ulid": "^3.0.2",
diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx
index 17e4ea0..e654e56 100644
--- a/frontend/src/App.jsx
+++ b/frontend/src/App.jsx
@@ -1,8 +1,9 @@
-import React, { useState, useEffect } from 'react';
+import React, { useState, useEffect, useCallback } from 'react';
 import { Routes, Route, Navigate } from 'react-router-dom';
 import './App.css';
 import { Sidebar } from './components/Sidebar';
 import { TitleBar } from './components/TitleBar';
+import { CommandPalette } from './components/CommandPalette';
 import { Theme } from '@carbon/react';
 import ToolRouter from './ToolRouter';
 
@@ -54,10 +55,15 @@ class ErrorBoundary extends React.Component {
 function App() {
   console.log('App mounting');
   const [isSidebarOpen, setIsSidebarOpen] = useState(true);
+  const [isCommandPaletteOpen, setIsCommandPaletteOpen] = useState(false);
   const [theme, setTheme] = useState('g100'); // 'white', 'g10', 'g90', 'g100'
   const [themeMode, setThemeMode] = useState('dark'); // 'system', 'light', 'dark'
 
   const toggleSidebar = () => setIsSidebarOpen(!isSidebarOpen);
+  const toggleCommandPalette = useCallback(() => {
+    setIsCommandPaletteOpen(prev => !prev);
+  }, []);
+  const closeCommandPalette = useCallback(() => setIsCommandPaletteOpen(false), []);
 
   useEffect(() => {
     // Detect System Theme
@@ -84,11 +90,27 @@ function App() {
         e.preventDefault();
         toggleSidebar();
       }
+      // Command palette shortcuts - toggle on/off
+      if ((e.metaKey || e.ctrlKey) && (e.key === 'k' || (e.key === 'p' && e.shiftKey))) {
+        e.preventDefault();
+        toggleCommandPalette();
+      }
     };
 
     window.addEventListener('keydown', handleKeyDown);
     return () => window.removeEventListener('keydown', handleKeyDown);
-  }, [isSidebarOpen]);
+  }, [isSidebarOpen, toggleCommandPalette]);
+
+  // Listen for command palette toggle event from backend (global hotkey)
+  useEffect(() => {
+    const unsubscribe = window.runtime?.EventsOn?.('command-palette:open', () => {
+      toggleCommandPalette();
+    });
+
+    return () => {
+      if (unsubscribe) unsubscribe();
+    };
+  }, [toggleCommandPalette]);
 
   return (
     
@@ -108,12 +130,19 @@ function App() {
               
} /> - } /> + } /> } />
+ +
diff --git a/frontend/src/components/CommandPalette.css b/frontend/src/components/CommandPalette.css new file mode 100644 index 0000000..42007c1 --- /dev/null +++ b/frontend/src/components/CommandPalette.css @@ -0,0 +1,158 @@ +.command-palette-modal { + --command-palette-bg: var(--cds-layer); + --command-palette-border: var(--cds-border-subtle); + --command-palette-text: var(--cds-text-primary); + --command-palette-text-secondary: var(--cds-text-secondary); + --command-palette-accent: var(--cds-interactive); + --command-palette-hover: var(--cds-layer-hover); + --command-palette-selected: var(--cds-layer-selected); +} + +.command-palette-modal .cds--modal-container { + background: var(--command-palette-bg); + border: 1px solid var(--command-palette-border); + border-radius: 8px; + box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.5); + max-width: 600px; + width: 90%; + max-height: 80vh; +} + +.command-palette-modal .cds--modal-header { + padding: 1rem; + border-bottom: 1px solid var(--command-palette-border); + display: flex; + align-items: center; + gap: 0.75rem; +} + +.command-palette-modal .cds--modal-close-button { + display: none; +} + +.command-palette-search-icon { + color: var(--command-palette-text-secondary); + flex-shrink: 0; +} + +.command-palette-input { + flex: 1; +} + +.command-palette-input .cds--text-input { + background: transparent; + border: none; + color: var(--command-palette-text); + font-size: 1rem; + padding: 0; +} + +.command-palette-input .cds--text-input:focus { + outline: none; + box-shadow: none; +} + +.command-palette-input .cds--text-input::placeholder { + color: var(--command-palette-text-secondary); +} + +.command-palette-shortcuts { + display: flex; + gap: 0.5rem; + align-items: center; + flex-shrink: 0; +} + +.command-palette-shortcuts kbd { + background: var(--cds-layer-active); + border: 1px solid var(--command-palette-border); + border-radius: 4px; + padding: 0.125rem 0.375rem; + font-size: 0.75rem; + color: var(--command-palette-text-secondary); + font-family: var(--cds-font-mono); +} + +.command-palette-body { + padding: 0; + max-height: 60vh; + overflow: hidden; +} + +.command-palette-empty { + padding: 2rem; + text-align: center; + color: var(--command-palette-text-secondary); +} + +.command-palette-list { + overflow-y: auto; + max-height: 50vh; +} + +.command-palette-item { + display: flex; + align-items: center; + justify-content: space-between; + padding: 0.75rem 1rem; + cursor: pointer; + transition: background-color 0.15s ease; + border-bottom: 1px solid transparent; +} + +.command-palette-item:hover, +.command-palette-item.selected { + background: var(--command-palette-hover); +} + +.command-palette-item.selected { + background: var(--command-palette-selected); +} + +.command-palette-item-content { + display: flex; + align-items: center; + gap: 0.75rem; + flex: 1; + min-width: 0; +} + +.command-palette-item-icon { + color: var(--command-palette-text-secondary); + flex-shrink: 0; +} + +.command-palette-item-label { + color: var(--command-palette-text); + font-size: 0.875rem; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.command-palette-item-category { + color: var(--command-palette-text-secondary); + font-size: 0.75rem; + text-transform: uppercase; + letter-spacing: 0.025em; + flex-shrink: 0; + margin-left: 1rem; +} + +/* Scrollbar styling */ +.command-palette-list::-webkit-scrollbar { + width: 8px; +} + +.command-palette-list::-webkit-scrollbar-track { + background: transparent; +} + +.command-palette-list::-webkit-scrollbar-thumb { + background: var(--cds-layer-active); + border-radius: 4px; +} + +.command-palette-list::-webkit-scrollbar-thumb:hover { + background: var(--cds-border-subtle); +} diff --git a/frontend/src/components/CommandPalette.jsx b/frontend/src/components/CommandPalette.jsx new file mode 100644 index 0000000..5fa3a15 --- /dev/null +++ b/frontend/src/components/CommandPalette.jsx @@ -0,0 +1,425 @@ +import React, { useState, useEffect, useRef, useCallback } from 'react'; +import { useNavigate } from 'react-router-dom'; +import { Search, Close, Application, Moon, Power } from '@carbon/icons-react'; +import { TextInput, ComposedModal, ModalHeader, ModalBody } from '@carbon/react'; +import './CommandPalette.css'; + +// Command definitions with tool presets +const COMMANDS = [ + // Code Formatter presets + { + id: 'formatter-json', + label: 'Code Formatter > JSON', + path: '/tool/code-formatter?format=json', + category: 'Formatter', + }, + { + id: 'formatter-xml', + label: 'Code Formatter > XML', + path: '/tool/code-formatter?format=xml', + category: 'Formatter', + }, + { + id: 'formatter-html', + label: 'Code Formatter > HTML', + path: '/tool/code-formatter?format=html', + category: 'Formatter', + }, + { + id: 'formatter-sql', + label: 'Code Formatter > SQL', + path: '/tool/code-formatter?format=sql', + category: 'Formatter', + }, + { + id: 'formatter-css', + label: 'Code Formatter > CSS', + path: '/tool/code-formatter?format=css', + category: 'Formatter', + }, + { + id: 'formatter-js', + label: 'Code Formatter > JavaScript', + path: '/tool/code-formatter?format=javascript', + category: 'Formatter', + }, + + // Text Converter - Encoding + { + id: 'converter-base64', + label: 'Text Converter > Base64', + path: '/tool/text-converter?category=Encode%20-%20Decode&method=Base64', + category: 'Converter', + }, + { + id: 'converter-url', + label: 'Text Converter > URL Encode', + path: '/tool/text-converter?category=Encode%20-%20Decode&method=URL', + category: 'Converter', + }, + { + id: 'converter-hex', + label: 'Text Converter > Hex', + path: '/tool/text-converter?category=Encode%20-%20Decode&method=Base16%20(Hex)', + category: 'Converter', + }, + { + id: 'converter-html', + label: 'Text Converter > HTML Entities', + path: '/tool/text-converter?category=Encode%20-%20Decode&method=HTML%20Entities', + category: 'Converter', + }, + + // Text Converter - Hashing + { + id: 'converter-md5', + label: 'Text Converter > MD5', + path: '/tool/text-converter?category=Hash&method=MD5', + category: 'Converter', + }, + { + id: 'converter-sha256', + label: 'Text Converter > SHA-256', + path: '/tool/text-converter?category=Hash&method=SHA-256', + category: 'Converter', + }, + { + id: 'converter-sha512', + label: 'Text Converter > SHA-512', + path: '/tool/text-converter?category=Hash&method=SHA-512', + category: 'Converter', + }, + { + id: 'converter-all-hashes', + label: 'Text Converter > All Hashes', + path: '/tool/text-converter?category=Hash&method=All', + category: 'Converter', + }, + + // Text Converter - Conversions + { + id: 'converter-json-yaml', + label: 'Text Converter > JSON ↔ YAML', + path: '/tool/text-converter?category=Convert&method=JSON%20%E2%86%94%20YAML', + category: 'Converter', + }, + { + id: 'converter-json-xml', + label: 'Text Converter > JSON ↔ XML', + path: '/tool/text-converter?category=Convert&method=JSON%20%E2%86%94%20XML', + category: 'Converter', + }, + { + id: 'converter-markdown-html', + label: 'Text Converter > Markdown ↔ HTML', + path: '/tool/text-converter?category=Convert&method=Markdown%20%E2%86%94%20HTML', + category: 'Converter', + }, + { + id: 'converter-csv-tsv', + label: 'Text Converter > CSV ↔ TSV', + path: '/tool/text-converter?category=Convert&method=CSV%20%E2%86%94%20TSV', + category: 'Converter', + }, + { + id: 'converter-case-swap', + label: 'Text Converter > Case Swap', + path: '/tool/text-converter?category=Convert&method=Case%20Swapping', + category: 'Converter', + }, + + // Direct navigation - no presets + { id: 'jwt', label: 'JWT Debugger', path: '/tool/jwt', category: 'Tools' }, + { id: 'barcode', label: 'Barcode Generator', path: '/tool/barcode', category: 'Tools' }, + { id: 'regexp', label: 'RegExp Tester', path: '/tool/regexp', category: 'Tools' }, + { id: 'cron', label: 'Cron Job Parser', path: '/tool/cron', category: 'Tools' }, + { id: 'diff', label: 'Text Diff Checker', path: '/tool/diff', category: 'Tools' }, + { id: 'number', label: 'Number Converter', path: '/tool/number-converter', category: 'Tools' }, + { id: 'color', label: 'Color Converter', path: '/tool/color-converter', category: 'Tools' }, + { id: 'string', label: 'String Utilities', path: '/tool/string-utilities', category: 'Tools' }, + { + id: 'datetime', + label: 'DateTime Converter', + path: '/tool/datetime-converter', + category: 'Tools', + }, + { id: 'text', label: 'Text Converter', path: '/tool/text-converter', category: 'Tools' }, + + // Data Generator presets (these will be populated dynamically) + { + id: 'data-user', + label: 'Data Generator > User', + path: '/tool/data-generator?preset=User', + category: 'Generator', + }, + { + id: 'data-address', + label: 'Data Generator > Address', + path: '/tool/data-generator?preset=Address', + category: 'Generator', + }, + + // System commands + { + id: 'theme-toggle', + label: 'Toggle Dark Mode', + action: 'toggle-theme', + category: 'System', + icon: Moon, + }, + { + id: 'window-toggle', + label: 'Show/Hide Window', + action: 'toggle-window', + category: 'System', + icon: Application, + }, + { id: 'app-quit', label: 'Quit DevToolbox', action: 'quit', category: 'System', icon: Power }, +]; + +export function CommandPalette({ isOpen, onClose, themeMode, setThemeMode }) { + const navigate = useNavigate(); + const [searchQuery, setSearchQuery] = useState(''); + const [selectedIndex, setSelectedIndex] = useState(0); + const [commands, setCommands] = useState(COMMANDS); + const inputRef = useRef(null); + const listRef = useRef(null); + + // Load recent commands from localStorage + const [recentCommands, setRecentCommands] = useState(() => { + try { + return JSON.parse(localStorage.getItem('commandPaletteRecent')) || []; + } catch { + return []; + } + }); + + // Fuzzy match function - checks if query characters appear in order in target + const fuzzyMatch = (target, query) => { + if (!query) return true; + + const targetLower = target.toLowerCase(); + const queryLower = query.toLowerCase(); + let targetIndex = 0; + let queryIndex = 0; + + while (targetIndex < targetLower.length && queryIndex < queryLower.length) { + if (targetLower[targetIndex] === queryLower[queryIndex]) { + queryIndex++; + } + targetIndex++; + } + + return queryIndex === queryLower.length; + }; + + // Calculate fuzzy match score (lower is better) + const fuzzyScore = (target, query) => { + if (!query) return 0; + + const targetLower = target.toLowerCase(); + const queryLower = query.toLowerCase(); + + // Exact match gets highest priority + if (targetLower === queryLower) return -1000; + + // Starts with query gets high priority + if (targetLower.startsWith(queryLower)) return -100; + + // Word boundary match gets medium priority + const words = targetLower.split(/[\s>]/); + for (let word of words) { + if (word.startsWith(queryLower)) return -50; + } + + // Calculate distance score for fuzzy match + let targetIndex = 0; + let queryIndex = 0; + let score = 0; + let lastMatchIndex = -1; + + while (targetIndex < targetLower.length && queryIndex < queryLower.length) { + if (targetLower[targetIndex] === queryLower[queryIndex]) { + if (lastMatchIndex !== -1) { + // Penalize gaps between matches + score += targetIndex - lastMatchIndex - 1; + } + lastMatchIndex = targetIndex; + queryIndex++; + } + targetIndex++; + } + + // If didn't match all query characters, return high score (bad match) + if (queryIndex < queryLower.length) return 9999; + + return score; + }; + + // Filter commands based on search query + useEffect(() => { + if (!searchQuery.trim()) { + // Show recent commands first when no search query + const recentIds = new Set(recentCommands); + const sortedCommands = [...COMMANDS].sort((a, b) => { + const aRecent = recentIds.has(a.id) ? 1 : 0; + const bRecent = recentIds.has(b.id) ? 1 : 0; + return bRecent - aRecent; + }); + setCommands(sortedCommands); + return; + } + + const query = searchQuery.toLowerCase(); + + // Filter and score commands + const scored = COMMANDS.map(cmd => { + const labelScore = fuzzyScore(cmd.label, query); + const categoryScore = fuzzyScore(cmd.category, query); + const bestScore = Math.min(labelScore, categoryScore); + return { cmd, score: bestScore }; + }).filter(item => item.score < 9999); + + // Sort by score (lower is better) + scored.sort((a, b) => a.score - b.score); + + setCommands(scored.map(item => item.cmd)); + setSelectedIndex(0); + }, [searchQuery, recentCommands]); + + // Reset selection when opening + useEffect(() => { + if (isOpen) { + setSearchQuery(''); + setSelectedIndex(0); + // Focus input after modal opens + setTimeout(() => inputRef.current?.focus(), 100); + } + }, [isOpen]); + + // Save recent command + const saveRecentCommand = useCallback((commandId) => { + setRecentCommands((prev) => { + const updated = [commandId, ...prev.filter((id) => id !== commandId)].slice(0, 10); + localStorage.setItem('commandPaletteRecent', JSON.stringify(updated)); + return updated; + }); + }, []); + + // Execute command + const executeCommand = useCallback( + (command) => { + saveRecentCommand(command.id); + + if (command.action) { + switch (command.action) { + case 'toggle-theme': + setThemeMode(themeMode === 'dark' ? 'light' : 'dark'); + break; + case 'toggle-window': + if (window.wails?.WindowHide) { + window.wails.WindowHide(); + } + break; + case 'quit': + if (window.wails?.Quit) { + window.wails.Quit(); + } + break; + default: + break; + } + } else if (command.path) { + navigate(command.path); + } + + onClose(); + }, + [navigate, onClose, themeMode, setThemeMode, saveRecentCommand] + ); + + // Handle keyboard navigation + const handleKeyDown = useCallback( + (e) => { + if (e.key === 'ArrowDown') { + e.preventDefault(); + setSelectedIndex((prev) => (prev + 1) % commands.length); + } else if (e.key === 'ArrowUp') { + e.preventDefault(); + setSelectedIndex((prev) => (prev - 1 + commands.length) % commands.length); + } else if (e.key === 'Enter') { + e.preventDefault(); + if (commands[selectedIndex]) { + executeCommand(commands[selectedIndex]); + } + } else if (e.key === 'Escape') { + e.preventDefault(); + onClose(); + } + }, + [commands, selectedIndex, executeCommand, onClose] + ); + + // Scroll selected item into view + useEffect(() => { + const selectedElement = listRef.current?.children[selectedIndex]; + if (selectedElement) { + selectedElement.scrollIntoView({ block: 'nearest', behavior: 'smooth' }); + } + }, [selectedIndex]); + + return ( + + + + setSearchQuery(e.target.value)} + hideLabel + className="command-palette-input" + autoComplete="off" + /> +
+ ↑↓ + Enter + Esc +
+
+ + {commands.length === 0 ? ( +
No commands found matching "{searchQuery}"
+ ) : ( +
+ {commands.map((command, index) => { + const Icon = command.icon || null; + return ( +
executeCommand(command)} + onMouseEnter={() => setSelectedIndex(index)} + > +
+ {Icon && } + {command.label} +
+ {command.category} +
+ ); + })} +
+ )} +
+
+ ); +} diff --git a/frontend/src/components/SettingsModal.css b/frontend/src/components/SettingsModal.css new file mode 100644 index 0000000..1f6491b --- /dev/null +++ b/frontend/src/components/SettingsModal.css @@ -0,0 +1,39 @@ +.settings-modal { + --settings-bg: var(--cds-layer); + --settings-border: var(--cds-border-subtle); +} + +.settings-modal .cds--modal-container { + background: var(--settings-bg); + border: 1px solid var(--settings-border); + border-radius: 8px; +} + +.settings-modal-body { + padding: 1.5rem; +} + +.settings-modal-body .cds--form-group { + margin-bottom: 1.5rem; +} + +.settings-modal-body .cds--form-group:last-child { + margin-bottom: 0; +} + +.settings-description { + color: var(--cds-text-secondary); + font-size: 0.875rem; + margin-top: 0.5rem; + margin-left: 1.75rem; + line-height: 1.5; +} + +.settings-modal-body .cds--radio-button-group { + display: flex; + gap: 1rem; +} + +.settings-modal-body .cds--checkbox-label-text { + font-weight: 500; +} diff --git a/frontend/src/components/SettingsModal.jsx b/frontend/src/components/SettingsModal.jsx new file mode 100644 index 0000000..ccb66d2 --- /dev/null +++ b/frontend/src/components/SettingsModal.jsx @@ -0,0 +1,104 @@ +import React, { useState, useEffect } from 'react'; +import { + ComposedModal, + ModalHeader, + ModalBody, + Checkbox, + FormGroup, + RadioButtonGroup, + RadioButton, +} from '@carbon/react'; +import { Settings } from '@carbon/icons-react'; +import { GetCloseMinimizesToTray, SetCloseMinimizesToTray } from '../generated'; +import './SettingsModal.css'; + +export function SettingsModal({ + isOpen, + onClose, + themeMode, + setThemeMode, +}) { + const [closeMinimizesToTray, setCloseMinimizesToTray] = useState(true); + const [isLoading, setIsLoading] = useState(false); + + // Load settings when modal opens + useEffect(() => { + if (isOpen) { + loadSettings(); + } + }, [isOpen]); + + const loadSettings = async () => { + try { + const value = await GetCloseMinimizesToTray(); + console.log('Loaded setting:', value); + setCloseMinimizesToTray(value); + } catch (err) { + console.error('Failed to load settings:', err); + } + }; + + return ( + + } + label="" + title="Application Settings" + /> + + + setThemeMode(value)} + > + + + + + + + + { + // Carbon Checkbox passes the event, we need to get checked from event.target + const checked = event.target.checked; + if (isLoading) { + console.log('Still loading, please wait'); + return; + } + + console.log('Setting close minimizes to:', checked); + setIsLoading(true); + SetCloseMinimizesToTray(checked) + .then(() => { + setCloseMinimizesToTray(checked); + console.log('Setting saved successfully'); + }) + .catch((err) => { + console.error('Failed to save setting:', err); + }) + .finally(() => { + setIsLoading(false); + }); + }} + disabled={isLoading} + id="close-minimizes" + /> +

+ When enabled, clicking the close button will minimize the app to the system tray instead of quitting. +

+
+
+
+ ); +} + +export default SettingsModal; diff --git a/frontend/src/components/Sidebar.jsx b/frontend/src/components/Sidebar.jsx index 578739c..cb1d8c8 100644 --- a/frontend/src/components/Sidebar.jsx +++ b/frontend/src/components/Sidebar.jsx @@ -56,7 +56,7 @@ export function Sidebar({ isVisible }) {
setSearchTerm(e.target.value)} style={{ diff --git a/frontend/src/components/TitleBar.jsx b/frontend/src/components/TitleBar.jsx index 3af320b..ddaa6d3 100644 --- a/frontend/src/components/TitleBar.jsx +++ b/frontend/src/components/TitleBar.jsx @@ -1,6 +1,8 @@ -import React from 'react'; -import { IconButton, OverflowMenu, OverflowMenuItem } from '@carbon/react'; +import React, { useState } from 'react'; +import { IconButton } from '@carbon/react'; import { Menu, Close, Subtract, Maximize, Settings } from '@carbon/icons-react'; +import { SettingsModal } from './SettingsModal'; +import { Minimise, Maximise, Close as WindowClose } from '../generated'; export function TitleBar({ isSidebarOpen, @@ -9,8 +11,9 @@ export function TitleBar({ themeMode, setThemeMode, }) { + const [isSettingsOpen, setIsSettingsOpen] = useState(false); // Detect if running in desktop mode - const isDesktop = typeof window !== 'undefined' && window.wails; + const isDesktop = typeof window !== 'undefined' && window.go?.devtoolbox?.service?.WindowControls; // Detect platform const userAgent = navigator.userAgent.toLowerCase(); @@ -23,21 +26,27 @@ export function TitleBar({ } // Window control handlers for non-macOS platforms - const handleMinimize = () => { - if (window.wails?.WindowMinimise) { - window.wails.WindowMinimise(); + const handleMinimize = async () => { + try { + await Minimise(); + } catch (err) { + console.error('Failed to minimise window:', err); } }; - const handleMaximize = () => { - if (window.wails?.WindowMaximise) { - window.wails.WindowMaximise(); + const handleMaximize = async () => { + try { + await Maximise(); + } catch (err) { + console.error('Failed to maximise window:', err); } }; - const handleClose = () => { - if (window.wails?.Quit) { - window.wails.Quit(); + const handleClose = async () => { + try { + await WindowClose(); + } catch (err) { + console.error('Failed to close window:', err); } }; @@ -64,32 +73,16 @@ export function TitleBar({ {/* Right section - Settings + Window controls */}
- {/* Settings menu */} - setIsSettingsOpen(true)} + label="Settings" className="titlebar-settings" > - setThemeMode('system')} - requireTitle - /> - setThemeMode('dark')} - requireTitle - /> - setThemeMode('light')} - requireTitle - /> - + + {/* Window controls for non-macOS platforms */} {!isMac && ( @@ -123,6 +116,14 @@ export function TitleBar({
)} + + {/* Settings Modal */} + setIsSettingsOpen(false)} + themeMode={themeMode} + setThemeMode={setThemeMode} + />
); diff --git a/frontend/src/components/ToolUI.jsx b/frontend/src/components/ToolUI.jsx index 3e79202..b57da13 100644 --- a/frontend/src/components/ToolUI.jsx +++ b/frontend/src/components/ToolUI.jsx @@ -5,7 +5,7 @@ import { Copy } from '@carbon/icons-react'; // Re-export new layout components export { ToolLayout, ToolLayoutToggle, ToolVerticalSplit } from './layout'; export { LAYOUT_DIRECTIONS, TOGGLE_POSITIONS } from './layout/constants'; -export { ToolCopyButton, ToolTextArea, ToolInput, ToolInputGroup, ToolTabBar } from './inputs'; +export { ToolCopyButton, ToolTextArea, ToolInput, ToolInputGroup, ToolTabBar, CodeEditor, HighlightedCode, EditorToggle } from './inputs'; export function ToolHeader({ title, description }) { return ( diff --git a/frontend/src/components/inputs/CodeEditor.jsx b/frontend/src/components/inputs/CodeEditor.jsx new file mode 100644 index 0000000..c7c54a5 --- /dev/null +++ b/frontend/src/components/inputs/CodeEditor.jsx @@ -0,0 +1,282 @@ +import React, { useEffect, useRef, useState } from 'react'; +import { EditorView, keymap, lineNumbers } from '@codemirror/view'; +import { EditorState } from '@codemirror/state'; +import { defaultKeymap } from '@codemirror/commands'; +import { carbonCodeMirrorExtension } from './carbonCodeMirrorTheme'; +import { createSQLKeywordHighlighter } from './sqlHighlighter'; +import { TextArea } from '@carbon/react'; + +/** + * Maps language names to CodeMirror language modules + */ +const languageLoaders = { + json: () => import('@codemirror/lang-json').then((m) => m.json()), + javascript: () => import('@codemirror/lang-javascript').then((m) => m.javascript()), + typescript: () => + import('@codemirror/lang-javascript').then((m) => m.javascript({ typescript: true })), + html: () => import('@codemirror/lang-html').then((m) => m.html()), + xml: () => import('@codemirror/lang-xml').then((m) => m.xml()), + css: () => import('@codemirror/lang-css').then((m) => m.css()), + sql: () => import('@codemirror/lang-sql').then((m) => m.sql()), + java: () => import('@codemirror/lang-java').then((m) => m.java()), + swift: () => + import('@codemirror/legacy-modes/mode/swift').then((m) => + import('@codemirror/language').then((lang) => lang.StreamLanguage.define(m.swift)) + ), +}; + +/** + * Loads a CodeMirror language extension dynamically + */ +async function loadLanguageExtension(language) { + const loader = languageLoaders[language.toLowerCase()]; + if (!loader) { + console.warn(`Language "${language}" not supported for syntax highlighting`); + return null; + } + try { + return await loader(); + } catch (err) { + console.warn(`Failed to load language "${language}":`, err); + return null; + } +} + +/** + * Editable code editor with syntax highlighting + * Falls back to Carbon TextArea when highlighting is disabled + * + * @param {string} value - Editor content + * @param {function} onChange - Callback when content changes: (value) => void + * @param {string} language - Programming language for syntax highlighting + * @param {boolean} highlight - Whether to show syntax highlighting + * @param {boolean} [readOnly=false] - Read-only mode + * @param {string} [placeholder] - Placeholder text + * @param {string} [label] - Label for the field + * @param {boolean} [showLineNumbers=false] - Show line numbers + * @param {string} [className] - Optional CSS class + * @param {object} [style] - Optional inline styles + */ +export default function CodeEditor({ + value = '', + onChange, + language, + highlight = true, + readOnly = false, + placeholder, + label, + showLineNumbers = false, + className = '', + style = {}, +}) { + const containerRef = useRef(null); + const viewRef = useRef(null); + const [isLoading, setIsLoading] = useState(false); + const [loadError, setLoadError] = useState(false); + const onChangeRef = useRef(onChange); + const valueRef = useRef(value); + const languageRef = useRef(language); + const readOnlyRef = useRef(readOnly); + const showLineNumbersRef = useRef(showLineNumbers); + + // Keep refs in sync + useEffect(() => { + onChangeRef.current = onChange; + valueRef.current = value; + languageRef.current = language; + readOnlyRef.current = readOnly; + showLineNumbersRef.current = showLineNumbers; + }); + + // Initialize CodeMirror editor - runs once when highlight becomes true + useEffect(() => { + if (!highlight || !containerRef.current || viewRef.current) return; + + let isCancelled = false; + + const initEditor = async () => { + try { + setIsLoading(true); + setLoadError(false); + + // Load language support + const langExtension = await loadLanguageExtension(languageRef.current); + if (isCancelled) return; + + const extensions = [ + ...carbonCodeMirrorExtension, + keymap.of(defaultKeymap), + EditorView.updateListener.of((update) => { + if (update.docChanged && onChangeRef.current) { + onChangeRef.current(update.state.doc.toString()); + } + }), + ]; + + if (readOnlyRef.current) { + extensions.push(EditorState.readOnly.of(true)); + extensions.push(EditorView.editable.of(false)); + } + + if (langExtension) { + extensions.push(langExtension); + } + + // Add SQL keyword categorization if language is SQL + if (languageRef.current?.toLowerCase() === 'sql') { + extensions.push(createSQLKeywordHighlighter()); + } + + if (showLineNumbersRef.current) { + extensions.push(lineNumbers()); + } + + const state = EditorState.create({ + doc: valueRef.current, + extensions, + }); + + const view = new EditorView({ + state, + parent: containerRef.current, + }); + + if (!isCancelled) { + viewRef.current = view; + setIsLoading(false); + } else { + view.destroy(); + } + } catch (err) { + if (!isCancelled) { + console.error('Failed to initialize code editor:', err); + setLoadError(true); + setIsLoading(false); + } + } + }; + + initEditor(); + + return () => { + isCancelled = true; + }; + }, [highlight]); + + // Cleanup on unmount + useEffect(() => { + return () => { + if (viewRef.current) { + viewRef.current.destroy(); + viewRef.current = null; + } + }; + }, []); + + // Destroy when highlight becomes false + useEffect(() => { + if (!highlight && viewRef.current) { + viewRef.current.destroy(); + viewRef.current = null; + } + }, [highlight]); + + // Update content when value prop changes (but NOT during initial render) + useEffect(() => { + const view = viewRef.current; + if (!view) return; + + const currentContent = view.state.doc.toString(); + if (value !== currentContent) { + const transaction = view.state.update({ + changes: { + from: 0, + to: view.state.doc.length, + insert: value, + }, + }); + view.dispatch(transaction); + } + }, [value]); + + // Common container style for both highlighted and fallback modes + const containerStyle = { + display: 'flex', + flexDirection: 'column', + height: '100%', + minHeight: '120px', + border: '1px solid var(--cds-border-strong)', + backgroundColor: 'var(--cds-field)', + position: 'relative', + overflow: 'hidden', + ...style, + }; + + const labelStyle = { + fontSize: '0.75rem', + fontWeight: 400, + textTransform: 'uppercase', + letterSpacing: '0.32px', + color: 'var(--cds-text-secondary)', + padding: '0.5rem 1rem', + borderBottom: '1px solid var(--cds-border-subtle)', + backgroundColor: 'var(--cds-layer)', + flexShrink: 0, + }; + + // Fallback to TextArea when highlighting is disabled or failed to load + if (!highlight || loadError) { + return ( +
+ {label &&
{label}
} +
+