diff --git a/.agents/skills/fdb-customization-system/SKILL.md b/.agents/skills/quell-customization-system/SKILL.md similarity index 90% rename from .agents/skills/fdb-customization-system/SKILL.md rename to .agents/skills/quell-customization-system/SKILL.md index c1acf47..49b8be1 100644 --- a/.agents/skills/fdb-customization-system/SKILL.md +++ b/.agents/skills/quell-customization-system/SKILL.md @@ -1,14 +1,14 @@ ---- -name: fdb-customization-system +--- +name: quell-customization-system description: >- - Architectural patterns, procedural runbook, and token catalog for Fdb Studio's customization and color system. - Use when adding, modifying, or extending UI customization, Color Maker (رنگ‌ساز), theme tokens, button/border/sidebar styling, - or SQLite persistence of user preferences in Fdb. + Architectural patterns, procedural runbook, and token catalog for Quell's customization and color system. + Use when adding, modifying, or extending UI customization, Color Maker, theme tokens, button/border/sidebar styling, + or SQLite persistence of user preferences in Quell. --- -# Fdb Studio Customization & Color System Skill +# Quell Customization & Color System Skill -This skill defines the architectural blueprint, token taxonomy, and step-by-step procedures for extending Fdb Studio's UI customization, Color Maker ("رنگ‌ساز"), theme brushes, and SQLite configuration engine. +This skill defines the architectural blueprint, token taxonomy, and step-by-step procedures for extending Quell's UI customization, Color Maker, theme brushes, and SQLite configuration engine. Follow this standard whenever you add new customizable elements, modify existing colors, or implement theme tokens. @@ -16,7 +16,7 @@ Follow this standard whenever you add new customizable elements, modify existing ## 1. Architectural Overview (The 4-Layer System) -Fdb Studio's customization engine operates across four synchronized layers: +Quell's customization engine operates across four synchronized layers: ``` ┌────────────────────────────────────────────────────────┐ @@ -133,7 +133,7 @@ public void SelectMyElementColor(string hex) } ``` -### Step 6: Register in the Color Maker ("رنگ‌ساز") +### Step 6: Register in the Color Maker In `ViewModels/SettingsModalViewModel.cs`: - Add `"My Element Category: Target Name"` to `ColorMakerTargets`. - In `ApplyColorMakerToTarget()`, add the switch case: @@ -190,6 +190,11 @@ Add an assertion in `VerifyLayoutAndPanelsAsync()` verifying that the property p | `AppTableGridLinesHorizontalBrush`| `#CBD5E1`| `#334155` | `GridLinesColor` | Horizontal data grid lines | | `AppTableGridLinesVerticalBrush` | `#E2E8F0` | `#334155` | `GridLinesColor` | Vertical data grid lines | | `AppTableSelectedBackgroundBrush`| `#DBEAFE` | `#1E3A5F` | `SelectionColor` | Selected row / cell highlight | +| `AppTerminalBackgroundBrush` | `#FFFFFF` | `#0F172A` | `TerminalBackground` | Integrated Terminal emulator background | +| `AppTerminalForegroundBrush` | `#0F172A` | `#F8FAFC` | `TerminalForeground` | Integrated Terminal output text | +| `AppTerminalToolbarBackgroundBrush`| `#F8FAFC` | `#1E293B` | Derived from Theme / `AppSurfaceBrush` | Terminal session bar background | +| `AppTerminalToolbarBorderBrush`| `#E2E8F0` | `#334155` | Derived from Theme / `AppBorderBrush` | Terminal session bar border | +| `AppTerminalStatusBackgroundBrush`| `#F8FAFC` | `#0B0F17` | Derived from Theme / `AppSurfaceBrush` | Terminal bottom status breadcrumb bar | --- @@ -199,7 +204,7 @@ Always run these two commands before committing any customization or styling cha ```powershell # 1. Compile in Release mode (must be 0 warnings, 0 errors) -dotnet build Fdb.csproj --configuration Release +dotnet build Quell.csproj --configuration Release # 2. Run automated headless verification suite dotnet run --configuration Release --no-build -- --verify diff --git a/.agents/skills/fdb-customization-system/references/color_maker_architecture.md b/.agents/skills/quell-customization-system/references/color_maker_architecture.md similarity index 98% rename from .agents/skills/fdb-customization-system/references/color_maker_architecture.md rename to .agents/skills/quell-customization-system/references/color_maker_architecture.md index c274a1a..c75e723 100644 --- a/.agents/skills/fdb-customization-system/references/color_maker_architecture.md +++ b/.agents/skills/quell-customization-system/references/color_maker_architecture.md @@ -1,6 +1,6 @@ -# Color Maker ("رنگ‌ساز") Architecture +# Color Maker Architecture -The Color Maker allows users to build custom colors via RGB sliders or direct HEX text input and apply them to any UI target in Fdb Studio. +The Color Maker allows users to build custom colors via RGB sliders or direct HEX text input and apply them to any UI target in Quell. ## 1. Bidirectional Synchronization Architecture diff --git a/.agents/skills/fdb-customization-system/references/sqlite_schema.md b/.agents/skills/quell-customization-system/references/sqlite_schema.md similarity index 94% rename from .agents/skills/fdb-customization-system/references/sqlite_schema.md rename to .agents/skills/quell-customization-system/references/sqlite_schema.md index f688159..0458404 100644 --- a/.agents/skills/fdb-customization-system/references/sqlite_schema.md +++ b/.agents/skills/quell-customization-system/references/sqlite_schema.md @@ -1,6 +1,6 @@ -# SQLite Customization Storage Schema +# SQLite Customization Storage Schema -Fdb Studio persists all user appearance preferences, colors, shortcuts, and panel dimensions in SQLite using a lightweight, key-value `settings` table. +Quell persists all user appearance preferences, colors, shortcuts, and panel dimensions in SQLite using a lightweight, key-value `settings` table. ## 1. Table Schema diff --git a/.agents/skills/fdb-customization-system/references/theme_tokens.md b/.agents/skills/quell-customization-system/references/theme_tokens.md similarity index 99% rename from .agents/skills/fdb-customization-system/references/theme_tokens.md rename to .agents/skills/quell-customization-system/references/theme_tokens.md index e273f3c..8c4648a 100644 --- a/.agents/skills/fdb-customization-system/references/theme_tokens.md +++ b/.agents/skills/quell-customization-system/references/theme_tokens.md @@ -1,4 +1,4 @@ -# Fdb Studio Theme Tokens Reference +# Quell Theme Tokens Reference This document catalogs all XAML color and brush resources defined in the design system, their fallback defaults, and their relationships with `ThemeService`. diff --git a/.github/workflows/ci-cd.yml b/.github/workflows/ci-cd.yml index e4bd399..b32357b 100644 --- a/.github/workflows/ci-cd.yml +++ b/.github/workflows/ci-cd.yml @@ -31,10 +31,10 @@ jobs: dotnet-version: '10.0.x' - name: Restore Dependencies - run: dotnet restore Fdb.csproj + run: dotnet restore Quell.csproj - name: Build Project - run: dotnet build Fdb.csproj --configuration Release --no-restore + run: dotnet build Quell.csproj --configuration Release --no-restore - name: Execute Verification Suite (--verify) run: dotnet run --configuration Release --no-build -- --verify @@ -57,7 +57,7 @@ jobs: - name: Publish Windows Standalone Executable (win-x64) run: > - dotnet publish Fdb.csproj + dotnet publish Quell.csproj -c Release -r win-x64 --self-contained true @@ -78,7 +78,7 @@ jobs: - name: Create Portable Zip Package shell: pwsh run: | - Compress-Archive -Path ./publish/win-x64/* -DestinationPath ./dist/windows/Fdb-Studio-1.0.0-win-x64-portable.zip -Force + Compress-Archive -Path ./publish/win-x64/* -DestinationPath ./dist/windows/Quell-1.0.0-win-x64-portable.zip -Force - name: Upload Windows Artifacts uses: actions/upload-artifact@v4 @@ -105,7 +105,7 @@ jobs: - name: Publish Linux Standalone Executable (linux-x64) run: > - dotnet publish Fdb.csproj + dotnet publish Quell.csproj -c Release -r linux-x64 --self-contained true @@ -120,7 +120,7 @@ jobs: - name: Create Portable .tar.gz Package run: | mkdir -p dist/linux - tar -czvf dist/linux/Fdb-Studio-1.0.0-linux-x64-portable.tar.gz -C publish/linux-x64 . + tar -czvf dist/linux/Quell-1.0.0-linux-x64-portable.tar.gz -C publish/linux-x64 . - name: Upload Linux Artifacts uses: actions/upload-artifact@v4 @@ -150,7 +150,7 @@ jobs: - name: Publish macOS App Binaries (${{ matrix.rid }}) run: > - dotnet publish Fdb.csproj + dotnet publish Quell.csproj -c Release -r ${{ matrix.rid }} --self-contained true @@ -218,11 +218,11 @@ jobs: if [[ "${{ github.ref }}" == refs/tags/* ]]; then TAG_NAME="${GITHUB_REF#refs/tags/}" echo "tag=$TAG_NAME" >> "$GITHUB_OUTPUT" - echo "title=Fdb Studio $TAG_NAME" >> "$GITHUB_OUTPUT" + echo "title=Quell $TAG_NAME" >> "$GITHUB_OUTPUT" echo "prerelease=false" >> "$GITHUB_OUTPUT" else echo "tag=latest" >> "$GITHUB_OUTPUT" - echo "title=Fdb Studio Continuous Build" >> "$GITHUB_OUTPUT" + echo "title=Quell Continuous Build" >> "$GITHUB_OUTPUT" echo "prerelease=false" >> "$GITHUB_OUTPUT" fi diff --git a/App.axaml b/App.axaml index a2f32cb..27d42b4 100644 --- a/App.axaml +++ b/App.axaml @@ -1,8 +1,8 @@ - + x:Class="Quell.App"> @@ -11,12 +11,12 @@ - + - - - + + + @@ -25,6 +25,6 @@ - + diff --git a/App.axaml.cs b/App.axaml.cs index 4e3f587..fca489a 100644 --- a/App.axaml.cs +++ b/App.axaml.cs @@ -1,13 +1,14 @@ +using System; using System.Linq; using System.Threading.Tasks; using Avalonia; using Avalonia.Controls.ApplicationLifetimes; using Avalonia.Markup.Xaml; using Avalonia.Threading; -using Fdb.ViewModels; -using Fdb.Views; +using Quell.ViewModels; +using Quell.Views; -namespace Fdb; +namespace Quell; public partial class App : Application { @@ -39,20 +40,50 @@ public override void OnFrameworkInitializationCompleted() _ = Task.Run(async () => { - // Execute startup SQL queries and initialize SQLite engine - await splash.RunStartupSequenceAsync(); - - await Dispatcher.UIThread.InvokeAsync(() => + try { - var mainWindow = new MainWindow + // 1. Real Phase: Storage, schema, graphics, appearance, shortcuts & keywords (0% -> 88%) + await splash.RunStartupSequenceAsync(completeTo100: false); + + // 2. Real Phase: Constructing Studio Workspace (89% -> 94%) + splash.UpdateProgress(89, "Building Studio Workspace...", "Initializing SQL Editor & IntelliSense engine..."); + + await Dispatcher.UIThread.InvokeAsync(async () => { - DataContext = new MainViewModel(), - }; + try + { + var vm = new MainViewModel(); + + // 3. Real Phase: Building MainWindow & UI Shell (95% -> 99%) + splash.UpdateProgress(95, "Rendering Desktop Interface...", "Binding controls, layout grids and tabs..."); + await Task.Delay(45); - desktop.MainWindow = mainWindow; - mainWindow.Show(); - splash.Close(); - }); + var mainWindow = new MainWindow + { + DataContext = vm, + }; + + // 4. Real Phase: Finalizing Studio Environment (100%) + splash.UpdateProgress(100, "Studio Environment Ready", "Quell v1.0.0 Ready"); + await Task.Delay(140); + + desktop.MainWindow = mainWindow; + mainWindow.Show(); + splash.Close(); + } + catch (Exception uiEx) + { + Console.WriteLine($"[App Startup UI Error]: {uiEx}"); + splash.Close(); + throw; + } + }); + } + catch (Exception ex) + { + Console.WriteLine($"[App Startup Error]: {ex}"); + await Dispatcher.UIThread.InvokeAsync(() => splash.Close()); + } }); } } diff --git a/Assets/Images/quell_splash.jpg b/Assets/Images/quell_splash.jpg new file mode 100644 index 0000000..9267140 Binary files /dev/null and b/Assets/Images/quell_splash.jpg differ diff --git a/Assets/Images/splash_screen.jpg b/Assets/Images/splash_screen.jpg index e7d2ae6..9267140 100644 Binary files a/Assets/Images/splash_screen.jpg and b/Assets/Images/splash_screen.jpg differ diff --git a/Assets/init_database.sql b/Assets/init_database.sql index 11c65c7..745ec7a 100644 --- a/Assets/init_database.sql +++ b/Assets/init_database.sql @@ -1,5 +1,5 @@ --- ============================================================================ --- Fdb Studio: SQLite Database Startup & Dialect Keywords Initialization Script +-- ============================================================================ +-- Quell: SQLite Database Startup & Dialect Keywords Initialization Script -- ============================================================================ -- [STEP 1/4] Core Schema & Tables diff --git a/Assets/quell_icon.ico b/Assets/quell_icon.ico new file mode 100644 index 0000000..79a9ac0 Binary files /dev/null and b/Assets/quell_icon.ico differ diff --git a/Assets/quell_icon.png b/Assets/quell_icon.png new file mode 100644 index 0000000..d7a65d1 Binary files /dev/null and b/Assets/quell_icon.png differ diff --git a/COMPONENTS.md b/COMPONENTS.md index 199b785..648458b 100644 --- a/COMPONENTS.md +++ b/COMPONENTS.md @@ -1,10 +1,10 @@ -# Fdb Studio — Reusable UI Components & Integration Guide +# Quell — Reusable UI Components & Integration Guide ## 1. Overview & Architecture -**Fdb Studio** is an enterprise-grade database management desktop IDE built with **.NET 10**, **Avalonia UI 12.1.1**, **FluentAvalonia 3.1.0**, **AvaloniaEdit 12.0.0**, and **Dapper 2.1.89**. +**Quell** is an enterprise-grade database management desktop IDE built with **.NET 10**, **Avalonia UI 12.1.1**, **FluentAvalonia 3.1.0**, **AvaloniaEdit 12.0.0**, and **Dapper 2.1.89**. -The application is structured around a **strictly modular, reusable component architecture**. Visual elements are isolated into self-contained `UserControl` components residing in the `Fdb.Components` namespace, accompanied by dedicated MVVM ViewModels, SQLite persistent services, and Microsoft Fluent vector iconography. +The application is structured around a **strictly modular, reusable component architecture**. Visual elements are isolated into self-contained `UserControl` components residing in the `Quell.Components` namespace, accompanied by dedicated MVVM ViewModels, SQLite persistent services, and Microsoft Fluent vector iconography. ### Core Principles 1. **Zero Emojis / Native Vector Icons:** All graphical glyphs strictly utilize Microsoft Fluent vector symbols (`FluentAvalonia.UI.Controls.FASymbolIcon`) and `PathIcon` vector geometries. @@ -38,13 +38,18 @@ Components/ │ └── SettingsModalComponent.axaml # Composed preferences dialog (Theme + Syntax Colors) │ ├── Settings Sub-Components: -│ ├── SettingsCategoriesNavComponent.axaml # Reusable categories navigation sidebar for settings (Appearance, Results Table, Customization, Shortcuts) +│ ├── SettingsCategoriesNavComponent.axaml # Reusable categories navigation sidebar for settings (Appearance, Results Table, Customization, Shortcuts, Terminal) │ ├── ThemeSelectionCardsComponent.axaml # Reusable Light, Dark, and System theme selection cards │ ├── SyntaxColorSelectorComponent.axaml # Reusable 6-preset SQL syntax keyword color palette │ ├── LiveSyntaxPreviewComponent.axaml # Reusable live code snippet preview with dynamic syntax highlighting │ ├── TableCustomizationComponent.axaml # Dedicated Query Results Table row height, font size, gutter, colors, and live preview │ ├── PanelCustomizationComponent.axaml # Custom Color Maker (RGB/Hex), Buttons, Global Borders, Sidebar multi-part, Editor, and Quick Bundles with live layout preview -│ └── ShortcutsSettingsComponent.axaml # Dynamic keyboard shortcuts manager with SQLite persistence and key capture +│ ├── ShortcutsSettingsComponent.axaml # Dynamic keyboard shortcuts manager with SQLite persistence and key capture +│ └── TerminalSettingsComponent.axaml # Terminal emulator shell, typography, and database CLI binary overrides +│ +├── Terminal & Command Line: +│ ├── TerminalComponent.axaml # Multi-session tabbed PTY terminal with shell dropdown & CLI launcher +│ └── TerminalSessionView.axaml # Interactive terminal control host with missing CLI detection banner │ ├── Themes & Design System Components: │ ├── Themes/ThemeTokens.axaml # Componentized shared design metrics & typography @@ -73,13 +78,13 @@ This section provides comprehensive documentation and copy-pasteable code exampl ```xml -xmlns:components="using:Fdb.Components" +xmlns:components="using:Quell.Components" ``` --- ### 3.1 `SqlIntelliSenseOverlayComponent` -* **File:** [SqlIntelliSenseOverlayComponent.axaml](file:///c:/Users/ehsan/Desktop/Fdb/Components/SqlIntelliSenseOverlayComponent.axaml) | [SqlIntelliSenseOverlayComponent.axaml.cs](file:///c:/Users/ehsan/Desktop/Fdb/Components/SqlIntelliSenseOverlayComponent.axaml.cs) +* **File:** [SqlIntelliSenseOverlayComponent.axaml](file:///c:/Users/ehsan/Desktop/Quell/Components/SqlIntelliSenseOverlayComponent.axaml) | [SqlIntelliSenseOverlayComponent.axaml.cs](file:///c:/Users/ehsan/Desktop/Quell/Components/SqlIntelliSenseOverlayComponent.axaml.cs) * **Description:** A floating IntelliSense suggestion card featuring SQL keyword prioritization (e.g., `SELECT` ranking #1 on typing 'S'), code snippet templates, vector icons, keyboard navigation, and live documentation previews. #### Public API: @@ -156,7 +161,7 @@ private void OnIntelliSenseItemCommitted(object? sender, SqlCompletionData item) --- ### 3.2 `EditorComponent` -* **File:** [EditorComponent.axaml](file:///c:/Users/ehsan/Desktop/Fdb/Components/EditorComponent.axaml) | [EditorComponent.axaml.cs](file:///c:/Users/ehsan/Desktop/Fdb/Components/EditorComponent.axaml.cs) +* **File:** [EditorComponent.axaml](file:///c:/Users/ehsan/Desktop/Quell/Components/EditorComponent.axaml) | [EditorComponent.axaml.cs](file:///c:/Users/ehsan/Desktop/Quell/Components/EditorComponent.axaml.cs) * **DataContext:** `EditorViewModel` * **Description:** A multi-tab SQL document editor featuring isolated per-tab undo/redo history, auto-expanding uniform tabs, blue syntax highlighting (`#2563EB`), zooming shortcuts, and built-in IntelliSense autocomplete. @@ -191,7 +196,7 @@ editorComponent.ResetZoom(); --- ### 3.3 `QueryResultTableComponent` -* **File:** [QueryResultTableComponent.axaml](file:///c:/Users/ehsan/Desktop/Fdb/Components/QueryResultTableComponent.axaml) | [QueryResultTableComponent.axaml.cs](file:///c:/Users/ehsan/Desktop/Fdb/Components/QueryResultTableComponent.axaml.cs) +* **File:** [QueryResultTableComponent.axaml](file:///c:/Users/ehsan/Desktop/Quell/Components/QueryResultTableComponent.axaml) | [QueryResultTableComponent.axaml.cs](file:///c:/Users/ehsan/Desktop/Quell/Components/QueryResultTableComponent.axaml.cs) * **DataContext:** `QueryResultTableViewModel` * **Description:** An Excel-grade, high-performance database query results grid with compact 25px row heights, dynamic alternating column backgrounds, an explicit `ROW #` gutter column, quick search filter, execution metrics, and full data export capabilities. Fully adapted to both Light and Dark themes via semantic brush bindings without visual bugs or contrast issues. @@ -229,7 +234,7 @@ var tableControl = new QueryResultTableComponent { DataContext = tableVm }; --- ### 3.4 `SchemaPropertiesTableComponent` -* **File:** [SchemaPropertiesTableComponent.axaml](file:///c:/Users/ehsan/Desktop/Fdb/Components/SchemaPropertiesTableComponent.axaml) | [SchemaPropertiesTableComponent.axaml.cs](file:///c:/Users/ehsan/Desktop/Fdb/Components/SchemaPropertiesTableComponent.axaml.cs) +* **File:** [SchemaPropertiesTableComponent.axaml](file:///c:/Users/ehsan/Desktop/Quell/Components/SchemaPropertiesTableComponent.axaml) | [SchemaPropertiesTableComponent.axaml.cs](file:///c:/Users/ehsan/Desktop/Quell/Components/SchemaPropertiesTableComponent.axaml.cs) * **DataContext:** `SchemaPropertiesTableViewModel` * **Description:** Interactive table schema metadata viewer showing primary keys, foreign keys, data types, nullability, default values, and column descriptions with compact 25px alternating rows. @@ -261,7 +266,7 @@ var schemaControl = new SchemaPropertiesTableComponent { DataContext = schemaVm --- ### 3.5 `ModalOverlayComponent` (Universal Modal Base Card) -* **File:** [ModalOverlayComponent.axaml](file:///c:/Users/ehsan/Desktop/Fdb/Components/ModalOverlayComponent.axaml) | [ModalOverlayComponent.axaml.cs](file:///c:/Users/ehsan/Desktop/Fdb/Components/ModalOverlayComponent.axaml.cs) +* **File:** [ModalOverlayComponent.axaml](file:///c:/Users/ehsan/Desktop/Quell/Components/ModalOverlayComponent.axaml) | [ModalOverlayComponent.axaml.cs](file:///c:/Users/ehsan/Desktop/Quell/Components/ModalOverlayComponent.axaml.cs) * **Description:** The foundational reusable modal card supporting arbitrary custom content via `ModalContent`, an interactive backdrop dismissal, keyboard `Escape` handling, and elevated `ZIndex`. #### Styled Properties: @@ -298,7 +303,7 @@ var schemaControl = new SchemaPropertiesTableComponent { DataContext = schemaVm --- ### 3.6 `ConnectDatabaseModalComponent` -* **File:** [ConnectDatabaseModalComponent.axaml](file:///c:/Users/ehsan/Desktop/Fdb/Components/ConnectDatabaseModalComponent.axaml) | [ConnectDatabaseModalComponent.axaml.cs](file:///c:/Users/ehsan/Desktop/Fdb/Components/ConnectDatabaseModalComponent.axaml.cs) +* **File:** [ConnectDatabaseModalComponent.axaml](file:///c:/Users/ehsan/Desktop/Quell/Components/ConnectDatabaseModalComponent.axaml) | [ConnectDatabaseModalComponent.axaml.cs](file:///c:/Users/ehsan/Desktop/Quell/Components/ConnectDatabaseModalComponent.axaml.cs) * **DataContext:** `ConnectDatabaseModalViewModel` * **Description:** Dedicated SQL Server connection dialog supporting host, port, database, authentication modes, encryption options, and live connection testing with status feedback. @@ -320,7 +325,7 @@ Console.WriteLine(mainVm.ConnectModal.StatusMessage); --- ### 3.7 `ConfirmActionModalComponent` -* **File:** [ConfirmActionModalComponent.axaml](file:///c:/Users/ehsan/Desktop/Fdb/Components/ConfirmActionModalComponent.axaml) | [ConfirmActionModalComponent.axaml.cs](file:///c:/Users/ehsan/Desktop/Fdb/Components/ConfirmActionModalComponent.axaml.cs) +* **File:** [ConfirmActionModalComponent.axaml](file:///c:/Users/ehsan/Desktop/Quell/Components/ConfirmActionModalComponent.axaml) | [ConfirmActionModalComponent.axaml.cs](file:///c:/Users/ehsan/Desktop/Quell/Components/ConfirmActionModalComponent.axaml.cs) * **DataContext:** `ConfirmActionModalViewModel` * **Description:** Danger alert dialog for destructive actions (`Disconnect`, `DROP`, `TRUNCATE`) featuring red warning accents, target resource tags, and confirmation commands. @@ -342,7 +347,7 @@ mainVm.ConfirmModal.Open( --- ### 3.8 `ExportDataModalComponent` -* **File:** [ExportDataModalComponent.axaml](file:///c:/Users/ehsan/Desktop/Fdb/Components/ExportDataModalComponent.axaml) | [ExportDataModalComponent.axaml.cs](file:///c:/Users/ehsan/Desktop/Fdb/Components/ExportDataModalComponent.axaml.cs) +* **File:** [ExportDataModalComponent.axaml](file:///c:/Users/ehsan/Desktop/Quell/Components/ExportDataModalComponent.axaml) | [ExportDataModalComponent.axaml.cs](file:///c:/Users/ehsan/Desktop/Quell/Components/ExportDataModalComponent.axaml.cs) * **DataContext:** `ExportDataModalViewModel` * **Description:** Data export configuration dialog supporting CSV, JSON, SQL INSERT, and TSV formats with native OS file dialog integration. @@ -354,7 +359,7 @@ mainVm.ConfirmModal.Open( --- ### 3.9 `SettingsModalComponent` (Appearance, Theme & Syntax Colors) -* **File:** [SettingsModalComponent.axaml](file:///c:/Users/ehsan/Desktop/Fdb/Components/SettingsModalComponent.axaml) | [SettingsModalComponent.axaml.cs](file:///c:/Users/ehsan/Desktop/Fdb/Components/SettingsModalComponent.axaml.cs) +* **File:** [SettingsModalComponent.axaml](file:///c:/Users/ehsan/Desktop/Quell/Components/SettingsModalComponent.axaml) | [SettingsModalComponent.axaml.cs](file:///c:/Users/ehsan/Desktop/Quell/Components/SettingsModalComponent.axaml.cs) * **DataContext:** `SettingsModalViewModel` * **Description:** Application preferences dialog featuring real-time color theme switching (`Light`, `Dark`, `System Default`) and SQL syntax keyword color customization, backed by SQLite persistent storage (`AppSettings` table via Dapper) and automatic loading during the startup splash screen. @@ -405,9 +410,9 @@ await mainVm.SettingsModal.SaveAsync(); ### 3.10 Theme Components & Color Tokens * **Files:** - * [ThemeTokens.axaml](file:///c:/Users/ehsan/Desktop/Fdb/Themes/ThemeTokens.axaml): Shared corner radii, table row heights (`25px`), and typography. - * [LightTheme.axaml](file:///c:/Users/ehsan/Desktop/Fdb/Themes/LightTheme.axaml): Light mode semantic color tokens and table brushes. - * [DarkTheme.axaml](file:///c:/Users/ehsan/Desktop/Fdb/Themes/DarkTheme.axaml): Dark mode semantic color tokens and table brushes. + * [ThemeTokens.axaml](file:///c:/Users/ehsan/Desktop/Quell/Themes/ThemeTokens.axaml): Shared corner radii, table row heights (`25px`), and typography. + * [LightTheme.axaml](file:///c:/Users/ehsan/Desktop/Quell/Themes/LightTheme.axaml): Light mode semantic color tokens and table brushes. + * [DarkTheme.axaml](file:///c:/Users/ehsan/Desktop/Quell/Themes/DarkTheme.axaml): Dark mode semantic color tokens and table brushes. * **Description:** Modular AXAML resource dictionaries componentizing the application design system. Each theme is an independent component loaded into `Application.Resources.ThemeDictionaries`. #### Usage in App.axaml: @@ -415,11 +420,11 @@ await mainVm.SettingsModal.SaveAsync(); - + - - + + @@ -428,7 +433,7 @@ await mainVm.SettingsModal.SaveAsync(); --- ### 3.10 `ToolWindowHeaderComponent` -* **File:** [ToolWindowHeaderComponent.axaml](file:///c:/Users/ehsan/Desktop/Fdb/Components/ToolWindowHeaderComponent.axaml) | [ToolWindowHeaderComponent.axaml.cs](file:///c:/Users/ehsan/Desktop/Fdb/Components/ToolWindowHeaderComponent.axaml.cs) +* **File:** [ToolWindowHeaderComponent.axaml](file:///c:/Users/ehsan/Desktop/Quell/Components/ToolWindowHeaderComponent.axaml) | [ToolWindowHeaderComponent.axaml.cs](file:///c:/Users/ehsan/Desktop/Quell/Components/ToolWindowHeaderComponent.axaml.cs) * **Description:** Reusable title header for dockable panels and tool windows with built-in Fullscreen/Maximize and Close buttons. #### Styled Properties: @@ -449,7 +454,7 @@ await mainVm.SettingsModal.SaveAsync(); --- ### 3.11 `NavbarComponent` -* **File:** [NavbarComponent.axaml](file:///c:/Users/ehsan/Desktop/Fdb/Components/NavbarComponent.axaml) | [NavbarComponent.axaml.cs](file:///c:/Users/ehsan/Desktop/Fdb/Components/NavbarComponent.axaml.cs) +* **File:** [NavbarComponent.axaml](file:///c:/Users/ehsan/Desktop/Quell/Components/NavbarComponent.axaml) | [NavbarComponent.axaml.cs](file:///c:/Users/ehsan/Desktop/Quell/Components/NavbarComponent.axaml.cs) * **DataContext:** `NavbarViewModel` * **Description:** The top navigation bar containing native desktop menus (`File`, `Edit`, `View`, `Window`, `Help`) and action shortcuts. @@ -462,7 +467,7 @@ await mainVm.SettingsModal.SaveAsync(); --- ### 3.12 `SidebarComponent` -* **File:** [SidebarComponent.axaml](file:///c:/Users/ehsan/Desktop/Fdb/Components/SidebarComponent.axaml) | [SidebarComponent.axaml.cs](file:///c:/Users/ehsan/Desktop/Fdb/Components/SidebarComponent.axaml.cs) +* **File:** [SidebarComponent.axaml](file:///c:/Users/ehsan/Desktop/Quell/Components/SidebarComponent.axaml) | [SidebarComponent.axaml.cs](file:///c:/Users/ehsan/Desktop/Quell/Components/SidebarComponent.axaml.cs) * **DataContext:** `SidebarViewModel` * **Description:** The left-hand tool panel hosting the Database Object Explorer and schema tree views. @@ -475,7 +480,7 @@ await mainVm.SettingsModal.SaveAsync(); --- ### 3.13 `FooterComponent` -* **File:** [FooterComponent.axaml](file:///c:/Users/ehsan/Desktop/Fdb/Components/FooterComponent.axaml) | [FooterComponent.axaml.cs](file:///c:/Users/ehsan/Desktop/Fdb/Components/FooterComponent.axaml.cs) +* **File:** [FooterComponent.axaml](file:///c:/Users/ehsan/Desktop/Quell/Components/FooterComponent.axaml) | [FooterComponent.axaml.cs](file:///c:/Users/ehsan/Desktop/Quell/Components/FooterComponent.axaml.cs) * **DataContext:** `FooterViewModel` * **Description:** The collapsible and maximizable bottom dock housing query output tables, execution statistics, and logs. @@ -488,7 +493,7 @@ await mainVm.SettingsModal.SaveAsync(); --- ### 3.14 `StatusBarComponent` -* **File:** [StatusBarComponent.axaml](file:///c:/Users/ehsan/Desktop/Fdb/Components/StatusBarComponent.axaml) | [StatusBarComponent.axaml.cs](file:///c:/Users/ehsan/Desktop/Fdb/Components/StatusBarComponent.axaml.cs) +* **File:** [StatusBarComponent.axaml](file:///c:/Users/ehsan/Desktop/Quell/Components/StatusBarComponent.axaml) | [StatusBarComponent.axaml.cs](file:///c:/Users/ehsan/Desktop/Quell/Components/StatusBarComponent.axaml.cs) * **Description:** Anchored application footer bar displaying active database connection, server version, execution time, and keyboard layout indicators. #### XAML Usage Example: @@ -499,7 +504,7 @@ await mainVm.SettingsModal.SaveAsync(); --- ### 3.15 `SplashScreenCardComponent` & Startup Pre-load Sequence -* **File:** [SplashScreenCardComponent.axaml](file:///c:/Users/ehsan/Desktop/Fdb/Components/Splash/SplashScreenCardComponent.axaml) | [SplashScreenWindow.axaml.cs](file:///c:/Users/ehsan/Desktop/Fdb/Views/SplashScreenWindow.axaml.cs) +* **File:** [SplashScreenCardComponent.axaml](file:///c:/Users/ehsan/Desktop/Quell/Components/Splash/SplashScreenCardComponent.axaml) | [SplashScreenWindow.axaml.cs](file:///c:/Users/ehsan/Desktop/Quell/Views/SplashScreenWindow.axaml.cs) * **DataContext:** `SplashScreenViewModel` * **Description:** Reusable frameless splash card with high-resolution artwork and live real-time SQLite initialization diagnostics. Pre-loads all core app configurations (theme, keyword syntax color, panel customizations, table dimensions, keyboard shortcuts, dialect keywords, saved connections, and query history) into in-memory caches before displaying `MainWindow`, ensuring instant rendering with zero startup query lag. @@ -541,7 +546,7 @@ MainViewModel ## 5. Verification & Test Suite -Fdb Studio includes an automated layout, component, and autocomplete verification suite in `Program.cs`. To verify all components and behaviors: +Quell includes an automated layout, component, and autocomplete verification suite in `Program.cs`. To verify all components and behaviors: ```powershell dotnet run -- --verify diff --git a/Components/Buttons/ActionButtonComponent.axaml b/Components/Buttons/ActionButtonComponent.axaml index e4bd125..109d57a 100644 --- a/Components/Buttons/ActionButtonComponent.axaml +++ b/Components/Buttons/ActionButtonComponent.axaml @@ -1,7 +1,7 @@ - + x:Class="Quell.Components.ActionButtonComponent"> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -67,6 +71,18 @@ + + + + + + + + + + + + @@ -94,6 +110,14 @@ FontSize="11" Foreground="{DynamicResource AppTextSecondaryBrush}"/> + + + @@ -117,7 +141,7 @@ Command="{Binding ExportCommand}"> - + diff --git a/Components/Modals/ExportDataModalComponent.axaml.cs b/Components/Modals/ExportDataModalComponent.axaml.cs index 8042ca5..48377c7 100644 --- a/Components/Modals/ExportDataModalComponent.axaml.cs +++ b/Components/Modals/ExportDataModalComponent.axaml.cs @@ -1,8 +1,8 @@ -using Avalonia.Controls; +using Avalonia.Controls; using Avalonia.Input; -using Fdb.ViewModels; +using Quell.ViewModels; -namespace Fdb.Components; +namespace Quell.Components; public partial class ExportDataModalComponent : UserControl { diff --git a/Components/Modals/ModalOverlayComponent.axaml b/Components/Modals/ModalOverlayComponent.axaml index 48283d6..8343ed7 100644 --- a/Components/Modals/ModalOverlayComponent.axaml +++ b/Components/Modals/ModalOverlayComponent.axaml @@ -1,24 +1,27 @@ - - - - @@ -70,7 +73,7 @@ FontSize="15" FontWeight="Bold" Foreground="{DynamicResource AppTextPrimaryBrush}"/> - @@ -100,6 +103,14 @@ + + + + + + diff --git a/Components/Modals/SettingsModalComponent.axaml.cs b/Components/Modals/SettingsModalComponent.axaml.cs index 34c5dbc..68c92c2 100644 --- a/Components/Modals/SettingsModalComponent.axaml.cs +++ b/Components/Modals/SettingsModalComponent.axaml.cs @@ -1,8 +1,8 @@ -using Avalonia.Controls; +using Avalonia.Controls; using Avalonia.Input; -using Fdb.ViewModels; +using Quell.ViewModels; -namespace Fdb.Components; +namespace Quell.Components; public partial class SettingsModalComponent : UserControl { diff --git a/Components/Navigation/MainMenuBarComponent.axaml b/Components/Navigation/MainMenuBarComponent.axaml index 88a0288..3a9f5ae 100644 --- a/Components/Navigation/MainMenuBarComponent.axaml +++ b/Components/Navigation/MainMenuBarComponent.axaml @@ -1,10 +1,10 @@ - @@ -18,7 +18,7 @@ - @@ -31,7 +31,7 @@ Margin="0,0,10,0"/> - + + + @@ -88,6 +92,9 @@ + - + diff --git a/Components/Navigation/MainMenuBarComponent.axaml.cs b/Components/Navigation/MainMenuBarComponent.axaml.cs index f6a39f8..07aacc7 100644 --- a/Components/Navigation/MainMenuBarComponent.axaml.cs +++ b/Components/Navigation/MainMenuBarComponent.axaml.cs @@ -1,6 +1,6 @@ -using Avalonia.Controls; +using Avalonia.Controls; -namespace Fdb.Components; +namespace Quell.Components; public partial class MainMenuBarComponent : UserControl { diff --git a/Components/Navigation/MainToolbarComponent.axaml b/Components/Navigation/MainToolbarComponent.axaml index 5cab9a5..b31885d 100644 --- a/Components/Navigation/MainToolbarComponent.axaml +++ b/Components/Navigation/MainToolbarComponent.axaml @@ -1,11 +1,11 @@ - @@ -63,6 +63,18 @@ + + + + + diff --git a/Components/Navigation/MainToolbarComponent.axaml.cs b/Components/Navigation/MainToolbarComponent.axaml.cs index 982b083..9f6da43 100644 --- a/Components/Navigation/MainToolbarComponent.axaml.cs +++ b/Components/Navigation/MainToolbarComponent.axaml.cs @@ -1,6 +1,6 @@ -using Avalonia.Controls; +using Avalonia.Controls; -namespace Fdb.Components; +namespace Quell.Components; public partial class MainToolbarComponent : UserControl { diff --git a/Components/Navigation/NavbarComponent.axaml b/Components/Navigation/NavbarComponent.axaml index f5eb26f..416a298 100644 --- a/Components/Navigation/NavbarComponent.axaml +++ b/Components/Navigation/NavbarComponent.axaml @@ -1,119 +1,563 @@ - - + xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" + xmlns:d="http://schemas.microsoft.com/expression/blend/2008" + xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" + xmlns:vm="using:Quell.ViewModels" + xmlns:components="using:Quell.Components" + xmlns:ui="using:FluentAvalonia.UI.Controls" + mc:Ignorable="d" d:DesignWidth="1200" d:DesignHeight="36" + x:Class="Quell.Components.NavbarComponent" + x:DataType="vm:NavbarViewModel"> + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + BorderThickness="0,0,0,1" + Height="36" + PointerPressed="OnTitleBarPointerPressed"> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Components/Navigation/NavbarComponent.axaml.cs b/Components/Navigation/NavbarComponent.axaml.cs index b090365..904474d 100644 --- a/Components/Navigation/NavbarComponent.axaml.cs +++ b/Components/Navigation/NavbarComponent.axaml.cs @@ -1,11 +1,116 @@ +using System; +using Avalonia; using Avalonia.Controls; +using Avalonia.Input; +using Avalonia.Interactivity; +using Avalonia.VisualTree; -namespace Fdb.Components; +namespace Quell.Components; public partial class NavbarComponent : UserControl { + private Window? _hostWindow; + public NavbarComponent() { InitializeComponent(); } + + private Window? GetHostWindow() => _hostWindow ?? VisualRoot as Window ?? TopLevel.GetTopLevel(this) as Window; + + protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e) + { + base.OnAttachedToVisualTree(e); + if (TopLevel.GetTopLevel(this) is Window win) + { + _hostWindow = win; + win.PropertyChanged += OnWindowPropertyChanged; + UpdateWindowState(win.WindowState); + } + } + + protected override void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e) + { + if (_hostWindow != null) + { + _hostWindow.PropertyChanged -= OnWindowPropertyChanged; + _hostWindow = null; + } + base.OnDetachedFromVisualTree(e); + } + + private void OnWindowPropertyChanged(object? sender, AvaloniaPropertyChangedEventArgs e) + { + if (e.Property == Window.WindowStateProperty && e.NewValue is WindowState state) + { + UpdateWindowState(state); + } + } + + private void UpdateWindowState(WindowState state) + { + var isMaximized = state == WindowState.Maximized; + var maxIcon = this.FindControl("MaximizeIcon"); + var resIcon = this.FindControl("RestoreIcon"); + if (maxIcon != null) maxIcon.IsVisible = !isMaximized; + if (resIcon != null) resIcon.IsVisible = isMaximized; + } + + private void OnTitleBarPointerPressed(object? sender, PointerPressedEventArgs e) + { + if (!e.GetCurrentPoint(this).Properties.IsLeftButtonPressed) + return; + + // If user clicked inside an interactive control (Button, Menu, MenuItem, ComboBox, etc.), do nothing + if (e.Source is Visual visual) + { + if (visual.FindAncestorOfType + + - + - + + + + + + + + + + + diff --git a/Components/Panels/FooterComponent.axaml.cs b/Components/Panels/FooterComponent.axaml.cs index b0d4eee..33b3580 100644 --- a/Components/Panels/FooterComponent.axaml.cs +++ b/Components/Panels/FooterComponent.axaml.cs @@ -1,6 +1,6 @@ -using Avalonia.Controls; +using Avalonia.Controls; -namespace Fdb.Components; +namespace Quell.Components; public partial class FooterComponent : UserControl { diff --git a/Components/Panels/ObjectExplorerEmptyStateComponent.axaml b/Components/Panels/ObjectExplorerEmptyStateComponent.axaml index 41c0c55..2c30779 100644 --- a/Components/Panels/ObjectExplorerEmptyStateComponent.axaml +++ b/Components/Panels/ObjectExplorerEmptyStateComponent.axaml @@ -1,11 +1,11 @@ - + x:Class="Quell.Components.ObjectExplorerEmptyStateComponent"> @@ -113,9 +113,9 @@ - + - + - + @@ -279,6 +279,12 @@ + + + + + @@ -620,9 +626,9 @@ - + - + @@ -638,7 +644,7 @@ - + @@ -681,6 +687,13 @@ + + + + + + @@ -746,11 +759,11 @@ - - + + - + diff --git a/Components/Panels/SidebarComponent.axaml.cs b/Components/Panels/SidebarComponent.axaml.cs index fac0730..3e69037 100644 --- a/Components/Panels/SidebarComponent.axaml.cs +++ b/Components/Panels/SidebarComponent.axaml.cs @@ -1,4 +1,4 @@ -using System.Threading.Tasks; +using System.Threading.Tasks; using Avalonia; using Avalonia.Controls; using Avalonia.Input; @@ -6,10 +6,10 @@ using Avalonia.Media; using Avalonia.VisualTree; using FluentAvalonia.UI.Controls; -using Fdb.Services; -using Fdb.ViewModels; +using Quell.Services; +using Quell.ViewModels; -namespace Fdb.Components; +namespace Quell.Components; public partial class SidebarComponent : UserControl { diff --git a/Components/Settings/PanelCustomizationComponent.axaml b/Components/Settings/PanelCustomizationComponent.axaml index 732715a..58f6edb 100644 --- a/Components/Settings/PanelCustomizationComponent.axaml +++ b/Components/Settings/PanelCustomizationComponent.axaml @@ -1,11 +1,11 @@ - @@ -70,7 +70,7 @@ - + - @@ -221,7 +221,7 @@ - + - + @@ -291,7 +291,7 @@ - + - + @@ -351,7 +351,7 @@ - + @@ -430,7 +430,7 @@ - + @@ -476,10 +476,61 @@ - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Components/Settings/PerformanceSettingsComponent.axaml.cs b/Components/Settings/PerformanceSettingsComponent.axaml.cs new file mode 100644 index 0000000..9d3a8a0 --- /dev/null +++ b/Components/Settings/PerformanceSettingsComponent.axaml.cs @@ -0,0 +1,11 @@ +using Avalonia.Controls; + +namespace Quell.Components; + +public partial class PerformanceSettingsComponent : UserControl +{ + public PerformanceSettingsComponent() + { + InitializeComponent(); + } +} diff --git a/Components/Settings/SettingsCategoriesNavComponent.axaml b/Components/Settings/SettingsCategoriesNavComponent.axaml index b51ea8f..09ba1ab 100644 --- a/Components/Settings/SettingsCategoriesNavComponent.axaml +++ b/Components/Settings/SettingsCategoriesNavComponent.axaml @@ -1,11 +1,11 @@ - @@ -132,6 +132,47 @@ VerticalAlignment="Center"/> + + + + + + diff --git a/Components/Settings/SettingsCategoriesNavComponent.axaml.cs b/Components/Settings/SettingsCategoriesNavComponent.axaml.cs index 0790383..1a7576e 100644 --- a/Components/Settings/SettingsCategoriesNavComponent.axaml.cs +++ b/Components/Settings/SettingsCategoriesNavComponent.axaml.cs @@ -1,6 +1,6 @@ -using Avalonia.Controls; +using Avalonia.Controls; -namespace Fdb.Components; +namespace Quell.Components; public partial class SettingsCategoriesNavComponent : UserControl { diff --git a/Components/Settings/ShortcutsSettingsComponent.axaml b/Components/Settings/ShortcutsSettingsComponent.axaml index 5e1da83..8b28364 100644 --- a/Components/Settings/ShortcutsSettingsComponent.axaml +++ b/Components/Settings/ShortcutsSettingsComponent.axaml @@ -1,12 +1,12 @@ - diff --git a/Components/Settings/ShortcutsSettingsComponent.axaml.cs b/Components/Settings/ShortcutsSettingsComponent.axaml.cs index 477361f..39ccbaa 100644 --- a/Components/Settings/ShortcutsSettingsComponent.axaml.cs +++ b/Components/Settings/ShortcutsSettingsComponent.axaml.cs @@ -1,8 +1,8 @@ -using Avalonia.Controls; +using Avalonia.Controls; using Avalonia.Input; -using Fdb.Services; +using Quell.Services; -namespace Fdb.Components; +namespace Quell.Components; public partial class ShortcutsSettingsComponent : UserControl { diff --git a/Components/Settings/SyntaxColorSelectorComponent.axaml b/Components/Settings/SyntaxColorSelectorComponent.axaml index 97d0670..ffbd335 100644 --- a/Components/Settings/SyntaxColorSelectorComponent.axaml +++ b/Components/Settings/SyntaxColorSelectorComponent.axaml @@ -1,11 +1,11 @@ - diff --git a/Components/Settings/SyntaxColorSelectorComponent.axaml.cs b/Components/Settings/SyntaxColorSelectorComponent.axaml.cs index 9da8bce..ccb97a5 100644 --- a/Components/Settings/SyntaxColorSelectorComponent.axaml.cs +++ b/Components/Settings/SyntaxColorSelectorComponent.axaml.cs @@ -1,8 +1,8 @@ -using Avalonia.Controls; +using Avalonia.Controls; using Avalonia.Input; -using Fdb.ViewModels; +using Quell.ViewModels; -namespace Fdb.Components; +namespace Quell.Components; public partial class SyntaxColorSelectorComponent : UserControl { diff --git a/Components/Settings/TableCustomizationComponent.axaml b/Components/Settings/TableCustomizationComponent.axaml index 925c995..c908f32 100644 --- a/Components/Settings/TableCustomizationComponent.axaml +++ b/Components/Settings/TableCustomizationComponent.axaml @@ -1,11 +1,11 @@ - diff --git a/Components/Settings/TableCustomizationComponent.axaml.cs b/Components/Settings/TableCustomizationComponent.axaml.cs index 053cce9..3122cff 100644 --- a/Components/Settings/TableCustomizationComponent.axaml.cs +++ b/Components/Settings/TableCustomizationComponent.axaml.cs @@ -1,6 +1,6 @@ -using Avalonia.Controls; +using Avalonia.Controls; -namespace Fdb.Components; +namespace Quell.Components; public partial class TableCustomizationComponent : UserControl { diff --git a/Components/Settings/TerminalSettingsComponent.axaml b/Components/Settings/TerminalSettingsComponent.axaml new file mode 100644 index 0000000..00a54c7 --- /dev/null +++ b/Components/Settings/TerminalSettingsComponent.axaml @@ -0,0 +1,122 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Components/Settings/TerminalSettingsComponent.axaml.cs b/Components/Settings/TerminalSettingsComponent.axaml.cs new file mode 100644 index 0000000..4b6d3b9 --- /dev/null +++ b/Components/Settings/TerminalSettingsComponent.axaml.cs @@ -0,0 +1,12 @@ +using Avalonia.Controls; +using Quell.ViewModels; + +namespace Quell.Components; + +public partial class TerminalSettingsComponent : UserControl +{ + public TerminalSettingsComponent() + { + InitializeComponent(); + } +} diff --git a/Components/Settings/ThemeSelectionCardsComponent.axaml b/Components/Settings/ThemeSelectionCardsComponent.axaml index e5960b3..a02d0d7 100644 --- a/Components/Settings/ThemeSelectionCardsComponent.axaml +++ b/Components/Settings/ThemeSelectionCardsComponent.axaml @@ -1,11 +1,11 @@ - diff --git a/Components/Settings/ThemeSelectionCardsComponent.axaml.cs b/Components/Settings/ThemeSelectionCardsComponent.axaml.cs index c11b2f6..19f4315 100644 --- a/Components/Settings/ThemeSelectionCardsComponent.axaml.cs +++ b/Components/Settings/ThemeSelectionCardsComponent.axaml.cs @@ -1,8 +1,8 @@ -using Avalonia.Controls; +using Avalonia.Controls; using Avalonia.Input; -using Fdb.ViewModels; +using Quell.ViewModels; -namespace Fdb.Components; +namespace Quell.Components; public partial class ThemeSelectionCardsComponent : UserControl { diff --git a/Components/Splash/SplashScreenCardComponent.axaml b/Components/Splash/SplashScreenCardComponent.axaml index f97dc18..7874ac9 100644 --- a/Components/Splash/SplashScreenCardComponent.axaml +++ b/Components/Splash/SplashScreenCardComponent.axaml @@ -1,21 +1,24 @@ - + x:Class="Quell.Components.SplashScreenCardComponent" + x:DataType="vm:SplashScreenViewModel" + Focusable="False"> - + Focusable="False"> - @@ -47,11 +50,17 @@ Value="{Binding ProgressValue}" Foreground="#E8943A" Background="#33FFFFFF" - HorizontalAlignment="Stretch"/> + HorizontalAlignment="Stretch"> + + + + + + @@ -389,7 +389,7 @@ - + diff --git a/Components/Tables/QueryResultTableComponent.axaml.cs b/Components/Tables/QueryResultTableComponent.axaml.cs index b38d1ad..377e65b 100644 --- a/Components/Tables/QueryResultTableComponent.axaml.cs +++ b/Components/Tables/QueryResultTableComponent.axaml.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Linq; using System.Reflection; @@ -9,11 +9,12 @@ using Avalonia.Input; using Avalonia.Interactivity; using Avalonia.Threading; +using Avalonia.Media; using Avalonia.VisualTree; -using Fdb.Services; -using Fdb.ViewModels; +using Quell.Services; +using Quell.ViewModels; -namespace Fdb.Components; +namespace Quell.Components; public partial class QueryResultTableComponent : UserControl { @@ -154,8 +155,8 @@ void UpdateColumns() var fkBadge = new Border { - Background = new Avalonia.Media.SolidColorBrush(Avalonia.Media.Color.Parse("#F1F5F9")), - BorderBrush = new Avalonia.Media.SolidColorBrush(Avalonia.Media.Color.Parse("#CBD5E1")), + Background = ResolveThemeBrush("AppTableFkBadgeBackgroundBrush", "#F1F5F9"), + BorderBrush = ResolveThemeBrush("AppTableFkBadgeBorderBrush", "#CBD5E1"), BorderThickness = new Avalonia.Thickness(1), CornerRadius = new Avalonia.CornerRadius(3), Padding = new Avalonia.Thickness(3, 1), @@ -173,7 +174,7 @@ void UpdateColumns() Data = Avalonia.Media.Geometry.Parse("M3.9 12c0-1.71 1.39-3.1 3.1-3.1h4V7H7c-2.76 0-5 2.24-5 5s2.24 5 5 5h4v-1.9H7c-1.71 0-3.1-1.39-3.1-3.1zM8 13h8v-2H8v2zm9-6h-4v1.9h4c1.71 0 3.1 1.39 3.1 3.1s-1.39 3.1-3.1 3.1h-4V17h4c2.76 0 5-2.24 5-5s-2.24-5-5-5z"), Width = 9, Height = 9, - Foreground = new Avalonia.Media.SolidColorBrush(Avalonia.Media.Color.Parse("#475569")), + Foreground = ResolveThemeBrush("AppTableFkBadgeForegroundBrush", "#475569"), VerticalAlignment = Avalonia.Layout.VerticalAlignment.Center }; @@ -182,7 +183,7 @@ void UpdateColumns() Text = "FK", FontSize = 9, FontWeight = Avalonia.Media.FontWeight.Bold, - Foreground = new Avalonia.Media.SolidColorBrush(Avalonia.Media.Color.Parse("#475569")), + Foreground = ResolveThemeBrush("AppTableFkBadgeForegroundBrush", "#475569"), VerticalAlignment = Avalonia.Layout.VerticalAlignment.Center }; @@ -194,7 +195,7 @@ void UpdateColumns() { Text = colName.ToUpperInvariant(), FontWeight = Avalonia.Media.FontWeight.Bold, - Foreground = new Avalonia.Media.SolidColorBrush(Avalonia.Media.Color.Parse("#0F172A")), + Foreground = ResolveThemeBrush("AppTableHeaderForegroundBrush", "#0F172A"), VerticalAlignment = Avalonia.Layout.VerticalAlignment.Center }; @@ -286,8 +287,8 @@ void UpdateColumns() var pkBadge = new Border { - Background = new Avalonia.Media.SolidColorBrush(Avalonia.Media.Color.Parse("#F1F5F9")), - BorderBrush = new Avalonia.Media.SolidColorBrush(Avalonia.Media.Color.Parse("#CBD5E1")), + Background = ResolveThemeBrush("AppTablePkBadgeBackgroundBrush", "#F1F5F9"), + BorderBrush = ResolveThemeBrush("AppTablePkBadgeBorderBrush", "#CBD5E1"), BorderThickness = new Avalonia.Thickness(1), CornerRadius = new Avalonia.CornerRadius(3), Padding = new Avalonia.Thickness(3, 1), @@ -305,7 +306,7 @@ void UpdateColumns() Data = Avalonia.Media.Geometry.Parse("M12.65 10C11.83 7.67 9.61 6 7 6c-3.31 0-6 2.69-6 6s2.69 6 6 6c2.61 0 4.83-1.67 5.65-4H17v4h4v-4h2v-4H12.65zM7 14c-1.1 0-2-.9-2-2s.9-2 2-2 2 .9 2 2-.9 2-2 2z"), Width = 9, Height = 9, - Foreground = new Avalonia.Media.SolidColorBrush(Avalonia.Media.Color.Parse("#64748B")), + Foreground = ResolveThemeBrush("AppTablePkBadgeForegroundBrush", "#64748B"), VerticalAlignment = Avalonia.Layout.VerticalAlignment.Center }; @@ -314,7 +315,7 @@ void UpdateColumns() Text = "PK", FontSize = 9, FontWeight = Avalonia.Media.FontWeight.Bold, - Foreground = new Avalonia.Media.SolidColorBrush(Avalonia.Media.Color.Parse("#64748B")), + Foreground = ResolveThemeBrush("AppTablePkBadgeForegroundBrush", "#64748B"), VerticalAlignment = Avalonia.Layout.VerticalAlignment.Center }; @@ -326,7 +327,7 @@ void UpdateColumns() { Text = colName.ToUpperInvariant(), FontWeight = Avalonia.Media.FontWeight.Bold, - Foreground = new Avalonia.Media.SolidColorBrush(Avalonia.Media.Color.Parse("#0F172A")), + Foreground = ResolveThemeBrush("AppTableHeaderForegroundBrush", "#0F172A"), VerticalAlignment = Avalonia.Layout.VerticalAlignment.Center }; @@ -354,6 +355,19 @@ void UpdateColumns() } } + private IBrush ResolveThemeBrush(string key, string fallbackHex) + { + if (Application.Current != null && Application.Current.Resources.TryGetValue(key, out var res) && res is IBrush b) + { + return b; + } + if (this.TryFindResource(key, out var localRes) && localRes is IBrush lb) + { + return lb; + } + return new SolidColorBrush(Color.Parse(fallbackHex)); + } + private void OnCellPointerPressed(object? sender, DataGridCellPointerPressedEventArgs e) { if (e.Row.DataContext is not QueryResultRow item || e.Column == null) diff --git a/Components/Tables/SchemaPropertiesTableComponent.axaml b/Components/Tables/SchemaPropertiesTableComponent.axaml index 1cc7f36..6942526 100644 --- a/Components/Tables/SchemaPropertiesTableComponent.axaml +++ b/Components/Tables/SchemaPropertiesTableComponent.axaml @@ -1,12 +1,12 @@ - diff --git a/Components/Tables/SchemaPropertiesTableComponent.axaml.cs b/Components/Tables/SchemaPropertiesTableComponent.axaml.cs index 8d8beb0..bf43fa9 100644 --- a/Components/Tables/SchemaPropertiesTableComponent.axaml.cs +++ b/Components/Tables/SchemaPropertiesTableComponent.axaml.cs @@ -1,6 +1,6 @@ -using Avalonia.Controls; +using Avalonia.Controls; -namespace Fdb.Components; +namespace Quell.Components; public partial class SchemaPropertiesTableComponent : UserControl { diff --git a/Components/Tables/TablePaginationComponent.axaml b/Components/Tables/TablePaginationComponent.axaml index 10c4c3e..0f150ac 100644 --- a/Components/Tables/TablePaginationComponent.axaml +++ b/Components/Tables/TablePaginationComponent.axaml @@ -1,10 +1,10 @@ - diff --git a/Components/Tables/TablePaginationComponent.axaml.cs b/Components/Tables/TablePaginationComponent.axaml.cs index d5b38d5..b885f29 100644 --- a/Components/Tables/TablePaginationComponent.axaml.cs +++ b/Components/Tables/TablePaginationComponent.axaml.cs @@ -1,6 +1,6 @@ -using Avalonia.Controls; +using Avalonia.Controls; -namespace Fdb.Components; +namespace Quell.Components; public partial class TablePaginationComponent : UserControl { diff --git a/Components/Tables/TableToolbarComponent.axaml b/Components/Tables/TableToolbarComponent.axaml index 07e527e..cb78215 100644 --- a/Components/Tables/TableToolbarComponent.axaml +++ b/Components/Tables/TableToolbarComponent.axaml @@ -1,11 +1,11 @@ - diff --git a/Components/Tables/TableToolbarComponent.axaml.cs b/Components/Tables/TableToolbarComponent.axaml.cs index 86539d2..8be2fd9 100644 --- a/Components/Tables/TableToolbarComponent.axaml.cs +++ b/Components/Tables/TableToolbarComponent.axaml.cs @@ -1,6 +1,6 @@ -using Avalonia.Controls; +using Avalonia.Controls; -namespace Fdb.Components; +namespace Quell.Components; public partial class TableToolbarComponent : UserControl { diff --git a/Components/Terminal/TerminalComponent.axaml b/Components/Terminal/TerminalComponent.axaml new file mode 100644 index 0000000..c8c042d --- /dev/null +++ b/Components/Terminal/TerminalComponent.axaml @@ -0,0 +1,351 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Components/Terminal/TerminalComponent.axaml.cs b/Components/Terminal/TerminalComponent.axaml.cs new file mode 100644 index 0000000..b1d28e0 --- /dev/null +++ b/Components/Terminal/TerminalComponent.axaml.cs @@ -0,0 +1,163 @@ +using System; +using System.Collections.Generic; +using System.Collections.Specialized; +using System.ComponentModel; +using Avalonia.Controls; +using Avalonia.Interactivity; +using Quell.ViewModels; + +namespace Quell.Components; + +public partial class TerminalComponent : UserControl +{ + private TerminalViewModel? _viewModel; + private readonly Dictionary _sessionViews = new(); + + public TerminalComponent() + { + InitializeComponent(); + DataContextChanged += OnDataContextChanged; + Loaded += OnLoaded; + } + + private void OnDataContextChanged(object? sender, EventArgs e) + { + if (_viewModel != null) + { + _viewModel.Sessions.CollectionChanged -= OnSessionsCollectionChanged; + _viewModel.PropertyChanged -= OnViewModelPropertyChanged; + _viewModel.RequestFocus -= OnRequestFocus; + } + + _viewModel = DataContext as TerminalViewModel; + + ClearAllSessionViews(); + + if (_viewModel != null) + { + _viewModel.Sessions.CollectionChanged += OnSessionsCollectionChanged; + _viewModel.PropertyChanged += OnViewModelPropertyChanged; + _viewModel.RequestFocus += OnRequestFocus; + + foreach (var session in _viewModel.Sessions) + { + AddSessionView(session); + } + + UpdateActiveSessionView(); + } + } + + private void OnLoaded(object? sender, RoutedEventArgs e) + { + UpdateActiveSessionView(); + } + + private void OnSessionsCollectionChanged(object? sender, NotifyCollectionChangedEventArgs e) + { + if (e.Action == NotifyCollectionChangedAction.Reset) + { + ClearAllSessionViews(); + if (_viewModel != null) + { + foreach (var session in _viewModel.Sessions) + { + AddSessionView(session); + } + } + } + else + { + if (e.OldItems != null) + { + foreach (TerminalSessionViewModel session in e.OldItems) + { + RemoveSessionView(session); + } + } + + if (e.NewItems != null) + { + foreach (TerminalSessionViewModel session in e.NewItems) + { + AddSessionView(session); + } + } + } + + UpdateActiveSessionView(); + } + + private void OnViewModelPropertyChanged(object? sender, PropertyChangedEventArgs e) + { + if (e.PropertyName == nameof(TerminalViewModel.ActiveSession)) + { + UpdateActiveSessionView(); + } + } + + private void OnRequestFocus() + { + if (_viewModel?.ActiveSession != null && + _sessionViews.TryGetValue(_viewModel.ActiveSession.Id, out var activeView)) + { + activeView.FocusTerminal(); + } + } + + private void AddSessionView(TerminalSessionViewModel session) + { + if (_sessionViews.ContainsKey(session.Id)) return; + + var view = new TerminalSessionView + { + DataContext = session, + IsVisible = (session == _viewModel?.ActiveSession) + }; + + _sessionViews[session.Id] = view; + SessionsHost.Children.Add(view); + } + + private void RemoveSessionView(TerminalSessionViewModel session) + { + if (_sessionViews.TryGetValue(session.Id, out var view)) + { + SessionsHost.Children.Remove(view); + _sessionViews.Remove(session.Id); + view.DisposeSession(); + } + } + + private void ClearAllSessionViews() + { + foreach (var view in _sessionViews.Values) + { + view.DisposeSession(); + } + _sessionViews.Clear(); + SessionsHost?.Children.Clear(); + } + + private void UpdateActiveSessionView() + { + if (_viewModel == null) return; + + TerminalSessionView? currentActiveView = null; + + foreach (var kvp in _sessionViews) + { + var isCurrent = (kvp.Key == _viewModel.ActiveSession?.Id); + kvp.Value.IsVisible = isCurrent; + if (isCurrent) + { + currentActiveView = kvp.Value; + } + } + + if (currentActiveView != null && IsLoaded) + { + currentActiveView.FocusTerminal(); + } + } +} diff --git a/Components/Terminal/TerminalSessionView.axaml b/Components/Terminal/TerminalSessionView.axaml new file mode 100644 index 0000000..5bf32c9 --- /dev/null +++ b/Components/Terminal/TerminalSessionView.axaml @@ -0,0 +1,207 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Components/Terminal/TerminalSessionView.axaml.cs b/Components/Terminal/TerminalSessionView.axaml.cs new file mode 100644 index 0000000..48bc5b8 --- /dev/null +++ b/Components/Terminal/TerminalSessionView.axaml.cs @@ -0,0 +1,389 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Avalonia.Controls; +using Avalonia.Input.Platform; +using Avalonia.Interactivity; +using Avalonia.Media; +using Avalonia.Threading; +using Avalonia; +using Quell.Services; +using Quell.Services.Terminal; +using Quell.ViewModels; + +namespace Quell.Components; + +public partial class TerminalSessionView : UserControl +{ + private TerminalSessionViewModel? _session; + private bool _processLaunched; + private bool _disposed; + private readonly Queue _pendingInput = new(); + + public TerminalSessionView() + { + InitializeComponent(); + DataContextChanged += OnDataContextChanged; + Loaded += OnLoaded; + Unloaded += OnUnloaded; + + OpenShellFallbackButton.Click += OnOpenShellFallback; + OpenSettingsButton.Click += OnOpenSettings; + CopyPathButton.Click += OnCopyPathClicked; + + TerminalSettingsService.Instance.SettingsChanged += OnSettingsChanged; + ThemeService.Instance.ThemeChanged += OnThemeChanged; + ThemeService.Instance.PanelCustomizationChanged += OnPanelCustomizationChanged; + ApplyThemeColors(); + } + + private async void OnCopyPathClicked(object? sender, RoutedEventArgs e) + { + if (_session != null && !string.IsNullOrWhiteSpace(_session.WorkingDirectory)) + { + await Quell.Services.ClipboardService.CopyTextAsync(_session.WorkingDirectory, this); + _session.IsCopied = true; + await Task.Delay(1500); + _session.IsCopied = false; + } + } + + public void FocusTerminal() + { + Dispatcher.UIThread.Post(() => + { + TermControl?.Focus(); + }, DispatcherPriority.Input); + } + + public void ClearScreen() + { + try + { + TermControl?.ClearScreen(); + } + catch (Exception ex) + { + Console.WriteLine($"[TerminalSessionView] ClearScreen error: {ex.Message}"); + } + } + + public void DisposeSession() + { + if (_disposed) return; + _disposed = true; + + DetachSessionEvents(); + TerminalSettingsService.Instance.SettingsChanged -= OnSettingsChanged; + ThemeService.Instance.ThemeChanged -= OnThemeChanged; + ThemeService.Instance.PanelCustomizationChanged -= OnPanelCustomizationChanged; + + try + { + TermControl?.Kill(); + } + catch { } + + try + { + TermControl?.Dispose(); + } + catch { } + + if (_session != null) + { + _session.IsRunning = false; + } + } + + private void OnDataContextChanged(object? sender, EventArgs e) + { + DetachSessionEvents(); + + _session = DataContext as TerminalSessionViewModel; + + if (_session != null) + { + AttachSessionEvents(); + ApplyCurrentSettings(); + + if (IsLoaded && !_session.HasCliError && !_processLaunched) + { + LaunchProcess(); + } + } + } + + private void AttachSessionEvents() + { + if (_session == null) return; + _session.SendTextRequested += OnSendTextRequested; + _session.ClearScreenRequested += OnClearScreenRequested; + _session.KillRequested += OnKillRequested; + _session.FocusRequested += OnFocusRequested; + _session.RestartRequested += OnRestartRequested; + _session.CopyToClipboardRequested += OnCopyToClipboardRequested; + } + + private void DetachSessionEvents() + { + if (_session == null) return; + _session.SendTextRequested -= OnSendTextRequested; + _session.ClearScreenRequested -= OnClearScreenRequested; + _session.KillRequested -= OnKillRequested; + _session.FocusRequested -= OnFocusRequested; + _session.RestartRequested -= OnRestartRequested; + _session.CopyToClipboardRequested -= OnCopyToClipboardRequested; + } + + private async Task OnCopyToClipboardRequested(string text) + { + try + { + await Quell.Services.ClipboardService.CopyTextAsync(text, this); + } + catch (Exception ex) + { + Console.WriteLine($"[TerminalSessionView] Copy clipboard error: {ex.Message}"); + } + } + + private void OnLoaded(object? sender, RoutedEventArgs e) + { + if (_session != null && !_session.HasCliError && !_processLaunched) + { + LaunchProcess(); + } + } + + private void OnUnloaded(object? sender, RoutedEventArgs e) + { + // Don't kill process on temporary tab switch or container re-order + } + + private async void LaunchProcess() + { + if (_session == null || _session.HasCliError || string.IsNullOrEmpty(_session.ExecutablePath)) + { + return; + } + + if (_processLaunched) + { + return; + } + + _processLaunched = true; + + try + { + TermControl.ApplyTemplate(); + + TermControl.StartingDirectory = !string.IsNullOrEmpty(_session.WorkingDirectory) + ? _session.WorkingDirectory + : Environment.CurrentDirectory; + + TermControl.Process = _session.ExecutablePath; + + if (!string.IsNullOrWhiteSpace(_session.Arguments)) + { + TermControl.ProcessArgs = _session.Arguments.Split(' ', StringSplitOptions.RemoveEmptyEntries); + } + else + { + TermControl.ProcessArgs = new List(); + } + + if (TermControl.EnvironmentVariables != null) + { + foreach (var kvp in _session.EnvironmentVariables) + { + TermControl.EnvironmentVariables[kvp.Key] = kvp.Value; + } + } + + _session.IsRunning = true; + + TermControl.ProcessExited += (s, args) => + { + Dispatcher.UIThread.Post(() => + { + if (_session != null) + { + _session.IsRunning = false; + _session.ExitCode = args.ExitCode; + } + }); + }; + + await TermControl.LaunchProcess(); + + while (_pendingInput.Count > 0) + { + var pending = _pendingInput.Dequeue(); + _ = TermControl.SendInputAsync(pending, default); + } + + FocusTerminal(); + } + catch (Exception ex) + { + Console.WriteLine($"[TerminalSessionView] LaunchProcess exception: {ex.Message}"); + if (_session != null) + { + _session.HasCliError = true; + _session.CliErrorTitle = "Process Launch Failed"; + _session.CliErrorMessage = ex.Message; + } + } + } + + private void OnSendTextRequested(string text) + { + try + { + if (TermControl != null && _session != null && _session.IsRunning) + { + _ = TermControl.SendInputAsync(text, default); + FocusTerminal(); + } + else + { + _pendingInput.Enqueue(text); + } + } + catch (Exception ex) + { + Console.WriteLine($"[TerminalSessionView] SendTextRequested error: {ex.Message}"); + } + } + + private void OnClearScreenRequested() + { + ClearScreen(); + } + + private void OnKillRequested() + { + try + { + TermControl?.Kill(); + } + catch { } + + if (_session != null) + { + _session.IsRunning = false; + } + } + + private void OnFocusRequested() + { + FocusTerminal(); + } + + private void OnRestartRequested() + { + try + { + TermControl?.Kill(); + } + catch { } + + _processLaunched = false; + if (_session != null && !_session.HasCliError) + { + LaunchProcess(); + } + } + + private void ApplyCurrentSettings() + { + var settings = TerminalSettingsService.Instance.CurrentSettings; + if (TermControl != null && settings != null) + { + if (!string.IsNullOrWhiteSpace(settings.FontFamily)) + { + try + { + TermControl.FontFamily = new FontFamily(settings.FontFamily); + } + catch { } + } + + if (settings.FontSize >= 8 && settings.FontSize <= 32) + { + TermControl.FontSize = settings.FontSize; + } + + TermControl.CursorBlink = settings.CursorBlink; + } + } + + private void OnThemeChanged(object? sender, string theme) + { + Dispatcher.UIThread.Post(ApplyThemeColors); + } + + private void OnPanelCustomizationChanged(object? sender, PanelCustomizationRecord custom) + { + Dispatcher.UIThread.Post(ApplyThemeColors); + } + + private void ApplyThemeColors() + { + if (TermControl == null) return; + + bool isDark = ThemeService.Instance.IsDarkTheme(); + + // Background + IBrush? bgBrush = null; + if (Application.Current != null && Application.Current.Resources.TryGetValue("AppTerminalBackgroundBrush", out var resBg) && resBg is IBrush b) + { + bgBrush = b; + } + else + { + bgBrush = isDark ? new SolidColorBrush(Color.Parse("#0F172A")) : new SolidColorBrush(Color.Parse("#FFFFFF")); + } + TermControl.Background = bgBrush; + + // Foreground + IBrush? fgBrush = null; + if (Application.Current != null && Application.Current.Resources.TryGetValue("AppTerminalForegroundBrush", out var resFg) && resFg is IBrush f) + { + fgBrush = f; + } + else + { + fgBrush = isDark ? new SolidColorBrush(Color.Parse("#F8FAFC")) : new SolidColorBrush(Color.Parse("#0F172A")); + } + TermControl.Foreground = fgBrush; + + // Accent & Cursor Color + Color accentCol = isDark ? Color.Parse("#3B82F6") : Color.Parse("#2563EB"); + if (Application.Current != null && Application.Current.Resources.TryGetValue("AppAccentBrush", out var resAcc) && resAcc is ISolidColorBrush scb) + { + accentCol = scb.Color; + } + TermControl.CursorColor = accentCol; + TermControl.SelectionBrush = new SolidColorBrush(Color.FromArgb(64, accentCol.R, accentCol.G, accentCol.B)); + } + + private void OnSettingsChanged(object? sender, Models.TerminalSettingsModel settings) + { + Dispatcher.UIThread.Post(ApplyCurrentSettings); + } + + private void OnOpenShellFallback(object? sender, RoutedEventArgs e) + { + var mainVm = (VisualRoot as Window)?.DataContext as MainViewModel; + mainVm?.Terminal.NewSession(); + } + + private void OnOpenSettings(object? sender, RoutedEventArgs e) + { + var mainVm = (VisualRoot as Window)?.DataContext as MainViewModel; + mainVm?.SettingsModal.OpenCommand.Execute(null); + } +} diff --git a/Fdb.Android/Resources/mipmap/icon.png b/Fdb.Android/Resources/mipmap/icon.png deleted file mode 100644 index 663d541..0000000 Binary files a/Fdb.Android/Resources/mipmap/icon.png and /dev/null differ diff --git a/Models/PropertyItem.cs b/Models/PropertyItem.cs index 149c8de..9c8428b 100644 --- a/Models/PropertyItem.cs +++ b/Models/PropertyItem.cs @@ -1,4 +1,4 @@ -namespace Fdb.Models; +namespace Quell.Models; public class PropertyItem { diff --git a/Models/TableColumnItem.cs b/Models/TableColumnItem.cs index 9f33c24..8f5f7cb 100644 --- a/Models/TableColumnItem.cs +++ b/Models/TableColumnItem.cs @@ -1,4 +1,4 @@ -namespace Fdb.Models; +namespace Quell.Models; public class TableColumnItem { diff --git a/Models/TerminalModels.cs b/Models/TerminalModels.cs new file mode 100644 index 0000000..ce7062c --- /dev/null +++ b/Models/TerminalModels.cs @@ -0,0 +1,44 @@ +using System; +using System.Collections.Generic; + +namespace Quell.Models; + +public class ShellProfile +{ + public string Id { get; set; } = string.Empty; + public string DisplayName { get; set; } = string.Empty; + public string ExecutablePath { get; set; } = string.Empty; + public string Arguments { get; set; } = string.Empty; + public string IconKey { get; set; } = "Terminal"; + public bool IsDatabaseCli { get; set; } + public string DatabaseType { get; set; } = string.Empty; + + public override string ToString() => DisplayName; +} + +public class TerminalSettingsModel +{ + public string DefaultShellId { get; set; } = "default"; + public string FontFamily { get; set; } = "Cascadia Mono, Consolas, Courier New, monospace"; + public double FontSize { get; set; } = 13.0; + public string CursorStyle { get; set; } = "Block"; // Block, Underline, Bar + public bool CursorBlink { get; set; } = true; + public int MaxScrollback { get; set; } = 5000; + + // Custom CLI overrides if not in PATH + public string PsqlPath { get; set; } = string.Empty; + public string MySqlPath { get; set; } = string.Empty; + public string SqlitePath { get; set; } = string.Empty; + public string SqlCmdPath { get; set; } = string.Empty; +} + +public class CliLaunchInfo +{ + public bool IsAvailable { get; set; } + public string ExecutablePath { get; set; } = string.Empty; + public List Arguments { get; set; } = new(); + public Dictionary EnvironmentVariables { get; set; } = new(); + public string MissingToolName { get; set; } = string.Empty; + public string DownloadUrl { get; set; } = string.Empty; + public string Instructions { get; set; } = string.Empty; +} diff --git a/Program.cs b/Program.cs index 556808a..e5dbfb4 100644 --- a/Program.cs +++ b/Program.cs @@ -4,7 +4,7 @@ using System.Linq; using System.Threading.Tasks; -namespace Fdb; +namespace Quell; sealed class Program { @@ -24,10 +24,20 @@ public static async Task Main(string[] args) if (args.Length > 0 && args[0] == "--verify") { Console.OutputEncoding = System.Text.Encoding.UTF8; - Console.WriteLine("=== Fdb Dynamic Layout Verification ==="); - var vm = new Fdb.ViewModels.MainViewModel(); + Console.WriteLine("=== Quell Dynamic Layout Verification ==="); + var vm = new Quell.ViewModels.MainViewModel(); var app = BuildAvaloniaApp().SetupWithoutStarting(); - var window = new Fdb.Views.MainWindow { DataContext = vm }; + var window = new Quell.Views.MainWindow { DataContext = vm }; + + // 0. Verify Focus Visual Suppression & Window/Menu Border Styling + Console.WriteLine("\n[Action] Verifying Focus Visual Suppression & Border Styling..."); + var splashWin = new Quell.Views.SplashScreenWindow(); + Console.WriteLine($"Splash Window: Width={splashWin.Width}, Height={splashWin.Height}, Background={splashWin.Background}"); + if (splashWin.Width != 720 || splashWin.Height != 400 || splashWin.Background == null) + { + throw new Exception("Splash Window dimensions or background are invalid!"); + } + Console.WriteLine("[Success] Splash Screen window sizing, background, and focus suppression verified!"); Console.WriteLine($"[Initial State]"); Console.WriteLine($"Sidebar: Visible={vm.Sidebar.IsVisible}, ColWidth={window.MainGrid.ColumnDefinitions[0].Width}"); @@ -126,7 +136,7 @@ public static async Task Main(string[] args) throw new Exception($"Expected clean initial footer tabs (0 tabs), but got {vm.Footer.Tabs.Count}"); } Console.WriteLine($"SchemaPropertiesTable Columns: {vm.SampleSchema.Columns.Count} columns loaded."); - var tableComp = new Fdb.Components.QueryResultTableComponent { DataContext = vm.SampleQueryResult }; + var tableComp = new Quell.Components.QueryResultTableComponent { DataContext = vm.SampleQueryResult }; var dataGrid = tableComp.FindControl("ResultsDataGrid"); Console.WriteLine($"DataGrid Columns Count: {dataGrid?.Columns.Count ?? 0}"); Console.WriteLine($"DataGrid RowHeight: {dataGrid?.RowHeight}px (Compact height)"); @@ -134,7 +144,7 @@ public static async Task Main(string[] args) { throw new Exception($"Expected compact DataGrid RowHeight=25, but got {dataGrid?.RowHeight}"); } - var testRow = new Fdb.ViewModels.QueryResultRow { RowIndex = 1, Id = 101, Name = "Alice Johnson" }; + var testRow = new Quell.ViewModels.QueryResultRow { RowIndex = 1, Id = 101, Name = "Alice Johnson" }; var col0 = dataGrid?.Columns[0] as Avalonia.Controls.DataGridTemplateColumn; var cellContent = col0?.CellTemplate?.Build(testRow) as Avalonia.Controls.Grid; if (cellContent != null) cellContent.DataContext = testRow; @@ -149,7 +159,7 @@ public static async Task Main(string[] args) // 5. Test Modal Dialogs Lifecycle & Triggers Console.WriteLine("\n[Action] Verifying Modal Dialogs Lifecycle..."); Console.WriteLine($"ConnectModal initially open: {vm.ConnectModal.IsOpen}"); - var navbarComp = window.FindControl("MainNavbar"); + var navbarComp = window.FindControl("MainNavbar"); var connectBtn = navbarComp?.FindControl("ConnectButton"); Console.WriteLine($"ConnectButton found: {connectBtn != null}, Command: {connectBtn?.Command}"); connectBtn?.Command?.Execute(null); @@ -189,26 +199,26 @@ public static async Task Main(string[] args) // Test switching to Dark theme Console.WriteLine("[Action] Switching to Dark theme..."); vm.SettingsModal.SelectTheme("Dark"); - Console.WriteLine($"Theme applied: CurrentTheme={Fdb.Services.ThemeService.Instance.CurrentTheme}, IsDark={vm.SettingsModal.IsDarkTheme}"); - if (!vm.SettingsModal.IsDarkTheme || Fdb.Services.ThemeService.Instance.CurrentTheme != "Dark") + Console.WriteLine($"Theme applied: CurrentTheme={Quell.Services.ThemeService.Instance.CurrentTheme}, IsDark={vm.SettingsModal.IsDarkTheme}"); + if (!vm.SettingsModal.IsDarkTheme || Quell.Services.ThemeService.Instance.CurrentTheme != "Dark") { throw new Exception("Dark theme selection failed!"); } await vm.SettingsModal.SaveAsync(); Console.WriteLine($"After SaveAsync: SettingsModal.IsOpen = {vm.SettingsModal.IsOpen}"); - string persistedTheme = await Fdb.Services.DatabaseStorageService.Instance.GetThemeAsync(); + string persistedTheme = await Quell.Services.DatabaseStorageService.Instance.GetThemeAsync(); Console.WriteLine($"SQLite Persisted Theme: '{persistedTheme}'"); if (persistedTheme != "Dark") throw new Exception("Failed to persist Dark theme to SQLite!"); // Test switching to System theme await vm.SettingsModal.OpenAsync(); vm.SettingsModal.SelectTheme("System"); - if (!vm.SettingsModal.IsSystemTheme || Fdb.Services.ThemeService.Instance.CurrentTheme != "System") + if (!vm.SettingsModal.IsSystemTheme || Quell.Services.ThemeService.Instance.CurrentTheme != "System") { throw new Exception("System theme selection failed!"); } await vm.SettingsModal.SaveAsync(); - string persistedSystemTheme = await Fdb.Services.DatabaseStorageService.Instance.GetThemeAsync(); + string persistedSystemTheme = await Quell.Services.DatabaseStorageService.Instance.GetThemeAsync(); Console.WriteLine($"SQLite Persisted System Theme: '{persistedSystemTheme}'"); if (persistedSystemTheme != "System") throw new Exception("Failed to persist System theme to SQLite!"); @@ -216,7 +226,7 @@ public static async Task Main(string[] args) await vm.SettingsModal.OpenAsync(); vm.SettingsModal.SelectTheme("Light"); await vm.SettingsModal.SaveAsync(); - string restoredTheme = await Fdb.Services.DatabaseStorageService.Instance.GetThemeAsync(); + string restoredTheme = await Quell.Services.DatabaseStorageService.Instance.GetThemeAsync(); Console.WriteLine($"Restored Theme in SQLite: '{restoredTheme}'"); if (restoredTheme != "Light") throw new Exception("Failed to restore Light theme in SQLite!"); @@ -231,7 +241,7 @@ public static async Task Main(string[] args) if (!vm.SettingsModal.IsGreenKeyword) throw new Exception("Green keyword selection failed in ViewModel!"); // In Light theme, Green should be emerald (#059669) - var lightBrush = Fdb.Services.ThemeService.Instance.CurrentKeywordBrush as Avalonia.Media.ISolidColorBrush; + var lightBrush = Quell.Services.ThemeService.Instance.CurrentKeywordBrush as Avalonia.Media.ISolidColorBrush; Console.WriteLine($"Light Theme Green Keyword Brush: {lightBrush?.Color}"); if (lightBrush?.Color != Avalonia.Media.Color.Parse("#059669")) { @@ -240,7 +250,7 @@ public static async Task Main(string[] args) // Switch to Dark Theme: Green keyword should automatically adapt to bright mint (#34D399) for high contrast! vm.SettingsModal.SelectTheme("Dark"); - var darkBrush = Fdb.Services.ThemeService.Instance.CurrentKeywordBrush as Avalonia.Media.ISolidColorBrush; + var darkBrush = Quell.Services.ThemeService.Instance.CurrentKeywordBrush as Avalonia.Media.ISolidColorBrush; Console.WriteLine($"Dark Theme Green Keyword Brush (Auto Contrast): {darkBrush?.Color}"); if (darkBrush?.Color != Avalonia.Media.Color.Parse("#34D399")) { @@ -249,7 +259,7 @@ public static async Task Main(string[] args) // Save Green Keyword Color & Dark Theme to SQLite await vm.SettingsModal.SaveAsync(); - string persistedKeywordColor = await Fdb.Services.DatabaseStorageService.Instance.GetKeywordColorAsync(); + string persistedKeywordColor = await Quell.Services.DatabaseStorageService.Instance.GetKeywordColorAsync(); Console.WriteLine($"SQLite Persisted Keyword Color: '{persistedKeywordColor}'"); if (persistedKeywordColor != "Green") throw new Exception("Failed to persist Green Keyword Color to SQLite!"); @@ -258,12 +268,12 @@ public static async Task Main(string[] args) vm.SettingsModal.SelectKeywordColor("Blue"); vm.SettingsModal.SelectTheme("Light"); await vm.SettingsModal.SaveAsync(); - string restoredKeywordColor = await Fdb.Services.DatabaseStorageService.Instance.GetKeywordColorAsync(); + string restoredKeywordColor = await Quell.Services.DatabaseStorageService.Instance.GetKeywordColorAsync(); Console.WriteLine($"Restored Keyword Color in SQLite: '{restoredKeywordColor}'"); if (restoredKeywordColor != "Blue") throw new Exception("Failed to restore Blue Keyword Color to SQLite!"); // 5b. Test Panel Customization, Color Maker, Button/Border/Sidebar & SQLite Persistence - Console.WriteLine("\n[Action] Verifying Color Maker ('رنگ‌ساز'), Buttons, Borders & Multi-Part Customization..."); + Console.WriteLine("\n[Action] Verifying Color Maker, Buttons, Borders & Multi-Part Customization..."); await vm.SettingsModal.OpenAsync(); vm.SettingsModal.SelectCategory("Customization"); Console.WriteLine($"Settings Category: Selected='{vm.SettingsModal.SelectedCategory}', IsCustomization={vm.SettingsModal.IsCustomizationCategory}"); @@ -300,15 +310,17 @@ public static async Task Main(string[] args) vm.SettingsModal.SelectEditorBackground("#1E293B"); vm.SettingsModal.SelectEditorForeground("#34D399"); + vm.SettingsModal.SelectTerminalBackground("#0C0E14"); + vm.SettingsModal.SelectTerminalForeground("#38BDF8"); vm.SettingsModal.SelectTableBackground("#1E293B"); vm.SettingsModal.EditorFontSize = 15.0; vm.SettingsModal.EditorShowLineNumbers = true; await vm.SettingsModal.SaveAsync(); - var savedCustom = await Fdb.Services.DatabaseStorageService.Instance.GetPanelCustomizationAsync(); + var savedCustom = await Quell.Services.DatabaseStorageService.Instance.GetPanelCustomizationAsync(); Console.WriteLine($"SQLite Saved Panel Customization: ButtonAccent={savedCustom.ButtonAccentColor}, BorderColor={savedCustom.BorderColor}, SidebarHeader={savedCustom.SidebarHeaderBackground}, EditorBg={savedCustom.EditorBackground}, FontSize={savedCustom.EditorFontSize}"); - if (savedCustom.ButtonAccentColor != "#38BDF8" || savedCustom.BorderColor != "#334155" || savedCustom.SidebarHeaderBackground != "#1E293B" || savedCustom.EditorBackground != "#1E293B" || savedCustom.EditorFontSize != 15.0) + if (savedCustom.ButtonAccentColor != "#38BDF8" || savedCustom.BorderColor != "#334155" || savedCustom.SidebarHeaderBackground != "#1E293B" || savedCustom.EditorBackground != "#1E293B" || savedCustom.TerminalBackground != "#0C0E14" || savedCustom.TerminalForeground != "#38BDF8" || savedCustom.EditorFontSize != 15.0) { throw new Exception("Expanded panel customization failed to persist to SQLite!"); } @@ -317,9 +329,9 @@ public static async Task Main(string[] args) await vm.SettingsModal.OpenAsync(); vm.SettingsModal.ResetPanelCustomization(); await vm.SettingsModal.SaveAsync(); - var resetCustom = await Fdb.Services.DatabaseStorageService.Instance.GetPanelCustomizationAsync(); + var resetCustom = await Quell.Services.DatabaseStorageService.Instance.GetPanelCustomizationAsync(); Console.WriteLine($"SQLite Restored Panel Customization: ButtonAccent={resetCustom.ButtonAccentColor}, BorderColor={resetCustom.BorderColor}, EditorBg={resetCustom.EditorBackground}, FontSize={resetCustom.EditorFontSize}"); - if (resetCustom.ButtonAccentColor != "Default" || resetCustom.BorderColor != "Default" || resetCustom.EditorBackground != "Default" || resetCustom.EditorFontSize != 13.0) + if (resetCustom.ButtonAccentColor != "Default" || resetCustom.BorderColor != "Default" || resetCustom.EditorBackground != "Default" || resetCustom.TerminalBackground != "Default" || resetCustom.TerminalForeground != "Default" || resetCustom.EditorFontSize != 13.0) { throw new Exception("Reset panel customization failed!"); } @@ -335,7 +347,7 @@ public static async Task Main(string[] args) Console.WriteLine($"Loaded Shortcuts Count: {vm.SettingsModal.Shortcuts.Count}"); if (vm.SettingsModal.Shortcuts.Count == 0) throw new Exception("No shortcuts loaded from SQLite!"); - var runQueryShortcut = vm.SettingsModal.Shortcuts.FirstOrDefault(s => s.Id == Fdb.Services.ShortcutActions.RunQuery); + var runQueryShortcut = vm.SettingsModal.Shortcuts.FirstOrDefault(s => s.Id == Quell.Services.ShortcutActions.RunQuery); if (runQueryShortcut == null) throw new Exception("RunQuery shortcut definition missing!"); Console.WriteLine($"Initial RunQuery Shortcut: '{runQueryShortcut.CurrentGesture}' (Default: '{runQueryShortcut.DefaultGesture}')"); @@ -352,8 +364,8 @@ public static async Task Main(string[] args) await vm.SettingsModal.SaveAsync(); // Verify in SQLite - var dbShortcuts = await Fdb.Services.DatabaseStorageService.Instance.GetShortcutsFromDbAsync(); - var dbRunQuery = dbShortcuts.FirstOrDefault(s => s.Id == Fdb.Services.ShortcutActions.RunQuery); + var dbShortcuts = await Quell.Services.DatabaseStorageService.Instance.GetShortcutsFromDbAsync(); + var dbRunQuery = dbShortcuts.FirstOrDefault(s => s.Id == Quell.Services.ShortcutActions.RunQuery); Console.WriteLine($"SQLite Persisted RunQuery Shortcut: '{dbRunQuery?.KeyGesture}'"); if (dbRunQuery?.KeyGesture != "F9") { @@ -361,19 +373,19 @@ public static async Task Main(string[] args) } // Verify ShortcutService key matching engine - bool isF9Match = Fdb.Services.ShortcutService.Instance.IsMatch(Fdb.Services.ShortcutActions.RunQuery, + bool isF9Match = Quell.Services.ShortcutService.Instance.IsMatch(Quell.Services.ShortcutActions.RunQuery, new Avalonia.Input.KeyEventArgs { Key = Avalonia.Input.Key.F9, KeyModifiers = Avalonia.Input.KeyModifiers.None }); Console.WriteLine($"ShortcutService matches 'F9' for RunQuery: {isF9Match}"); if (!isF9Match) throw new Exception("ShortcutService key matching engine failed for customized 'F9'!"); // Test Reset Shortcut to Default await vm.SettingsModal.OpenAsync(); - var runQueryToReset = vm.SettingsModal.Shortcuts.FirstOrDefault(s => s.Id == Fdb.Services.ShortcutActions.RunQuery); + var runQueryToReset = vm.SettingsModal.Shortcuts.FirstOrDefault(s => s.Id == Quell.Services.ShortcutActions.RunQuery); vm.SettingsModal.ResetShortcut(runQueryToReset!); await vm.SettingsModal.SaveAsync(); - var dbResetShortcuts = await Fdb.Services.DatabaseStorageService.Instance.GetShortcutsFromDbAsync(); - var dbResetRunQuery = dbResetShortcuts.FirstOrDefault(s => s.Id == Fdb.Services.ShortcutActions.RunQuery); + var dbResetShortcuts = await Quell.Services.DatabaseStorageService.Instance.GetShortcutsFromDbAsync(); + var dbResetRunQuery = dbResetShortcuts.FirstOrDefault(s => s.Id == Quell.Services.ShortcutActions.RunQuery); Console.WriteLine($"SQLite Restored RunQuery Shortcut: '{dbResetRunQuery?.KeyGesture}'"); if (dbResetRunQuery?.KeyGesture != "F5") { @@ -393,6 +405,8 @@ public static async Task Main(string[] args) vm.SettingsModal.SetTableHeaderHeight(30.0); vm.SettingsModal.SetTableGutterWidth(64.0); vm.SettingsModal.SetTableCellFontSize(12.0); + vm.SettingsModal.SelectTerminalBackground("#0C0E14"); + vm.SettingsModal.SelectTerminalForeground("#38BDF8"); vm.SettingsModal.SelectTableBackground("#1E293B"); vm.SettingsModal.SelectTableAlternatingRowColor("#141E2E"); vm.SettingsModal.SelectTableCellTextColor("#F8FAFC"); @@ -401,7 +415,7 @@ public static async Task Main(string[] args) await vm.SettingsModal.SaveAsync(); - var savedTableCustom = await Fdb.Services.DatabaseStorageService.Instance.GetTableCustomizationAsync(); + var savedTableCustom = await Quell.Services.DatabaseStorageService.Instance.GetTableCustomizationAsync(); Console.WriteLine($"SQLite Saved Table Customization: RowHeight={savedTableCustom.RowHeight}, HeaderHeight={savedTableCustom.HeaderHeight}, GutterWidth={savedTableCustom.GutterWidth}, FontSize={savedTableCustom.FontSize}, TableBg={savedTableCustom.TableBackground}, AltRowBg={savedTableCustom.AlternatingRowColor}"); if (savedTableCustom.RowHeight != 28.0 || savedTableCustom.HeaderHeight != 30.0 || savedTableCustom.GutterWidth != 64.0 || savedTableCustom.FontSize != 12.0 || savedTableCustom.TableBackground != "#1E293B" || savedTableCustom.AlternatingRowColor != "#141E2E") { @@ -409,8 +423,8 @@ public static async Task Main(string[] args) } // Verify live dynamic update in ThemeService - Console.WriteLine($"ThemeService CurrentTableCustomization: RowHeight={Fdb.Services.ThemeService.Instance.CurrentTableCustomization.RowHeight}, FontSize={Fdb.Services.ThemeService.Instance.CurrentTableCustomization.FontSize}"); - if (Fdb.Services.ThemeService.Instance.CurrentTableCustomization.RowHeight != 28.0 || Fdb.Services.ThemeService.Instance.CurrentTableCustomization.FontSize != 12.0) + Console.WriteLine($"ThemeService CurrentTableCustomization: RowHeight={Quell.Services.ThemeService.Instance.CurrentTableCustomization.RowHeight}, FontSize={Quell.Services.ThemeService.Instance.CurrentTableCustomization.FontSize}"); + if (Quell.Services.ThemeService.Instance.CurrentTableCustomization.RowHeight != 28.0 || Quell.Services.ThemeService.Instance.CurrentTableCustomization.FontSize != 12.0) { throw new Exception("ThemeService failed to dynamically update CurrentTableCustomization!"); } @@ -420,7 +434,7 @@ public static async Task Main(string[] args) vm.SettingsModal.ResetTableCustomization(); await vm.SettingsModal.SaveAsync(); - var resetTableCustom = await Fdb.Services.DatabaseStorageService.Instance.GetTableCustomizationAsync(); + var resetTableCustom = await Quell.Services.DatabaseStorageService.Instance.GetTableCustomizationAsync(); Console.WriteLine($"SQLite Restored Table Customization: RowHeight={resetTableCustom.RowHeight}, FontSize={resetTableCustom.FontSize}, TableBg={resetTableCustom.TableBackground}"); if (resetTableCustom.RowHeight != 25.0 || resetTableCustom.FontSize != 11.0 || resetTableCustom.TableBackground != "Default") { @@ -438,7 +452,7 @@ public static async Task Main(string[] args) throw new Exception("Editor tab activation failed!"); } - var editorComp = new Fdb.Components.EditorComponent { DataContext = vm.Editor }; + var editorComp = new Quell.Components.EditorComponent { DataContext = vm.Editor }; var textEditor = editorComp.FindControl("CodeEditor"); Console.WriteLine($"TextEditor Document Text Length: {textEditor?.Document?.TextLength}"); @@ -480,7 +494,7 @@ public static async Task Main(string[] args) // 7. Test SQL Keywords Service & Regex Console.WriteLine("\n[Action] Verifying SQL Keywords Service..."); - var keywordsService = Fdb.Services.SqlKeywordsService.Instance; + var keywordsService = Quell.Services.SqlKeywordsService.Instance; Console.WriteLine($"Loaded SQL Keywords: {keywordsService.Keywords.Count} keywords, {keywordsService.Snippets.Count} snippets."); var regex = keywordsService.KeywordRegex; bool selectMatches = regex != null && regex.IsMatch("SELECT * FROM dbo.Users"); @@ -494,7 +508,7 @@ public static async Task Main(string[] args) Console.WriteLine("\n[Action] Testing typing 'se' in TextEditor..."); // Test typing in editor & Zoom shortcuts Console.WriteLine("\n[Action] Testing typing 'se' in TextEditor & Zoom Shortcuts..."); - var editorForTest = new Fdb.Components.EditorComponent { DataContext = vm.Editor }; + var editorForTest = new Quell.Components.EditorComponent { DataContext = vm.Editor }; var te = editorForTest.FindControl("CodeEditor")!; te.Document.Text = ""; te.CaretOffset = 0; @@ -529,8 +543,8 @@ public static async Task Main(string[] args) // Test CompletionData creation and replacement Console.WriteLine("\n[Action] Testing CompletionData instantiation and insertion..."); - var kwData = new Fdb.Services.SqlCompletionData("SELECT", "SQL Keyword: SELECT", 10.0, false); - var snipData = new Fdb.Services.SqlCompletionData("SELECT * FROM", "Select all columns", 8.0, true, "SELECT * FROM "); + var kwData = new Quell.Services.SqlCompletionData("SELECT", "SQL Keyword: SELECT", 10.0, false); + var snipData = new Quell.Services.SqlCompletionData("SELECT * FROM", "Select all columns", 8.0, true, "SELECT * FROM "); Console.WriteLine($"kwData: Text={kwData.Text}, IsSnippet={kwData.IsSnippet}, Category={kwData.Category}"); Console.WriteLine($"snipData: Text={snipData.Text}, IsSnippet={snipData.IsSnippet}, InsertionText='{snipData.InsertionText}'"); @@ -547,7 +561,7 @@ public static async Task Main(string[] args) te.CaretOffset = 1; editorForTest.UpdateAndShowCompletion("S", showAll: false); - var overlay = editorForTest.FindControl("IntelliSenseOverlay")!; + var overlay = editorForTest.FindControl("IntelliSenseOverlay")!; Console.WriteLine($"After typing 'S': Overlay.IsVisible = {overlay.IsVisible}, ItemCount = {overlay.ItemCount}"); if (!overlay.IsVisible || overlay.ItemCount == 0) { @@ -582,7 +596,7 @@ public static async Task Main(string[] args) // Test MainEditor in Window Console.WriteLine("\n[Action] Verifying MainEditor in Window visual tree..."); - var mainEditor = window.FindControl("MainEditor"); + var mainEditor = window.FindControl("MainEditor"); Console.WriteLine($"MainEditor found in window: {mainEditor != null}"); var mainCodeEditor = mainEditor?.FindControl("CodeEditor"); Console.WriteLine($"MainCodeEditor found: {mainCodeEditor != null}"); @@ -601,7 +615,7 @@ public static async Task Main(string[] args) }; mainCodeEditor.TextArea.RaiseEvent(textArgs); - var winOverlay = mainEditor.FindControl("IntelliSenseOverlay")!; + var winOverlay = mainEditor.FindControl("IntelliSenseOverlay")!; Console.WriteLine($"Window MainEditor after typing 's': Overlay.IsVisible={winOverlay.IsVisible}, ItemCount={winOverlay.ItemCount}"); var winTopItem = winOverlay.SelectedItem; Console.WriteLine($"Window MainEditor TopItem: '{winTopItem?.Text}'"); @@ -639,7 +653,7 @@ public static async Task Main(string[] args) // 9. Test Splash Screen Lifecycle & SQLite Startup Execution Console.WriteLine("\n[Action] Verifying Splash Screen Window & SQL Startup Execution..."); - var splashWindow = new Fdb.Views.SplashScreenWindow(); + var splashWindow = new Quell.Views.SplashScreenWindow(); Console.WriteLine($"Splash initial status: '{splashWindow.ViewModel.StatusText}', Progress: {splashWindow.ViewModel.ProgressValue}%"); await splashWindow.RunStartupSequenceAsync().ConfigureAwait(false); Console.WriteLine($"Splash completed status: '{splashWindow.ViewModel.StatusText}', Progress: {splashWindow.ViewModel.ProgressValue}%"); @@ -651,7 +665,7 @@ public static async Task Main(string[] args) // 10. Test SQLite Local Storage Service & Dialect Keywords Console.WriteLine("\n[Action] Verifying SQLite Local Storage Service & Multi-Dialect Keywords..."); - var storageService = Fdb.Services.DatabaseStorageService.Instance; + var storageService = Quell.Services.DatabaseStorageService.Instance; await storageService.InitializeAsync(); Console.WriteLine($"SQLite DatabasePath: '{storageService.DatabasePath}'"); @@ -718,19 +732,19 @@ public static async Task Main(string[] args) // Test Dynamic Dialect Switching in SqlKeywordsService Console.WriteLine("\n[Action] Testing Dynamic Dialect Switching in SqlKeywordsService..."); - await Fdb.Services.SqlKeywordsService.Instance.LoadKeywordsFromDatabaseAsync("PostgreSQL"); - if (Fdb.Services.SqlKeywordsService.Instance.CurrentDialect != "PostgreSQL" || !Fdb.Services.SqlKeywordsService.Instance.Keywords.Contains("RETURNING")) + await Quell.Services.SqlKeywordsService.Instance.LoadKeywordsFromDatabaseAsync("PostgreSQL"); + if (Quell.Services.SqlKeywordsService.Instance.CurrentDialect != "PostgreSQL" || !Quell.Services.SqlKeywordsService.Instance.Keywords.Contains("RETURNING")) { throw new Exception("SqlKeywordsService failed to switch to PostgreSQL dialect!"); } - Console.WriteLine($"Switched to PostgreSQL. Contains 'RETURNING': {Fdb.Services.SqlKeywordsService.Instance.Keywords.Contains("RETURNING")}"); + Console.WriteLine($"Switched to PostgreSQL. Contains 'RETURNING': {Quell.Services.SqlKeywordsService.Instance.Keywords.Contains("RETURNING")}"); - await Fdb.Services.SqlKeywordsService.Instance.LoadKeywordsFromDatabaseAsync("MSSQL"); - if (Fdb.Services.SqlKeywordsService.Instance.CurrentDialect != "MSSQL" || !Fdb.Services.SqlKeywordsService.Instance.Keywords.Contains("TOP")) + await Quell.Services.SqlKeywordsService.Instance.LoadKeywordsFromDatabaseAsync("MSSQL"); + if (Quell.Services.SqlKeywordsService.Instance.CurrentDialect != "MSSQL" || !Quell.Services.SqlKeywordsService.Instance.Keywords.Contains("TOP")) { throw new Exception("SqlKeywordsService failed to switch back to MSSQL dialect!"); } - Console.WriteLine($"Switched back to MSSQL. Contains 'TOP': {Fdb.Services.SqlKeywordsService.Instance.Keywords.Contains("TOP")}"); + Console.WriteLine($"Switched back to MSSQL. Contains 'TOP': {Quell.Services.SqlKeywordsService.Instance.Keywords.Contains("TOP")}"); var connections = await storageService.GetRecentConnectionsAsync(); Console.WriteLine($"SQLite Connections loaded: {connections.Count} connection(s)."); @@ -750,7 +764,7 @@ public static async Task Main(string[] args) // 11. Test Dynamic Database Providers & Connect Modal Engine Switching Console.WriteLine("\n[Action] Verifying Dynamic Database Providers & Engine Switching..."); - var registry = Fdb.Services.DatabaseProviders.DatabaseProviderRegistry.Instance; + var registry = Quell.Services.DatabaseProviders.DatabaseProviderRegistry.Instance; var allProviders = registry.GetAllProviders(); Console.WriteLine($"Registered Providers: {string.Join(", ", System.Linq.Enumerable.Select(allProviders, p => p.Id))}"); if (allProviders.Count < 4) @@ -759,7 +773,7 @@ public static async Task Main(string[] args) } var mssqlProv = registry.GetProvider("MSSQL"); - var connStr = mssqlProv.BuildConnectionString(new Fdb.Services.DatabaseProviders.DatabaseConnectionConfig + var connStr = mssqlProv.BuildConnectionString(new Quell.Services.DatabaseProviders.DatabaseConnectionConfig { Server = "127.0.0.1", Port = 1433, @@ -777,7 +791,7 @@ public static async Task Main(string[] args) // Test SQLite Provider In-Memory var sqliteProv = registry.GetProvider("SQLite"); - var sqliteTest = await sqliteProv.TestConnectionAsync(new Fdb.Services.DatabaseProviders.DatabaseConnectionConfig + var sqliteTest = await sqliteProv.TestConnectionAsync(new Quell.Services.DatabaseProviders.DatabaseConnectionConfig { FilePath = ":memory:" }); @@ -832,7 +846,7 @@ public static async Task Main(string[] args) // 13. Test Hierarchical Tree View: Adding connection, clicking to connect, displaying tables in tree Console.WriteLine("\n[Action] Verifying Hierarchical Tree View & Connecting from Tree..."); - int testDbId = await Fdb.Services.DatabaseStorageService.Instance.SaveConnectionAsync( + int testDbId = await Quell.Services.DatabaseStorageService.Instance.SaveConnectionAsync( ":memory:", 0, "TestStudioDB", "None", "sa", "SQLite"); await vm.Sidebar.RefreshAsync(); await vm.RefreshAvailableDatabasesAsync(); @@ -865,15 +879,15 @@ public static async Task Main(string[] args) Console.WriteLine($"Tree Folder: '{testDbNode.TablesFolder.Title}', Child items count={testDbNode.TablesFolder.Children.Count}"); // Create a table with rows in the connected database and verify it appears in the tree - await Fdb.Services.DatabaseProviders.DatabaseSessionService.Instance.ExecuteQueryAsync("CREATE TABLE Users (Id INTEGER PRIMARY KEY, Name TEXT);", testDbNode.ConnectionKey); - await Fdb.Services.DatabaseProviders.DatabaseSessionService.Instance.ExecuteQueryAsync("INSERT INTO Users (Id, Name) VALUES (1, 'Alice'), (2, 'Bob');", testDbNode.ConnectionKey); + await Quell.Services.DatabaseProviders.DatabaseSessionService.Instance.ExecuteQueryAsync("CREATE TABLE Users (Id INTEGER PRIMARY KEY, Name TEXT);", testDbNode.ConnectionKey); + await Quell.Services.DatabaseProviders.DatabaseSessionService.Instance.ExecuteQueryAsync("INSERT INTO Users (Id, Name) VALUES (1, 'Alice'), (2, 'Bob');", testDbNode.ConnectionKey); await vm.Sidebar.RefreshDatabaseNodeAsync(testDbNode); Console.WriteLine($"After table creation: Tree Folder '{testDbNode.TablesFolder.Title}', Child count={testDbNode.TablesFolder.Children.Count}"); if (testDbNode.TablesFolder.Children.Count == 0) { throw new Exception("Expected at least 1 table in tree folder after CREATE TABLE!"); } - var tableItem = testDbNode.TablesFolder.Children[0] as Fdb.ViewModels.TableTreeNode; + var tableItem = testDbNode.TablesFolder.Children[0] as Quell.ViewModels.TableTreeNode; Console.WriteLine($"Found Table in Tree: {tableItem?.FullName}"); if (tableItem?.TableName != "Users") { @@ -915,7 +929,7 @@ public static async Task Main(string[] args) { throw new Exception($"Expected 1 matching table for 'user', got {testDbNode.TablesFolder.Children.Count}!"); } - if (((Fdb.ViewModels.TableTreeNode)testDbNode.TablesFolder.Children[0]).TableName != "Users") + if (((Quell.ViewModels.TableTreeNode)testDbNode.TablesFolder.Children[0]).TableName != "Users") { throw new Exception("Filtered table did not match 'Users'!"); } @@ -945,14 +959,14 @@ public static async Task Main(string[] args) // 13.5 Test Table Auto-Completion from Active Database (e.g. SELECT * FROM u -> Users) Console.WriteLine("\n[Action] Verifying Active Database Table Auto-Completion in Editor..."); - Console.WriteLine($"Active Database in KeywordsService: '{Fdb.Services.SqlKeywordsService.Instance.ActiveDatabaseName}', Tables Count: {Fdb.Services.SqlKeywordsService.Instance.ActiveTables.Count}"); - if (Fdb.Services.SqlKeywordsService.Instance.ActiveTables.Count != 1 || Fdb.Services.SqlKeywordsService.Instance.ActiveTables[0].TableName != "Users") + Console.WriteLine($"Active Database in KeywordsService: '{Quell.Services.SqlKeywordsService.Instance.ActiveDatabaseName}', Tables Count: {Quell.Services.SqlKeywordsService.Instance.ActiveTables.Count}"); + if (Quell.Services.SqlKeywordsService.Instance.ActiveTables.Count != 1 || Quell.Services.SqlKeywordsService.Instance.ActiveTables[0].TableName != "Users") { - throw new Exception($"Expected active tables to contain 'Users', got {Fdb.Services.SqlKeywordsService.Instance.ActiveTables.Count} table(s)!"); + throw new Exception($"Expected active tables to contain 'Users', got {Quell.Services.SqlKeywordsService.Instance.ActiveTables.Count} table(s)!"); } - bool isFromTableContext = Fdb.Components.SqlIntelliSenseOverlayComponent.IsTableContext("SELECT * FROM "); - bool isJoinTableContext = Fdb.Components.SqlIntelliSenseOverlayComponent.IsTableContext("SELECT * FROM Orders JOIN "); - bool isSelectContext = Fdb.Components.SqlIntelliSenseOverlayComponent.IsTableContext("SELECT "); + bool isFromTableContext = Quell.Components.SqlIntelliSenseOverlayComponent.IsTableContext("SELECT * FROM "); + bool isJoinTableContext = Quell.Components.SqlIntelliSenseOverlayComponent.IsTableContext("SELECT * FROM Orders JOIN "); + bool isSelectContext = Quell.Components.SqlIntelliSenseOverlayComponent.IsTableContext("SELECT "); Console.WriteLine($"Context Detection: 'FROM ' -> {isFromTableContext}, 'JOIN ' -> {isJoinTableContext}, 'SELECT ' -> {isSelectContext}"); if (!isFromTableContext || !isJoinTableContext || isSelectContext) { @@ -982,6 +996,86 @@ public static async Task Main(string[] args) throw new Exception($"Expected dynamic column values '42' and 'Antigravity', got '{resultRow["Answer"]}' and '{resultRow["Assistant"]}'!"); } + // 14.1 Test Selective Query Execution (Selected Text vs Full Text) + Console.WriteLine("\n[Action] Verifying Selective Text Execution in Code Editor..."); + vm.Editor.SelectedTab.Text = "SELECT 'Full' AS ExecMode; SELECT 'Partial' AS ExecMode;"; + + // Case A: A sub-part of query is selected with mouse/keyboard + vm.Editor.SelectedTab.SelectedText = "SELECT 'Partial' AS ExecMode;"; + Console.WriteLine($"Editor Selection Set: '{vm.Editor.SelectedTab.SelectedText}'"); + string resolvedQuery = vm.Editor.GetQueryToExecute(); + Console.WriteLine($"Resolved Query to Execute: '{resolvedQuery}'"); + if (resolvedQuery != "SELECT 'Partial' AS ExecMode;") + { + throw new Exception($"Expected selected query 'SELECT 'Partial' AS ExecMode;', but got '{resolvedQuery}'"); + } + await vm.ExecuteQuery(); + if (vm.SampleQueryResult.Rows.Count == 0 || vm.SampleQueryResult.Rows[0]["ExecMode"] != "Partial") + { + throw new Exception($"Expected partial selection result 'Partial', but got '{vm.SampleQueryResult.Rows.FirstOrDefault()?["ExecMode"]}'!"); + } + Console.WriteLine("[Success] Partial selected text executed successfully!"); + + // Case B: No text selected (selection cleared) -> Full editor content executed + vm.Editor.SelectedTab.SelectedText = string.Empty; + string fallbackQuery = vm.Editor.GetQueryToExecute(); + Console.WriteLine($"Fallback Query (no selection): '{fallbackQuery}'"); + if (fallbackQuery != "SELECT 'Full' AS ExecMode; SELECT 'Partial' AS ExecMode;") + { + throw new Exception($"Expected full query text fallback, but got '{fallbackQuery}'"); + } + Console.WriteLine("[Success] Full text fallback execution verified when nothing is selected!"); + + // 14.2 Test Database Tools in Hamburger Menu: Excel Table Export, Backup, ER Diagram, DDL + Console.WriteLine("\n[Action] Verifying Database Tools: Excel Export, Backup, ER Diagram & DDL..."); + + // A. Quick Export to Excel from Table + vm.QuickExportExcel(); + Console.WriteLine($"Quick Export Excel Modal State: IsOpen={vm.ExportModal.IsOpen}, Format='{vm.ExportModal.SelectedFormat}', Source='{vm.ExportModal.ExportSource}', Tables={vm.ExportModal.AvailableTables.Count}"); + if (!vm.ExportModal.IsOpen || !vm.ExportModal.SelectedFormat.Contains("Excel") || vm.ExportModal.AvailableTables.Count == 0) + { + throw new Exception("QuickExportExcel failed to configure ExportModal properly!"); + } + await vm.ExportModal.ExportAsync(); + Console.WriteLine($"Export Status: {vm.ExportModal.StatusMessage}, File: {vm.ExportModal.FilePath}"); + if (System.IO.File.Exists(vm.ExportModal.FilePath)) + { + Console.WriteLine($"Exported file verified on disk: size = {new System.IO.FileInfo(vm.ExportModal.FilePath).Length} bytes"); + } + vm.ExportModal.Close(); + + // B. Full Database Backup + await vm.BackupDatabase(); + Console.WriteLine($"Backup Status: {vm.SampleQueryResult.LastQueryMessage}"); + if (!vm.SampleQueryResult.LastQueryMessage.Contains("Backup")) + { + throw new Exception("BackupDatabase failed to execute and report status!"); + } + + // C. Interactive ER Diagram & Schema Relations + await vm.OpenDiagramModal(); + Console.WriteLine($"ER Diagram Modal State: IsOpen={vm.DiagramModal.IsOpen}, Total Tables={vm.DiagramModal.TotalTablesCount}, Relations={vm.DiagramModal.TotalRelationsCount}"); + if (!vm.DiagramModal.IsOpen || vm.DiagramModal.TotalTablesCount == 0) + { + throw new Exception("OpenDiagramModal failed to load tables and open dialog!"); + } + await vm.DiagramModal.CopyMermaidDiagramAsync(); + Console.WriteLine($"Mermaid Diagram status: {vm.DiagramModal.StatusMessage}"); + vm.DiagramModal.Close(); + + // D. Generate Table DDL + vm.GenerateTableDdl(); + Console.WriteLine($"Generated DDL Tab: Title='{vm.Editor.SelectedTab.Title}', Content Length={vm.Editor.SelectedTab.Text.Length}"); + if (!vm.Editor.SelectedTab.Text.Contains("CREATE TABLE")) + { + throw new Exception("GenerateTableDdl failed to create DDL script!"); + } + + // E. Database Optimization & Vacuum + await vm.OptimizeDatabase(); + Console.WriteLine($"Optimize Status: {vm.SampleQueryResult.LastQueryMessage}"); + Console.WriteLine("[Success] Dedicated database tools (Excel, Backup, Diagram, DDL, Optimize) verified!"); + // 15. Test Saving Connections in Connect Modal and SQLite Persistence Console.WriteLine("\n[Action] Verifying Connection Persistence in SQLite..."); int initialSaved = vm.Sidebar.SavedConnections.Count; @@ -991,7 +1085,7 @@ public static async Task Main(string[] args) vm.ConnectModal.Database = "SalesDB"; vm.ConnectModal.Username = "sales_admin"; // Persist connection - await Fdb.Services.DatabaseStorageService.Instance.SaveConnectionAsync( + await Quell.Services.DatabaseStorageService.Instance.SaveConnectionAsync( vm.ConnectModal.Server, vm.ConnectModal.Port, vm.ConnectModal.Database, vm.ConnectModal.AuthType, vm.ConnectModal.Username, "MSSQL"); await vm.Sidebar.RefreshAsync(); @@ -1017,7 +1111,7 @@ await Fdb.Services.DatabaseStorageService.Instance.SaveConnectionAsync( } // Update database name in modal and save vm.ConnectModal.Database = "SalesDB_Updated"; - await Fdb.Services.DatabaseStorageService.Instance.UpdateConnectionAsync( + await Quell.Services.DatabaseStorageService.Instance.UpdateConnectionAsync( vm.ConnectModal.EditingConnectionId.Value, vm.ConnectModal.Server, vm.ConnectModal.Port, vm.ConnectModal.Database, vm.ConnectModal.AuthType, vm.ConnectModal.Username, vm.ConnectModal.SelectedDatabaseType); @@ -1055,7 +1149,7 @@ await Fdb.Services.DatabaseStorageService.Instance.UpdateConnectionAsync( // [Action] Verifying Graphical Cell Editor & Query Generator in Footer Console.WriteLine("\n[Action] Verifying Graphical Cell Editor & UPDATE Query Generator..."); - var testUserRow = new Fdb.ViewModels.QueryResultRow + var testUserRow = new Quell.ViewModels.QueryResultRow { RowIndex = 1, Id = 101, @@ -1067,7 +1161,7 @@ await Fdb.Services.DatabaseStorageService.Instance.UpdateConnectionAsync( testUserRow["Email"] = "alice@example.com"; vm.SampleQueryResult.SetResults( new System.Collections.Generic.List { "Id", "Name", "Email" }, - new System.Collections.Generic.List { testUserRow }, + new System.Collections.Generic.List { testUserRow }, "5 ms"); vm.SampleQueryResult.CurrentTableName = "Users"; var firstRow = vm.SampleQueryResult.Rows[0]; @@ -1110,45 +1204,45 @@ await Fdb.Services.DatabaseStorageService.Instance.UpdateConnectionAsync( } Console.WriteLine($"[Success] Cell graphically edited, query executed, and reflected in grid and editor: '{vm.SampleQueryResult.LastQueryMessage}'"); - // Verify Persian / Unicode Text UPDATE & Literal Encoding - Console.WriteLine("\n[Action] Verifying Persian / Unicode Text UPDATE Query Generation & Execution..."); + // Verify Unicode / Multilingual Text UPDATE & Literal Encoding + Console.WriteLine("\n[Action] Verifying Unicode / Multilingual Text UPDATE Query Generation & Execution..."); // Verify FormatLiteral across dialects - string mssqlPersian = Fdb.ViewModels.EditCellModalViewModel.FormatLiteral("احسان رضایی", "MSSQL"); - string sqlitePersian = Fdb.ViewModels.EditCellModalViewModel.FormatLiteral("احسان رضایی", "SQLITE"); - string mysqlPersian = Fdb.ViewModels.EditCellModalViewModel.FormatLiteral("احسان رضایی", "MYSQL"); - string pgPersian = Fdb.ViewModels.EditCellModalViewModel.FormatLiteral("احسان رضایی", "POSTGRESQL"); - Console.WriteLine($"Persian Formatted Literals:\nMSSQL: {mssqlPersian}\nSQLite: {sqlitePersian}\nMySQL: {mysqlPersian}\nPostgres: {pgPersian}"); - if (mssqlPersian != "N'احسان رضایی'") throw new Exception($"MSSQL must use N prefix for Persian text! Got '{mssqlPersian}'"); - if (sqlitePersian != "'احسان رضایی'") throw new Exception($"SQLite must use standard quotes without N prefix! Got '{sqlitePersian}'"); - if (mysqlPersian != "N'احسان رضایی'") throw new Exception($"MySQL must use N prefix for Unicode! Got '{mysqlPersian}'"); - if (pgPersian != "N'احسان رضایی'") throw new Exception($"Postgres must use N prefix for Unicode! Got '{pgPersian}'"); + string mssqlUnicode = Quell.ViewModels.EditCellModalViewModel.FormatLiteral("José García", "MSSQL"); + string sqliteUnicode = Quell.ViewModels.EditCellModalViewModel.FormatLiteral("José García", "SQLITE"); + string mysqlUnicode = Quell.ViewModels.EditCellModalViewModel.FormatLiteral("José García", "MYSQL"); + string pgUnicode = Quell.ViewModels.EditCellModalViewModel.FormatLiteral("José García", "POSTGRESQL"); + Console.WriteLine($"Unicode Formatted Literals:\nMSSQL: {mssqlUnicode}\nSQLite: {sqliteUnicode}\nMySQL: {mysqlUnicode}\nPostgres: {pgUnicode}"); + if (mssqlUnicode != "N'José García'") throw new Exception($"MSSQL must use N prefix for Unicode text! Got '{mssqlUnicode}'"); + if (sqliteUnicode != "'José García'") throw new Exception($"SQLite must use standard quotes without N prefix! Got '{sqliteUnicode}'"); + if (mysqlUnicode != "N'José García'") throw new Exception($"MySQL must use N prefix for Unicode! Got '{mysqlUnicode}'"); + if (pgUnicode != "N'José García'") throw new Exception($"Postgres must use N prefix for Unicode! Got '{pgUnicode}'"); - // Verify MSSQL UPDATE query with N prefix for Persian text - string mssqlUpdate = Fdb.ViewModels.EditCellModalViewModel.BuildUpdateQuery("Users", "Name", "احسان رضایی", firstRow, "MSSQL"); - Console.WriteLine($"Generated MSSQL Query for Persian Name:\n{mssqlUpdate}"); - if (!mssqlUpdate.Contains("N'احسان رضایی'") || !mssqlUpdate.Contains("SET [Name] = N'احسان رضایی'")) + // Verify MSSQL UPDATE query with N prefix for Unicode text + string mssqlUpdate = Quell.ViewModels.EditCellModalViewModel.BuildUpdateQuery("Users", "Name", "José García", firstRow, "MSSQL"); + Console.WriteLine($"Generated MSSQL Query for Unicode Name:\n{mssqlUpdate}"); + if (!mssqlUpdate.Contains("N'José García'") || !mssqlUpdate.Contains("SET [Name] = N'José García'")) { - throw new Exception($"MSSQL UPDATE query failed to contain 'SET [Name] = N\'احسان رضایی\'', got:\n{mssqlUpdate}"); + throw new Exception($"MSSQL UPDATE query failed to contain 'SET [Name] = N\'José García\'', got:\n{mssqlUpdate}"); } // Test executing in the current active session (SQLite) vm.SampleQueryResult.OpenEditCell(firstRow, "Name"); - editModal.NewValue = "احسان رضایی"; + editModal.NewValue = "José García"; Console.WriteLine($"Generated Active DB Query ({editModal.DatabaseType}):\n{editModal.GeneratedQuery}"); - if (!editModal.GeneratedQuery.Contains("'احسان رضایی'")) + if (!editModal.GeneratedQuery.Contains("'José García'")) { - throw new Exception($"Expected active query to contain Persian text, got:\n{editModal.GeneratedQuery}"); + throw new Exception($"Expected active query to contain Unicode text, got:\n{editModal.GeneratedQuery}"); } await editModal.ExecuteAndSaveAsync(); - if (editModal.IsOpen || firstRow.Name != "احسان رضایی" || firstRow["Name"] != "احسان رضایی") + if (editModal.IsOpen || firstRow.Name != "José García" || firstRow["Name"] != "José García") { - throw new Exception($"Row was not updated to Persian 'احسان رضایی'! Got Name='{firstRow.Name}'"); + throw new Exception($"Row was not updated to Unicode 'José García'! Got Name='{firstRow.Name}'"); } - if (vm.Editor.SelectedTab == null || !vm.Editor.SelectedTab.Text.Contains("احسان رضایی")) + if (vm.Editor.SelectedTab == null || !vm.Editor.SelectedTab.Text.Contains("José García")) { - throw new Exception("SQL Editor did not receive the executed Persian UPDATE statement!"); + throw new Exception("SQL Editor did not receive the executed Unicode UPDATE statement!"); } - Console.WriteLine("[Success] Persian / Unicode text editing and UPDATE query generation verified!"); + Console.WriteLine("[Success] Unicode / Multilingual text editing and UPDATE query generation verified!"); // [Action] Verifying Primary Key Visual Distinctiveness (Badges & Headers) Console.WriteLine("\n[Action] Verifying Primary Key UI Highlighting & Detection..."); @@ -1241,7 +1335,7 @@ await Fdb.Services.DatabaseStorageService.Instance.UpdateConnectionAsync( } // 3. Test Refresh on Active Database Session - var activeSession = Fdb.Services.DatabaseProviders.DatabaseSessionService.Instance.ActiveSession; + var activeSession = Quell.Services.DatabaseProviders.DatabaseSessionService.Instance.ActiveSession; if (activeSession != null) { Console.WriteLine($"[Action] Testing Refresh on active session: {activeSession.DisplayName}..."); @@ -1384,14 +1478,14 @@ await Fdb.Services.DatabaseStorageService.Instance.UpdateConnectionAsync( } // C. Verify heuristic FK detection on other common schema patterns - var deptFk = Fdb.ViewModels.QueryResultTableViewModel.DetectProbableForeignKey("department_id", "Employees"); + var deptFk = Quell.ViewModels.QueryResultTableViewModel.DetectProbableForeignKey("department_id", "Employees"); Console.WriteLine($"Heuristic FK 'department_id': Table={deptFk?.TargetTable}, Col={deptFk?.TargetColumn}"); if (deptFk == null || deptFk.TargetTable != "Departments" || deptFk.TargetColumn != "Id") { throw new Exception("Heuristic FK detection failed for 'department_id'!"); } - var custFk = Fdb.ViewModels.QueryResultTableViewModel.DetectProbableForeignKey("customer_id", "Orders"); + var custFk = Quell.ViewModels.QueryResultTableViewModel.DetectProbableForeignKey("customer_id", "Orders"); Console.WriteLine($"Heuristic FK 'customer_id': Table={custFk?.TargetTable}, Col={custFk?.TargetColumn}"); if (custFk == null || custFk.TargetTable != "Customers" || custFk.TargetColumn != "Id") { @@ -1427,20 +1521,20 @@ await Fdb.Services.DatabaseStorageService.Instance.UpdateConnectionAsync( throw new Exception($"Referenced row was not selected or title was unexpected: {vm.SampleQueryResult.SelectedRow?["TITLE"]}"); } Console.WriteLine("[Success] Foreign Key visual indicators and arrow navigation to referenced table verified!"); -// E. Test Persian & English Course (دوره) -> Users (کاربران) Navigation - Console.WriteLine("\n[Action] Testing Course (دوره) Foreign Key -> User Navigation..."); - var courseFk = Fdb.ViewModels.QueryResultTableViewModel.DetectProbableForeignKey("دوره"); - Console.WriteLine($"DetectProbableForeignKey('دوره'): TargetTable={courseFk?.TargetTable}, TargetColumn={courseFk?.TargetColumn}"); + // E. Test Course -> Users Foreign Key Navigation + Console.WriteLine("\n[Action] Testing Course Foreign Key -> User Navigation..."); + var courseFk = Quell.ViewModels.QueryResultTableViewModel.DetectProbableForeignKey("Course"); + Console.WriteLine($"DetectProbableForeignKey('Course'): TargetTable={courseFk?.TargetTable}, TargetColumn={courseFk?.TargetColumn}"); if (courseFk == null || courseFk.TargetTable != "Users") { - throw new Exception("Foreign Key detection failed for 'دوره' linking to Users!"); + throw new Exception("Foreign Key detection failed for 'Course' linking to Users!"); } - var studentFk = Fdb.ViewModels.QueryResultTableViewModel.DetectProbableForeignKey("نام_دانشجو"); - Console.WriteLine($"DetectProbableForeignKey('نام_دانشجو'): TargetTable={studentFk?.TargetTable}, TargetColumn={studentFk?.TargetColumn}"); + var studentFk = Quell.ViewModels.QueryResultTableViewModel.DetectProbableForeignKey("student_name"); + Console.WriteLine($"DetectProbableForeignKey('student_name'): TargetTable={studentFk?.TargetTable}, TargetColumn={studentFk?.TargetColumn}"); if (studentFk == null || studentFk.TargetTable != "Users") { - throw new Exception("Foreign Key detection failed for 'نام_دانشجو' linking to Users!"); + throw new Exception("Foreign Key detection failed for 'student_name' linking to Users!"); } // Load Courses table @@ -1453,7 +1547,7 @@ await Fdb.Services.DatabaseStorageService.Instance.UpdateConnectionAsync( // Click navigation arrow on course 'C# & Avalonia UI' Console.WriteLine("[Action] Navigating from Course 'C# & Avalonia UI' to User via Foreign Key..."); - await vm.SampleQueryResult.NavigateToForeignKeyAsync("دوره", "C# & Avalonia UI"); + await vm.SampleQueryResult.NavigateToForeignKeyAsync("TITLE", "C# & Avalonia UI"); Console.WriteLine($"After Course->User Navigation: CurrentTable='{vm.SampleQueryResult.CurrentTableName}', SelectedUser='{vm.SampleQueryResult.SelectedRow?.Name}'"); Console.WriteLine($"Generated Query: '{vm.Editor.SelectedTab?.Text}'"); @@ -1471,9 +1565,249 @@ await Fdb.Services.DatabaseStorageService.Instance.UpdateConnectionAsync( { throw new Exception("Editor failed to receive User selection query for Course!"); } - Console.WriteLine("[Success] Course (دوره) -> User navigation and item retrieval verified!"); + Console.WriteLine("[Success] Course -> User navigation and item retrieval verified!"); + + // [Action] Verifying Hardware Graphics Adapter Detection, GPU vs CPU Modes & SQLite Persistence + Console.WriteLine("\n[Action] Verifying Hardware Graphics Adapter Detection, GPU/CPU Modes & SQLite Persistence..."); + var graphicsService = Quell.Services.HardwareGraphicsService.Instance; + graphicsService.DetectHardwareGpu(); + Console.WriteLine($"Detected GPU Adapter: '{graphicsService.GpuAdapterName}', IsGpuDetected={graphicsService.IsGpuDetected}"); + if (string.IsNullOrWhiteSpace(graphicsService.GpuAdapterName)) + { + throw new Exception("Hardware graphics adapter detection returned empty adapter name!"); + } + + // Verify effective mode resolution + string resolvedGpu = graphicsService.ResolveEffectiveMode("GPU"); + string resolvedCpu = graphicsService.ResolveEffectiveMode("CPU"); + string resolvedAuto = graphicsService.ResolveEffectiveMode("Auto"); + Console.WriteLine($"Resolved Modes: GPU='{resolvedGpu}', CPU='{resolvedCpu}', Auto='{resolvedAuto}'"); + if (resolvedGpu != "GPU" || resolvedCpu != "CPU") + { + throw new Exception("Effective mode resolution for GPU/CPU failed!"); + } + if (resolvedAuto != (graphicsService.IsGpuDetected ? "GPU" : "CPU")) + { + throw new Exception("Auto mode resolution did not match hardware detection state!"); + } + + // Verify splash screen initialization routine + await graphicsService.InitializeAsync(); + Console.WriteLine($"Initialized Summary: '{graphicsService.ActiveRendererSummary}', VRAM Cache={graphicsService.VramCacheMb}MB"); + if (graphicsService.VramCacheMb != 128 && graphicsService.VramCacheMb != 16) + { + throw new Exception($"Invalid VRAM cache limit: {graphicsService.VramCacheMb}MB"); + } + + // Verify Settings Modal Performance category & live selection + await vm.SettingsModal.OpenAsync(); + vm.SettingsModal.SelectCategory("Performance"); + if (!vm.SettingsModal.IsPerformanceCategory) + { + throw new Exception("Failed to navigate to Performance & GPU settings category!"); + } + + // Test selecting CPU mode & persisting to SQLite + vm.SettingsModal.SelectRenderingMode("CPU"); + if (!vm.SettingsModal.IsCpuRenderingMode || vm.SettingsModal.SelectedRenderingMode != "CPU") + { + throw new Exception("Failed to switch selected rendering mode to CPU!"); + } + await vm.SettingsModal.SaveAsync(); + + string dbModeAfterCpu = await Quell.Services.DatabaseStorageService.Instance.GetGraphicsRenderingModeAsync(); + Console.WriteLine($"SQLite Saved GraphicsRenderingMode: '{dbModeAfterCpu}'"); + if (dbModeAfterCpu != "CPU") + { + throw new Exception($"Expected SQLite to persist 'CPU', got '{dbModeAfterCpu}'!"); + } + + // Test revert on Close() + await vm.SettingsModal.OpenAsync(); + vm.SettingsModal.SelectRenderingMode("GPU"); + vm.SettingsModal.Close(); + if (vm.SettingsModal.SelectedRenderingMode != "CPU") + { + throw new Exception("Close() failed to revert uncommitted rendering mode change!"); + } + + // Reset back to Auto mode and verify SQLite + await vm.SettingsModal.OpenAsync(); + vm.SettingsModal.SelectRenderingMode("Auto"); + await vm.SettingsModal.SaveAsync(); + string dbModeAfterAuto = await Quell.Services.DatabaseStorageService.Instance.GetGraphicsRenderingModeAsync(); + Console.WriteLine($"SQLite Reset GraphicsRenderingMode: '{dbModeAfterAuto}'"); + if (dbModeAfterAuto != "Auto") + { + throw new Exception($"Expected SQLite to persist 'Auto', got '{dbModeAfterAuto}'!"); + } + Console.WriteLine("[Success] Hardware graphics detection, GPU vs CPU mode switching, and SQLite persistence verified!"); + + // ========================================================================= + // F. Test Integrated Cross-Platform Terminal & Database CLI Suite + // ========================================================================= + Console.WriteLine("\n[Action] Testing Integrated Terminal & Database CLI Suite..."); + + // 1. Terminal Component Initialization & Footer Integration + if (vm.Terminal == null) throw new Exception("Terminal ViewModel was not initialized!"); + vm.RestoreFooterTab("Results"); + vm.RestoreFooterTab("Terminal"); + if (!vm.Footer.Tabs.Contains("Results") || !vm.Footer.Tabs.Contains("Terminal")) + { + throw new Exception($"Footer tabs missing Results or Terminal! Found: {string.Join(", ", vm.Footer.Tabs)}"); + } + Console.WriteLine("[Success] Terminal ViewModel and Footer Tabs verified!"); + + // Verify Footer TabItems and active states synchronization + if (vm.Footer.TabItems.Count != vm.Footer.Tabs.Count) + throw new Exception($"Footer TabItems count mismatch: expected {vm.Footer.Tabs.Count}, got {vm.Footer.TabItems.Count}"); + + vm.Footer.SelectTabCommand.Execute("Results"); + var resultsTab = vm.Footer.TabItems.FirstOrDefault(t => t.Name == "Results"); + var terminalTab = vm.Footer.TabItems.FirstOrDefault(t => t.Name == "Terminal"); + if (resultsTab == null || !resultsTab.IsActive || !resultsTab.IsResults) + throw new Exception("Results TabItem active state verification failed!"); + if (terminalTab == null || terminalTab.IsActive || !terminalTab.IsTerminal) + throw new Exception("Terminal TabItem inactive state verification failed!"); + + vm.Footer.SelectTabCommand.Execute("Terminal"); + if (!terminalTab.IsActive || resultsTab.IsActive) + throw new Exception("Switching active footer tab to Terminal failed!"); + + Console.WriteLine("[Success] Footer TabItems and Theme-Synchronized Tab Switching verified!"); + + // 2. Shell Profiles Detection + var shells = Quell.Services.Terminal.ShellDetectorService.Instance.GetAvailableShells(); + Console.WriteLine($"Detected available shells: {shells.Count} ({string.Join(", ", shells.Select(s => s.DisplayName))})"); + if (shells.Count == 0) throw new Exception("No available system shells were detected!"); + var defaultShell = Quell.Services.Terminal.ShellDetectorService.Instance.GetDefaultShell(); + if (defaultShell == null || string.IsNullOrEmpty(defaultShell.ExecutablePath)) + { + throw new Exception("Default shell resolution failed!"); + } + Console.WriteLine($"[Success] Default shell resolved: {defaultShell.DisplayName} ({defaultShell.ExecutablePath})"); + + // 3. Database CLI Resolution Tests (PostgreSQL, MySQL, SQLite, SQL Server) + var pgConfig = new Quell.Services.DatabaseProviders.DatabaseConnectionConfig + { + DatabaseType = "PostgreSQL", + Server = "localhost", + Port = 5432, + Database = "testdb", + Username = "postgres", + Password = "secretpassword" + }; + var pgCli = Quell.Services.Terminal.DatabaseCliService.Instance.ResolveCli(pgConfig); + if (!pgCli.EnvironmentVariables.ContainsKey("PGPASSWORD") || pgCli.EnvironmentVariables["PGPASSWORD"] != "secretpassword") + { + throw new Exception("PostgreSQL CLI failed to set PGPASSWORD environment variable!"); + } + if (pgCli.EnvironmentVariables["DATABASE_URL"] != "postgresql://postgres:secretpassword@localhost:5432/testdb") + { + throw new Exception($"PostgreSQL DATABASE_URL incorrect: {pgCli.EnvironmentVariables["DATABASE_URL"]}"); + } + Console.WriteLine("[Success] PostgreSQL CLI resolution and secure environment variables verified!"); + + var myConfig = new Quell.Services.DatabaseProviders.DatabaseConnectionConfig + { + DatabaseType = "MySQL", + Server = "127.0.0.1", + Port = 3306, + Database = "shopdb", + Username = "root", + Password = "mysqlsecret" + }; + var myCli = Quell.Services.Terminal.DatabaseCliService.Instance.ResolveCli(myConfig); + if (!myCli.EnvironmentVariables.ContainsKey("MYSQL_PWD") || myCli.EnvironmentVariables["MYSQL_PWD"] != "mysqlsecret") + { + throw new Exception("MySQL CLI failed to set MYSQL_PWD environment variable!"); + } + Console.WriteLine("[Success] MySQL CLI resolution and password environment variable verified!"); + + // 4. Terminal Session Lifecycle & Multi-Session Strip + vm.Terminal.Sessions.Clear(); + vm.Terminal.NewSession(defaultShell); + if (vm.Terminal.Sessions.Count != 1) throw new Exception("Failed to add new terminal session!"); + var session1 = vm.Terminal.Sessions[0]; + if (vm.Terminal.ActiveSession != session1 || !session1.IsActive) + { + throw new Exception("Active terminal session was not set as active!"); + } + Console.WriteLine($"[Success] Created Terminal session 1: '{session1.Title}'"); - Console.WriteLine("\n>>> ALL TESTS (LAYOUT, TABLES, MODALS, TABS, SQL AUTOCOMPLETE, MULTI-DIALECT SQLITE, VECTOR ICONS, SPLASH SCREEN, STORAGE, DYNAMIC DATABASE PROVIDERS, ISOLATED STARTUP, HIERARCHICAL TREE VIEW, REAL QUERY, TARGET DATABASE ROUTING, DISCONNECT/DELETE, GRAPHICAL FOOTER CELL UPDATE, PRIMARY KEY NEUTRAL STYLING, MULTI-ROW BATCH DELETION, MULTI-COLUMN DROP, FOOTER REFRESH/FILTER, SIMULTANEOUS BATCH CELL EDITING & FOREIGN KEY NAVIGATION) PASSED! <<<"); + // Add a second session + vm.Terminal.NewSession(defaultShell); + if (vm.Terminal.Sessions.Count != 2) throw new Exception("Failed to create second terminal session!"); + var session2 = vm.Terminal.Sessions[1]; + Console.WriteLine($"[Success] Created Terminal session 2: '{session2.Title}'"); + + // Switch session + vm.Terminal.SelectSession(session1); + if (vm.Terminal.ActiveSession != session1 || !session1.IsActive || session2.IsActive) + { + throw new Exception("Session selection failed!"); + } + + // Close session + vm.Terminal.CloseSession(session2); + if (vm.Terminal.Sessions.Count != 1 || vm.Terminal.ActiveSession != session1) + { + throw new Exception("Session closure failed!"); + } + Console.WriteLine("[Success] Multi-session management (Add, Switch, Close) verified!"); + + // 5. Terminal Keyboard Shortcut & Navigation (Toggle Open, Toggle Close, Toggle Open) + vm.Footer.IsVisible = false; + vm.ToggleTerminalCommand.Execute(null); + if (!vm.Footer.IsVisible || vm.Footer.SelectedTab != "Terminal" || !vm.Footer.IsTerminalTabSelected) + { + throw new Exception("ToggleTerminalCommand failed to activate Terminal tab in Footer when closed!"); + } + + vm.ToggleTerminalCommand.Execute(null); + if (vm.Footer.IsVisible) + { + throw new Exception("ToggleTerminalCommand failed to close Footer when already on Terminal tab!"); + } + + vm.ToggleTerminalCommand.Execute(null); + if (!vm.Footer.IsVisible || vm.Footer.SelectedTab != "Terminal") + { + throw new Exception("ToggleTerminalCommand failed to reopen Terminal tab!"); + } + Console.WriteLine("[Success] ToggleTerminalCommand toggle lifecycle verified!"); + + // Run in Terminal execution + vm.RunSelectionInTerminal("SELECT 'Hello from Quell Terminal';"); + if (vm.Footer.SelectedTab != "Terminal") + { + throw new Exception("RunSelectionInTerminal failed to switch to Terminal tab!"); + } + Console.WriteLine("[Success] RunSelectionInTerminal workflow verified!"); + + // 6. Terminal Preferences Persistence + var testSettings = new Quell.Models.TerminalSettingsModel + { + DefaultShellId = defaultShell.Id, + FontFamily = "JetBrains Mono", + FontSize = 14.5, + CursorStyle = "Underline", + CursorBlink = false, + MaxScrollback = 8000, + PsqlPath = @"C:\Custom\psql.exe" + }; + await Quell.Services.Terminal.TerminalSettingsService.Instance.SaveSettingsAsync(testSettings); + var loadedSettings = await Quell.Services.Terminal.TerminalSettingsService.Instance.LoadSettingsAsync(); + if (loadedSettings.FontFamily != "JetBrains Mono" || loadedSettings.FontSize != 14.5 || loadedSettings.PsqlPath != @"C:\Custom\psql.exe") + { + throw new Exception("TerminalSettingsService persistence test failed!"); + } + Console.WriteLine("[Success] Terminal settings persistence in SQLite verified!"); + + // Restore defaults + await Quell.Services.Terminal.TerminalSettingsService.Instance.SaveSettingsAsync(new Quell.Models.TerminalSettingsModel()); + + Console.WriteLine("\n>>> ALL TESTS (LAYOUT, TABLES, MODALS, TABS, SQL AUTOCOMPLETE, MULTI-DIALECT SQLITE, VECTOR ICONS, SPLASH SCREEN, STORAGE, DYNAMIC DATABASE PROVIDERS, ISOLATED STARTUP, HIERARCHICAL TREE VIEW, REAL QUERY, TARGET DATABASE ROUTING, DISCONNECT/DELETE, GRAPHICAL FOOTER CELL UPDATE, PRIMARY KEY NEUTRAL STYLING, MULTI-ROW BATCH DELETION, MULTI-COLUMN DROP, FOOTER REFRESH/FILTER, SIMULTANEOUS BATCH CELL EDITING, FOREIGN KEY NAVIGATION, HARDWARE GPU/CPU RENDERING PIPELINE & INTEGRATED TERMINAL) PASSED! <<<"); return; } @@ -1482,11 +1816,18 @@ await Fdb.Services.DatabaseStorageService.Instance.UpdateConnectionAsync( } public static AppBuilder BuildAvaloniaApp() - => AppBuilder.Configure() + { + var builder = AppBuilder.Configure() .UsePlatformDetect() #if DEBUG .WithDeveloperTools() #endif .WithInterFont() .LogToTrace(); + + // Hardware Graphics & Rendering Configuration (DirectX/ANGLE GPU vs CPU Software) + Quell.Services.HardwareGraphicsService.Instance.ConfigureAppBuilder(builder); + + return builder; + } } diff --git a/Fdb.Android/AndroidManifest.xml b/Quell.Android/AndroidManifest.xml similarity index 88% rename from Fdb.Android/AndroidManifest.xml rename to Quell.Android/AndroidManifest.xml index b8c1adb..58c1759 100644 --- a/Fdb.Android/AndroidManifest.xml +++ b/Quell.Android/AndroidManifest.xml @@ -1,12 +1,12 @@ - Exe net10.0-android 21 - com.cipherunits.fdb + com.cipherunits.quell 1 1.0.0 apk @@ -21,7 +21,7 @@ - + TargetFramework=net10.0 IsAndroidApp=true diff --git a/Quell.Android/Resources/mipmap/icon.png b/Quell.Android/Resources/mipmap/icon.png new file mode 100644 index 0000000..d7a65d1 Binary files /dev/null and b/Quell.Android/Resources/mipmap/icon.png differ diff --git a/Fdb.Android/Resources/values/colors.xml b/Quell.Android/Resources/values/colors.xml similarity index 100% rename from Fdb.Android/Resources/values/colors.xml rename to Quell.Android/Resources/values/colors.xml diff --git a/Fdb.Android/Resources/values/styles.xml b/Quell.Android/Resources/values/styles.xml similarity index 100% rename from Fdb.Android/Resources/values/styles.xml rename to Quell.Android/Resources/values/styles.xml diff --git a/Fdb.Android/SplashActivity.cs b/Quell.Android/SplashActivity.cs similarity index 93% rename from Fdb.Android/SplashActivity.cs rename to Quell.Android/SplashActivity.cs index fbd9b96..9eb88b0 100644 --- a/Fdb.Android/SplashActivity.cs +++ b/Quell.Android/SplashActivity.cs @@ -2,8 +2,9 @@ using Android.OS; using Avalonia; using Avalonia.Android; +using Quell; -namespace Fdb.Android; +namespace Quell.Android; [Activity( Theme = "@style/MyTheme.Splash", diff --git a/Fdb.csproj b/Quell.csproj similarity index 82% rename from Fdb.csproj rename to Quell.csproj index 12aae77..b589ecc 100644 --- a/Fdb.csproj +++ b/Quell.csproj @@ -9,16 +9,16 @@ 1.0.0 1.0.0.0 1.0.0.0 - Fdb Studio + Quell Modern, Cross-Platform Database Client and SQL IDE CipherUnits CipherUnits - Assets\fdb_icon.ico + Assets\quell_icon.ico - - + + @@ -43,13 +43,14 @@ + - + diff --git a/README.md b/README.md index 7bc7ad0..973120c 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# Fdb Studio +# Quell Modern, high-performance, cross-platform database management tool and SQL IDE built with **Avalonia UI** and **.NET 10**. @@ -8,13 +8,14 @@ Modern, high-performance, cross-platform database management tool and SQL IDE bu - **Cross-Platform**: Runs natively on Windows (`win-x64`), Linux (`linux-x64`), and macOS (`osx-x64` & `osx-arm64`). - **Modern UI & Theming**: Light, Dark, and System Default themes with fully decoupled semantic design tokens (`ThemeTokens.axaml`, `LightTheme.axaml`, `DarkTheme.axaml`). -- **Interactive Color Maker & Workspace Customization**: Built-in Color Maker ("رنگ‌ساز") with bidirectional RGB/HEX conversion, button accent/face/text/border customization, global borders and splitters, multi-part sidebar styling, and one-click designer bundles with real-time live preview. +- **Interactive Color Maker & Workspace Customization**: Built-in Color Maker with bidirectional RGB/HEX conversion, button accent/face/text/border customization, global borders and splitters, multi-part sidebar styling, and one-click designer bundles with real-time live preview. - **Dynamic Syntax Coloring**: User-configurable SQL keyword syntax colors (Blue, Purple, Emerald Green, Amber Gold, Rose Red, Cyan) with automatic WCAG contrast preservation. - **Dynamic Keyboard Shortcuts**: Reconfigurable keyboard shortcut manager with conflict resolution, category filters, and SQLite persistence. - **Embedded SQLite Local Engine**: Powered by **Dapper** and `Microsoft.Data.Sqlite` for local state persistence, query history, saved connections, and dynamic multi-dialect keywords. - **Unified Splash Screen Pre-loading**: Frameless Splash Screen executing real SQLite table provisioning, schema migrations, and batch pre-caching of themes, colors, panel/table customizations, shortcuts, saved connections, and dialect keywords for instant application launch. - **IntelliSense & Autocomplete**: Floating contextual overlay (`SqlIntelliSenseOverlayComponent`) for dialect-aware keyword matching and template code snippets. - **Excel-Grade Grid**: `QueryResultTableComponent` featuring native virtualization, sortable columns, row metrics, search filtering, and pagination. +- **Integrated PTY Terminal**: Full pseudo-terminal emulator with ANSI 256/TrueColor support, multi-session tabs, and 1-click database CLI launcher (`psql`, `mysql`, `sqlcmd`, `sqlite3`). - **Zoom & Shortcuts**: Native `Ctrl + Plus`, `Ctrl + Minus`, and `Ctrl + 0` font scaling inside the text editor. - **Vector Graphics Only**: Exclusively uses Fluent UI vector icons (`FASymbolIcon` and `PathIcon`), completely emoji-free. @@ -38,6 +39,9 @@ The application is decomposed into isolated, testable Avalonia components locate - `ThemeSelectionCardsComponent`: Interactive theme selection cards. - `SyntaxColorSelectorComponent`: Keyword syntax palette swatches with live previews. - `LiveSyntaxPreviewComponent`: Real-time mini code snippet preview. +- `TerminalComponent`: Integrated multi-session PTY terminal workspace with shell switcher and CLI quick actions. +- `TerminalSessionView`: Embedded XTerm / PTY host with missing CLI detection and fallback guidance. +- `TerminalSettingsComponent`: Emulator typography, cursor, and custom database CLI binary paths configuration. For detailed component documentation, refer to [COMPONENTS.md](COMPONENTS.md). @@ -62,12 +66,12 @@ dotnet run -- --verify ## Cross-Platform Packaging & CI/CD -Fdb Studio includes an automated GitHub Actions workflow (`.github/workflows/ci-cd.yml`) that builds, packages, and attaches native installers whenever code is pushed to branch `main`, on pull requests to `main`, or via manual workflow dispatch. +Quell includes an automated GitHub Actions workflow (`.github/workflows/ci-cd.yml`) that builds, packages, and attaches native installers whenever code is pushed to branch `main`, on pull requests to `main`, or via manual workflow dispatch. ### Supported Installer Formats | Platform | Target Runtime | Installer Format | Portable Package | Packaging Tool | | :--- | :--- | :--- | :--- | :--- | -| **Windows** | `win-x64` | `Fdb-Studio-Setup-win-x64.exe` | `.zip` | Inno Setup (`packaging/windows/installer.iss`) | -| **Linux** | `linux-x64` | `fdb-studio_1.0.0_amd64.deb` | `.tar.gz` | `dpkg-deb` (`packaging/linux/build-deb.sh`) | -| **macOS** | `osx-x64` | `Fdb-Studio-1.0.0-osx-x64.dmg` | `.zip` | `hdiutil` (`packaging/macos/build-app.sh`) | -| **macOS** | `osx-arm64` | `Fdb-Studio-1.0.0-osx-arm64.dmg` | `.zip` | `hdiutil` (`packaging/macos/build-app.sh`) | +| **Windows** | `win-x64` | `Quell-Setup-win-x64.exe` | `.zip` | Inno Setup (`packaging/windows/installer.iss`) | +| **Linux** | `linux-x64` | `quell_1.0.0_amd64.deb` | `.tar.gz` | `dpkg-deb` (`packaging/linux/build-deb.sh`) | +| **macOS** | `osx-x64` | `Quell-1.0.0-osx-x64.dmg` | `.zip` | `hdiutil` (`packaging/macos/build-app.sh`) | +| **macOS** | `osx-arm64` | `Quell-1.0.0-osx-arm64.dmg` | `.zip` | `hdiutil` (`packaging/macos/build-app.sh`) | diff --git a/Resources/ClassicStyles.axaml b/Resources/ClassicStyles.axaml index 267edfe..3003126 100644 --- a/Resources/ClassicStyles.axaml +++ b/Resources/ClassicStyles.axaml @@ -7,6 +7,13 @@ + + + + + @@ -14,6 +21,7 @@ - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Resources/Colors.axaml b/Resources/Colors.axaml index 4f90d56..41869bb 100644 --- a/Resources/Colors.axaml +++ b/Resources/Colors.axaml @@ -1,19 +1,19 @@ - - + - + - - - + + + diff --git a/Services/ClipboardService.cs b/Services/ClipboardService.cs new file mode 100644 index 0000000..caf2ce5 --- /dev/null +++ b/Services/ClipboardService.cs @@ -0,0 +1,154 @@ +using System; +using System.Runtime.InteropServices; +using System.Threading; +using System.Threading.Tasks; +using Avalonia; +using Avalonia.Controls; +using Avalonia.Controls.ApplicationLifetimes; +using Avalonia.Input.Platform; +using Avalonia.Threading; + +namespace Quell.Services; + +/// +/// Cross-platform resilient clipboard service that handles Windows OLE/Win32 locks with retries +/// and safe fallback to Avalonia clipboard APIs on UI Dispatcher. +/// +public static class ClipboardService +{ + [DllImport("user32.dll", SetLastError = true)] + private static extern bool OpenClipboard(IntPtr hWndNewOwner); + + [DllImport("user32.dll", SetLastError = true)] + private static extern bool CloseClipboard(); + + [DllImport("user32.dll", SetLastError = true)] + private static extern bool EmptyClipboard(); + + [DllImport("user32.dll", SetLastError = true)] + private static extern IntPtr SetClipboardData(uint uFormat, IntPtr hMem); + + [DllImport("kernel32.dll", SetLastError = true)] + private static extern IntPtr GlobalAlloc(uint uFlags, UIntPtr dwBytes); + + [DllImport("kernel32.dll", SetLastError = true)] + private static extern IntPtr GlobalLock(IntPtr hMem); + + [DllImport("kernel32.dll", SetLastError = true)] + private static extern bool GlobalUnlock(IntPtr hMem); + + [DllImport("kernel32.dll", SetLastError = true)] + private static extern IntPtr GlobalFree(IntPtr hMem); + + private const uint CF_UNICODETEXT = 13; + private const uint GMEM_MOVEABLE = 0x0002; + + /// + /// Copies text to the system clipboard across platforms with retry logic and exception shielding. + /// + public static async Task CopyTextAsync(string text, Visual? visual = null) + { + if (string.IsNullOrEmpty(text)) + { + return false; + } + + // 1. Direct Win32 clipboard API on Windows (immune to Avalonia Dispatcher thread violations & OLE apartment mismatches) + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + if (TrySetTextWindows(text)) + { + return true; + } + } + + // 2. Avalonia Clipboard API fallback (scheduled on Dispatcher.UIThread) + try + { + return await Dispatcher.UIThread.InvokeAsync(async () => + { + IClipboard? clipboard = null; + if (visual != null) + { + clipboard = TopLevel.GetTopLevel(visual)?.Clipboard; + } + + if (clipboard == null && Application.Current?.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop) + { + clipboard = desktop.MainWindow?.Clipboard; + } + + if (clipboard != null) + { + for (int attempt = 0; attempt < 5; attempt++) + { + try + { + await clipboard.SetTextAsync(text); + return true; + } + catch when (attempt < 4) + { + await Task.Delay(50); + } + } + } + + return false; + }); + } + catch (Exception ex) + { + Console.WriteLine($"[ClipboardService] Copy failed: {ex.Message}"); + return false; + } + } + + private static bool TrySetTextWindows(string text) + { + for (int attempt = 0; attempt < 5; attempt++) + { + if (OpenClipboard(IntPtr.Zero)) + { + try + { + EmptyClipboard(); + int bytes = (text.Length + 1) * 2; + IntPtr hGlobal = GlobalAlloc(GMEM_MOVEABLE, (UIntPtr)bytes); + if (hGlobal != IntPtr.Zero) + { + IntPtr target = GlobalLock(hGlobal); + if (target != IntPtr.Zero) + { + try + { + Marshal.Copy(text.ToCharArray(), 0, target, text.Length); + Marshal.WriteInt16(target + (text.Length * 2), 0); + SetClipboardData(CF_UNICODETEXT, hGlobal); + hGlobal = IntPtr.Zero; // ownership successfully transferred to system + return true; + } + finally + { + GlobalUnlock(target); + } + } + + if (hGlobal != IntPtr.Zero) + { + GlobalFree(hGlobal); + } + } + } + finally + { + CloseClipboard(); + } + } + + Thread.Sleep(50); + } + + return false; + } +} diff --git a/Services/DatabaseProviders/ConnectionTestResult.cs b/Services/DatabaseProviders/ConnectionTestResult.cs index e684f3e..cc41554 100644 --- a/Services/DatabaseProviders/ConnectionTestResult.cs +++ b/Services/DatabaseProviders/ConnectionTestResult.cs @@ -1,6 +1,6 @@ -using System; +using System; -namespace Fdb.Services.DatabaseProviders; +namespace Quell.Services.DatabaseProviders; public record ConnectionTestResult { diff --git a/Services/DatabaseProviders/DatabaseConnectionConfig.cs b/Services/DatabaseProviders/DatabaseConnectionConfig.cs index 45982d5..8d3a493 100644 --- a/Services/DatabaseProviders/DatabaseConnectionConfig.cs +++ b/Services/DatabaseProviders/DatabaseConnectionConfig.cs @@ -1,7 +1,7 @@ -using System; +using System; using System.IO; -namespace Fdb.Services.DatabaseProviders; +namespace Quell.Services.DatabaseProviders; public record DatabaseConnectionConfig { diff --git a/Services/DatabaseProviders/DatabaseProviderRegistry.cs b/Services/DatabaseProviders/DatabaseProviderRegistry.cs index c4b4505..8233209 100644 --- a/Services/DatabaseProviders/DatabaseProviderRegistry.cs +++ b/Services/DatabaseProviders/DatabaseProviderRegistry.cs @@ -1,7 +1,7 @@ -using System; +using System; using System.Collections.Generic; -namespace Fdb.Services.DatabaseProviders; +namespace Quell.Services.DatabaseProviders; public class DatabaseProviderRegistry { diff --git a/Services/DatabaseProviders/DatabaseSessionService.cs b/Services/DatabaseProviders/DatabaseSessionService.cs index abdd1b3..cba8d5d 100644 --- a/Services/DatabaseProviders/DatabaseSessionService.cs +++ b/Services/DatabaseProviders/DatabaseSessionService.cs @@ -6,9 +6,9 @@ using System.Linq; using System.Threading; using System.Threading.Tasks; -using Fdb.ViewModels; +using Quell.ViewModels; -namespace Fdb.Services.DatabaseProviders; +namespace Quell.Services.DatabaseProviders; public record QueryExecutionResult { @@ -525,6 +525,71 @@ FROM INFORMATION_SCHEMA.KEY_COLUMN_USAGE } } + public async Task> GetColumnsAsync(string tableName, string? sessionKey = null, CancellationToken cancellationToken = default) + { + if (string.IsNullOrWhiteSpace(tableName)) return new List<(string, string)>(); + + await _connectionLock.WaitAsync(cancellationToken); + try + { + var session = ResolveSession(sessionKey); + if (session == null || session.Connection.State != ConnectionState.Open) + { + return new List<(string, string)>(); + } + + string cleanTable = tableName.Trim('[', ']', '"', '`'); + if (cleanTable.Contains('.')) + { + cleanTable = cleanTable.Split('.')[1]; + } + + var list = new List<(string, string)>(); + + if (session.Config.DatabaseType == "SQLite") + { + using var cmd = session.Connection.CreateCommand(); + cmd.CommandText = $"PRAGMA table_info('{cleanTable.Replace("'", "''")}');"; + using var reader = cmd.ExecuteReader(); + while (reader.Read()) + { + string colName = reader.GetString(1); + string colType = reader.GetString(2); + list.Add((colName, string.IsNullOrWhiteSpace(colType) ? "TEXT" : colType)); + } + } + else + { + string sql = $@" + SELECT COLUMN_NAME, DATA_TYPE + FROM INFORMATION_SCHEMA.COLUMNS + WHERE TABLE_NAME = '{cleanTable.Replace("'", "''")}' + ORDER BY ORDINAL_POSITION;"; + + using var cmd = session.Connection.CreateCommand(); + cmd.CommandText = sql; + using var reader = cmd.ExecuteReader(); + while (reader.Read()) + { + string colName = reader.GetString(0); + string colType = reader.GetString(1); + list.Add((colName, colType)); + } + } + + return list; + } + catch (Exception ex) + { + Console.WriteLine($"[DatabaseSessionService] GetColumnsAsync error: {ex.Message}"); + return new List<(string, string)>(); + } + finally + { + _connectionLock.Release(); + } + } + public async Task> GetForeignKeyRelationsAsync(string tableName, string? sessionKey = null, CancellationToken cancellationToken = default) { if (string.IsNullOrWhiteSpace(tableName)) return new List(); diff --git a/Services/DatabaseProviders/IDatabaseProvider.cs b/Services/DatabaseProviders/IDatabaseProvider.cs index fe56b14..6e9d196 100644 --- a/Services/DatabaseProviders/IDatabaseProvider.cs +++ b/Services/DatabaseProviders/IDatabaseProvider.cs @@ -1,8 +1,8 @@ -using System.Data; +using System.Data; using System.Threading; using System.Threading.Tasks; -namespace Fdb.Services.DatabaseProviders; +namespace Quell.Services.DatabaseProviders; public interface IDatabaseProvider { diff --git a/Services/DatabaseProviders/MySqlDatabaseProvider.cs b/Services/DatabaseProviders/MySqlDatabaseProvider.cs index 364b3e9..2118339 100644 --- a/Services/DatabaseProviders/MySqlDatabaseProvider.cs +++ b/Services/DatabaseProviders/MySqlDatabaseProvider.cs @@ -1,11 +1,11 @@ -using System; +using System; using System.Data; using System.Diagnostics; using System.Threading; using System.Threading.Tasks; using MySqlConnector; -namespace Fdb.Services.DatabaseProviders; +namespace Quell.Services.DatabaseProviders; public class MySqlDatabaseProvider : IDatabaseProvider { diff --git a/Services/DatabaseProviders/PostgreSqlDatabaseProvider.cs b/Services/DatabaseProviders/PostgreSqlDatabaseProvider.cs index a33139e..2bf7356 100644 --- a/Services/DatabaseProviders/PostgreSqlDatabaseProvider.cs +++ b/Services/DatabaseProviders/PostgreSqlDatabaseProvider.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Data; using System.Diagnostics; using System.Linq; @@ -6,7 +6,7 @@ using System.Threading.Tasks; using Npgsql; -namespace Fdb.Services.DatabaseProviders; +namespace Quell.Services.DatabaseProviders; public class PostgreSqlDatabaseProvider : IDatabaseProvider { diff --git a/Services/DatabaseProviders/SqlServerDatabaseProvider.cs b/Services/DatabaseProviders/SqlServerDatabaseProvider.cs index a676c2c..d431dbb 100644 --- a/Services/DatabaseProviders/SqlServerDatabaseProvider.cs +++ b/Services/DatabaseProviders/SqlServerDatabaseProvider.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Data; using System.Diagnostics; using System.Linq; @@ -6,7 +6,7 @@ using System.Threading.Tasks; using Microsoft.Data.SqlClient; -namespace Fdb.Services.DatabaseProviders; +namespace Quell.Services.DatabaseProviders; public class SqlServerDatabaseProvider : IDatabaseProvider { diff --git a/Services/DatabaseProviders/SqliteDatabaseProvider.cs b/Services/DatabaseProviders/SqliteDatabaseProvider.cs index d9e0db1..b0f20cd 100644 --- a/Services/DatabaseProviders/SqliteDatabaseProvider.cs +++ b/Services/DatabaseProviders/SqliteDatabaseProvider.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Data; using System.Diagnostics; using System.IO; @@ -6,7 +6,7 @@ using System.Threading.Tasks; using Microsoft.Data.Sqlite; -namespace Fdb.Services.DatabaseProviders; +namespace Quell.Services.DatabaseProviders; public class SqliteDatabaseProvider : IDatabaseProvider { diff --git a/Services/DatabaseProviders/TableMetadata.cs b/Services/DatabaseProviders/TableMetadata.cs index 9a0c763..efcebd7 100644 --- a/Services/DatabaseProviders/TableMetadata.cs +++ b/Services/DatabaseProviders/TableMetadata.cs @@ -1,4 +1,4 @@ -namespace Fdb.Services.DatabaseProviders; +namespace Quell.Services.DatabaseProviders; public class TableMetadata { diff --git a/Services/DatabaseProviders/TargetDatabaseItem.cs b/Services/DatabaseProviders/TargetDatabaseItem.cs index f6f0c81..93c8fe9 100644 --- a/Services/DatabaseProviders/TargetDatabaseItem.cs +++ b/Services/DatabaseProviders/TargetDatabaseItem.cs @@ -1,7 +1,7 @@ -using System; +using System; using CommunityToolkit.Mvvm.ComponentModel; -namespace Fdb.Services.DatabaseProviders; +namespace Quell.Services.DatabaseProviders; public class TargetDatabaseItem : ObservableObject { diff --git a/Services/DatabaseStorageService.cs b/Services/DatabaseStorageService.cs index 65f9d5b..25bdead 100644 --- a/Services/DatabaseStorageService.cs +++ b/Services/DatabaseStorageService.cs @@ -1,14 +1,14 @@ -using System; +using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Threading.Tasks; using Dapper; using Microsoft.Data.Sqlite; -using Fdb.ViewModels; -using Fdb.Services.DatabaseProviders; +using Quell.ViewModels; +using Quell.Services.DatabaseProviders; -namespace Fdb.Services; +namespace Quell.Services; public class ConnectionRecord { @@ -119,6 +119,10 @@ public class PanelCustomizationRecord // Legacy / Shared Table public string TableBackground { get; set; } = "Default"; + + // Terminal Multi-part + public string TerminalBackground { get; set; } = "Default"; + public string TerminalForeground { get; set; } = "Default"; } public class TableCustomizationRecord @@ -166,7 +170,11 @@ public class DatabaseStorageService public static string ResolveDatabasePath() { - string? envPath = Environment.GetEnvironmentVariable("FDB_STUDIO_DB_PATH"); + string? envPath = Environment.GetEnvironmentVariable("QUELL_DB_PATH"); + if (string.IsNullOrWhiteSpace(envPath)) + { + envPath = Environment.GetEnvironmentVariable("FDB_STUDIO_DB_PATH"); + } if (!string.IsNullOrWhiteSpace(envPath)) { var dir = Path.GetDirectoryName(envPath); @@ -177,15 +185,60 @@ public static string ResolveDatabasePath() return envPath; } - string appData = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData); - string appFolder = Path.Combine(appData, "FdbStudio"); - if (!Directory.Exists(appFolder)) + try { - Directory.CreateDirectory(appFolder); + string appData = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData); + if (!string.IsNullOrEmpty(appData)) + { + string appFolder = Path.Combine(appData, "Quell"); + if (!Directory.Exists(appFolder)) + { + Directory.CreateDirectory(appFolder); + } + + string quellDbPath = Path.Combine(appFolder, "quell.db"); + if (!File.Exists(quellDbPath)) + { + string oldDbPath = Path.Combine(appData, "FdbStudio", "fdb_studio.db"); + if (File.Exists(oldDbPath)) + { + try + { + File.Copy(oldDbPath, quellDbPath, overwrite: false); + } + catch + { + // Ignore copy error + } + } + } + + string testFile = Path.Combine(appFolder, ".perm_test"); + File.WriteAllText(testFile, "1"); + File.Delete(testFile); + + return quellDbPath; + } + } + catch + { + // AppData not writable in current environment (sandbox/container), fallback below } - return Path.Combine(appFolder, "fdb_studio.db"); - } + string fallbackDir = Path.Combine(Directory.GetCurrentDirectory(), "data"); + if (!Directory.Exists(fallbackDir)) + { + try + { + Directory.CreateDirectory(fallbackDir); + } + catch + { + fallbackDir = AppDomain.CurrentDomain.BaseDirectory; + } + } + return Path.Combine(fallbackDir, "quell.db"); + } public DatabaseStorageService() { DatabasePath = ResolveDatabasePath(); @@ -319,7 +372,7 @@ UpdatedAt TEXT NOT NULL reportProgress?.Invoke(90, "Pre-caching active SQL dialect keywords..."); await SqlKeywordsService.Instance.LoadKeywordsFromDatabaseAsync(); - reportProgress?.Invoke(100, "Studio Environment Ready — SQLite Engine Initialized."); + reportProgress?.Invoke(100, "Studio Environment Ready ΓÇö SQLite Engine Initialized."); } catch (Exception ex) { @@ -372,20 +425,41 @@ public async Task LoadAllSettingsAsync(SqliteConnection? existingConnection = nu /// Runs SQLite schema initialization/migrations, pre-caches all settings, applies theme, /// pre-caches SQL dialect keywords, shortcuts, saved connections, and query history. /// - public async Task PreloadStartupDataAsync(Action? reportProgress = null) + /// Unified high-performance startup routine executed during the splash screen sequence. + /// Runs SQLite schema initialization/migrations, pre-caches all settings, applies theme, + /// pre-caches SQL dialect keywords, shortcuts, saved connections, and query history. + /// + public Task PreloadStartupDataAsync(Action? reportProgress) + { + return PreloadStartupDataAsync(reportProgress == null ? null : (p, s, sub) => + { + reportProgress(p, s); + return Task.CompletedTask; + }); + } + + public async Task PreloadStartupDataAsync(Func? reportProgress = null) { - reportProgress?.Invoke(10, "Opening SQLite Storage Engine..."); + if (reportProgress != null) + await reportProgress(10, "Opening SQLite Storage Engine...", "Verifying database file and initial connections...").ConfigureAwait(false); + await InitializeWithProgressAsync((p, s) => { - // Map the schema init progress from 10 to 50 - double mapped = 10 + (p * 0.4); - reportProgress?.Invoke(mapped, s); + // Map the schema init progress from 10 to 30 + double mapped = 10 + (p * 0.2); + reportProgress?.Invoke(mapped, s, "Verifying schema tables and migrations..."); }).ConfigureAwait(false); - reportProgress?.Invoke(55, "Pre-caching Application Settings & Preferences..."); + if (reportProgress != null) + await reportProgress(32, "Loading Preferences & Settings...", "Reading AppSettings cache from SQLite...").ConfigureAwait(false); await LoadAllSettingsAsync().ConfigureAwait(false); - reportProgress?.Invoke(68, "Applying Appearance, Themes & Customizations..."); + if (reportProgress != null) + await reportProgress(45, "Detecting GPU Hardware...", "Resolving DirectX 11 / ANGLE hardware pipeline...").ConfigureAwait(false); + await HardwareGraphicsService.Instance.InitializeAsync().ConfigureAwait(false); + + if (reportProgress != null) + await reportProgress(58, "Applying Appearance & Themes...", "Configuring Light/Dark palette and syntax colors...").ConfigureAwait(false); string theme = await GetThemeAsync().ConfigureAwait(false); ThemeService.Instance.ApplyTheme(theme); string keywordColor = await GetKeywordColorAsync().ConfigureAwait(false); @@ -395,16 +469,21 @@ await InitializeWithProgressAsync((p, s) => var tableCustom = await GetTableCustomizationAsync().ConfigureAwait(false); ThemeService.Instance.ApplyTableCustomization(tableCustom); - reportProgress?.Invoke(80, "Pre-caching Keyboard Shortcuts..."); + if (reportProgress != null) + await reportProgress(70, "Pre-caching Keyboard Shortcuts...", "Loading key gestures and shortcut map...").ConfigureAwait(false); await ShortcutService.Instance.InitializeAsync().ConfigureAwait(false); - reportProgress?.Invoke(90, "Pre-caching Saved Connections & Database Nodes..."); + if (reportProgress != null) + await reportProgress(78, "Loading Multi-Dialect SQL Lexers...", "Pre-caching MSSQL, PostgreSQL, MySQL, SQLite keywords...").ConfigureAwait(false); + await SqlKeywordsService.Instance.LoadKeywordsFromDatabaseAsync().ConfigureAwait(false); + + if (reportProgress != null) + await reportProgress(84, "Pre-caching Saved Connections...", "Pre-loading database tree nodes...").ConfigureAwait(false); await GetRecentConnectionsAsync(forceReload: true).ConfigureAwait(false); - reportProgress?.Invoke(96, "Pre-caching Execution History & Dialect Metadata..."); + if (reportProgress != null) + await reportProgress(88, "Pre-caching Execution History...", "Loading recent query logs & workspace state...").ConfigureAwait(false); await GetQueryHistoryAsync(forceReload: true).ConfigureAwait(false); - - reportProgress?.Invoke(100, "Studio Environment Ready — SQLite Engine Initialized."); } private static async Task ExecuteInitScriptStepsAsync(SqliteConnection connection, string fullSql, Action? reportProgress) @@ -937,7 +1016,7 @@ public async Task> GetRecentConnectionsAsync(bool forceRe COALESCE(Password, '') AS Password, COALESCE(DatabaseType, 'MSSQL') AS DatabaseType, LastConnectedAt FROM Connections - WHERE Server NOT LIKE '%fdb_studio%' AND DatabaseName NOT LIKE '%fdb_studio%' + WHERE Server NOT LIKE '%quell%' AND DatabaseName NOT LIKE '%quell%' AND Server NOT LIKE '%fdb_studio%' AND DatabaseName NOT LIKE '%fdb_studio%' ORDER BY Id DESC LIMIT 20;"; @@ -968,7 +1047,7 @@ ORDER BY Id DESC COALESCE(Password, '') AS Password, COALESCE(DatabaseType, 'MSSQL') AS DatabaseType, LastConnectedAt FROM Connections - WHERE Id = @id AND Server NOT LIKE '%fdb_studio%' AND DatabaseName NOT LIKE '%fdb_studio%';"; + WHERE Id = @id AND Server NOT LIKE '%quell%' AND DatabaseName NOT LIKE '%quell%' AND Server NOT LIKE '%fdb_studio%' AND DatabaseName NOT LIKE '%fdb_studio%';"; return await connection.QueryFirstOrDefaultAsync(sql, new { id }); } @@ -1191,6 +1270,16 @@ public async Task ResetAllShortcutsInDbAsync() } } + public async Task GetGraphicsRenderingModeAsync() + { + return await GetSettingAsync("GraphicsRenderingMode", "Auto").ConfigureAwait(false); + } + + public async Task SaveGraphicsRenderingModeAsync(string mode) + { + await SaveSettingAsync("GraphicsRenderingMode", mode ?? "Auto").ConfigureAwait(false); + } + public async Task GetPanelCustomizationAsync() { var record = new PanelCustomizationRecord(); @@ -1213,6 +1302,9 @@ public async Task GetPanelCustomizationAsync() record.SplitterColor = await GetSettingAsync("SplitterColor", "Default"); record.TableBackground = await GetSettingAsync("TableBackground", "Default"); + record.TerminalBackground = await GetSettingAsync("TerminalBackground", "Default"); + record.TerminalForeground = await GetSettingAsync("TerminalForeground", "Default"); + string fontSizeStr = await GetSettingAsync("EditorFontSize", "13"); if (double.TryParse(fontSizeStr, System.Globalization.CultureInfo.InvariantCulture, out double fs)) { @@ -1249,6 +1341,8 @@ public async Task SavePanelCustomizationAsync(PanelCustomizationRecord record) ["BorderColor"] = record.BorderColor, ["SplitterColor"] = record.SplitterColor, ["TableBackground"] = record.TableBackground, + ["TerminalBackground"] = record.TerminalBackground, + ["TerminalForeground"] = record.TerminalForeground, ["EditorFontSize"] = record.EditorFontSize.ToString(System.Globalization.CultureInfo.InvariantCulture), ["EditorShowLineNumbers"] = record.EditorShowLineNumbers ? "true" : "false" }; diff --git a/Services/HardwareGraphicsService.cs b/Services/HardwareGraphicsService.cs new file mode 100644 index 0000000..b443195 --- /dev/null +++ b/Services/HardwareGraphicsService.cs @@ -0,0 +1,242 @@ +using System; +using System.IO; +using System.Runtime.InteropServices; +using Avalonia; +using Avalonia.Skia; +using Avalonia.Win32; +using Microsoft.Win32; + +namespace Quell.Services; + +/// +/// Service managing graphics hardware adapter detection, GPU vs CPU rendering mode resolution, +/// Skia memory buffer optimization, and SQLite configuration persistence. +/// +public class HardwareGraphicsService +{ + private static HardwareGraphicsService? _instance; + public static HardwareGraphicsService Instance => _instance ??= new HardwareGraphicsService(); + + public string GpuAdapterName { get; private set; } = "Detecting Graphics Adapter..."; + public bool IsGpuDetected { get; private set; } + public string RenderingMode { get; set; } = "Auto"; // "Auto", "GPU", "CPU" + public string EffectiveRenderingMode { get; set; } = "GPU"; + public string ActiveRendererSummary { get; private set; } = "Skia Hardware (DirectX 11 / ANGLE)"; + public int VramCacheMb { get; private set; } = 128; + + public bool IsGpuMotionActive => string.Equals(EffectiveRenderingMode, "GPU", StringComparison.OrdinalIgnoreCase); + public event EventHandler? RenderingModeChanged; + + public void NotifyRenderingModeChanged() + { + RenderingModeChanged?.Invoke(this, EventArgs.Empty); + } + + private bool _hasDetectedHardware; + + public void DetectHardwareGpu() + { + if (_hasDetectedHardware) return; + _hasDetectedHardware = true; + + try + { + if (OperatingSystem.IsWindows()) + { + DetectWindowsGpu(); + } + else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) + { + IsGpuDetected = true; + GpuAdapterName = "Apple Metal Graphics Processor"; + } + else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) + { + bool hasDrm = Directory.Exists("/sys/class/drm") && Directory.GetDirectories("/sys/class/drm").Length > 0; + IsGpuDetected = hasDrm; + GpuAdapterName = hasDrm ? "Linux DRM/KMS Hardware GPU" : "Generic Display Controller"; + } + else + { + IsGpuDetected = false; + GpuAdapterName = "Standard Display Controller"; + } + } + catch (Exception ex) + { + Console.WriteLine($"[HardwareGraphicsService] DetectHardwareGpu error: {ex.Message}"); + IsGpuDetected = true; // Safe fallback: assume GPU available + GpuAdapterName = "Standard Hardware Graphics Device"; + } + } + + [System.Runtime.Versioning.SupportedOSPlatform("windows")] + private void DetectWindowsGpu() + { + try + { + // Query Windows Display Adapter Class Registry Keys + const string videoClassKey = @"SYSTEM\CurrentControlSet\Control\Class\{4d36e968-e325-11ce-bfc1-08002be10318}"; + using var baseKey = Registry.LocalMachine.OpenSubKey(videoClassKey); + if (baseKey != null) + { + string[] subKeys = baseKey.GetSubKeyNames(); + foreach (string sub in subKeys) + { + if (sub.Length == 4 && int.TryParse(sub, out _)) + { + using var adapterKey = baseKey.OpenSubKey(sub); + if (adapterKey != null) + { + var desc = adapterKey.GetValue("DriverDesc") as string; + if (!string.IsNullOrWhiteSpace(desc)) + { + // Filter out Microsoft Basic Display Adapter (which is software-emulated in VMs/RDP) + if (!desc.Contains("Microsoft Basic Display", StringComparison.OrdinalIgnoreCase) && + !desc.Contains("Standard VGA", StringComparison.OrdinalIgnoreCase) && + !desc.Contains("VSR", StringComparison.OrdinalIgnoreCase)) + { + IsGpuDetected = true; + GpuAdapterName = desc.Trim(); + return; + } + else + { + GpuAdapterName = desc.Trim(); + } + } + } + } + } + } + + // If no discrete or integrated GPU was verified + if (string.IsNullOrWhiteSpace(GpuAdapterName) || GpuAdapterName == "Detecting Graphics Adapter...") + { + GpuAdapterName = "Microsoft Basic Display Adapter (Software)"; + IsGpuDetected = false; + } + } + catch (Exception ex) + { + Console.WriteLine($"[HardwareGraphicsService] Windows GPU query error: {ex.Message}"); + IsGpuDetected = true; + GpuAdapterName = "Standard Direct3D / OpenGL Graphics"; + } + } + + public string ResolveEffectiveMode(string mode) + { + DetectHardwareGpu(); + if (string.Equals(mode, "GPU", StringComparison.OrdinalIgnoreCase)) return "GPU"; + if (string.Equals(mode, "CPU", StringComparison.OrdinalIgnoreCase)) return "CPU"; + // Auto mode: GPU if detected, otherwise CPU + return IsGpuDetected ? "GPU" : "CPU"; + } + + /// + /// Reads initial rendering configuration from SQLite before Avalonia UI initializes. + /// + public string GetInitialRenderingMode() + { + DetectHardwareGpu(); + string mode = "Auto"; + try + { + string dbPath = DatabaseStorageService.Instance.DatabasePath; + if (File.Exists(dbPath)) + { + using var conn = new Microsoft.Data.Sqlite.SqliteConnection($"Data Source={dbPath}"); + conn.Open(); + using var cmd = conn.CreateCommand(); + cmd.CommandText = "SELECT Value FROM AppSettings WHERE Key = 'GraphicsRenderingMode';"; + var res = cmd.ExecuteScalar(); + if (res != null && res != DBNull.Value) + { + string? str = res.ToString(); + if (!string.IsNullOrWhiteSpace(str)) mode = str; + } + } + } + catch (Exception ex) + { + Console.WriteLine($"[HardwareGraphicsService] GetInitialRenderingMode SQLite fallback: {ex.Message}"); + } + + RenderingMode = mode; + EffectiveRenderingMode = ResolveEffectiveMode(mode); + NotifyRenderingModeChanged(); + return EffectiveRenderingMode; + } + + /// + /// Applies DirectX 11 / ANGLE / Vulkan or CPU Software platform options and Skia memory buffers to AppBuilder. + /// + public void ConfigureAppBuilder(AppBuilder builder) + { + string effective = GetInitialRenderingMode(); + + if (string.Equals(effective, "GPU", StringComparison.OrdinalIgnoreCase)) + { + VramCacheMb = 128; + ActiveRendererSummary = "Skia Hardware Pipeline (DirectX 11 / ANGLE)"; + if (OperatingSystem.IsWindows()) + { + builder.With(new Win32PlatformOptions + { + RenderingMode = new[] + { + Win32RenderingMode.AngleEgl, // DirectX 11 via ANGLE + Win32RenderingMode.Wgl, // Native OpenGL fallback + Win32RenderingMode.Software // Safe CPU fallback + }, + CompositionMode = new[] + { + Win32CompositionMode.LowLatencyDxgiSwapChain, + Win32CompositionMode.RedirectionSurface + } + }); + } + + builder.With(new SkiaOptions + { + MaxGpuResourceSizeBytes = 128 * 1024 * 1024 // 128 MB VRAM cache for buttery smooth rendering + }); + } + else + { + VramCacheMb = 16; + ActiveRendererSummary = "CPU Software Rasterizer (Lightweight)"; + if (OperatingSystem.IsWindows()) + { + builder.With(new Win32PlatformOptions + { + RenderingMode = new[] + { + Win32RenderingMode.Software // Force lightweight CPU rasterization + } + }); + } + + builder.With(new SkiaOptions + { + MaxGpuResourceSizeBytes = 16 * 1024 * 1024 // 16 MB minimal memory buffer + }); + } + } + + /// + /// Pre-caches graphics settings and hardware device detection during Splash Screen sequence. + /// + public async System.Threading.Tasks.Task InitializeAsync() + { + DetectHardwareGpu(); + string mode = await DatabaseStorageService.Instance.GetSettingAsync("GraphicsRenderingMode", "Auto").ConfigureAwait(false); + RenderingMode = mode; + EffectiveRenderingMode = ResolveEffectiveMode(mode); + ActiveRendererSummary = string.Equals(EffectiveRenderingMode, "GPU", StringComparison.OrdinalIgnoreCase) + ? "Skia Hardware Pipeline (DirectX 11 / ANGLE)" + : "CPU Software Rasterizer (Lightweight)"; + VramCacheMb = string.Equals(EffectiveRenderingMode, "GPU", StringComparison.OrdinalIgnoreCase) ? 128 : 16; + } +} diff --git a/Services/ShortcutService.cs b/Services/ShortcutService.cs index ef00ef7..0c113d4 100644 --- a/Services/ShortcutService.cs +++ b/Services/ShortcutService.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Collections.ObjectModel; using System.Linq; @@ -6,7 +6,7 @@ using Avalonia.Input; using CommunityToolkit.Mvvm.ComponentModel; -namespace Fdb.Services; +namespace Quell.Services; public static class ShortcutActions { @@ -23,6 +23,8 @@ public static class ShortcutActions public const string ZoomIn = "ZoomIn"; public const string ZoomOut = "ZoomOut"; public const string ResetZoom = "ResetZoom"; + public const string ToggleTerminal = "ToggleTerminal"; + public const string RunInTerminal = "RunInTerminal"; } public partial class ShortcutItem : ObservableObject @@ -75,7 +77,9 @@ public class ShortcutService new() { Id = ShortcutActions.ExportData, Name = "Export Query Results", Category = "Data", DefaultGesture = "Ctrl+E", CurrentGesture = "Ctrl+E", Description = "Opens data export modal (CSV, JSON, SQL, TSV formats)." }, new() { Id = ShortcutActions.ZoomIn, Name = "Editor Zoom In", Category = "Editor", DefaultGesture = "Ctrl++", CurrentGesture = "Ctrl++", Description = "Increases SQL code editor font size." }, new() { Id = ShortcutActions.ZoomOut, Name = "Editor Zoom Out", Category = "Editor", DefaultGesture = "Ctrl+-", CurrentGesture = "Ctrl+-", Description = "Decreases SQL code editor font size." }, - new() { Id = ShortcutActions.ResetZoom, Name = "Reset Editor Zoom", Category = "Editor", DefaultGesture = "Ctrl+0", CurrentGesture = "Ctrl+0", Description = "Resets SQL code editor font size to default 13px." } + new() { Id = ShortcutActions.ResetZoom, Name = "Reset Editor Zoom", Category = "Editor", DefaultGesture = "Ctrl+0", CurrentGesture = "Ctrl+0", Description = "Resets SQL code editor font size to default 13px." }, + new() { Id = ShortcutActions.ToggleTerminal, Name = "Toggle Terminal Panel", Category = "Panels & View", DefaultGesture = "Ctrl+`", CurrentGesture = "Ctrl+`", Description = "Toggles or focuses the integrated terminal in the footer panel." }, + new() { Id = ShortcutActions.RunInTerminal, Name = "Run Selection in Terminal", Category = "Query & Execution", DefaultGesture = "Ctrl+Shift+Enter", CurrentGesture = "Ctrl+Shift+Enter", Description = "Sends selected SQL query or command directly to active terminal." } }; public IReadOnlyList Shortcuts @@ -224,6 +228,11 @@ public static bool MatchesGesture(string gestureStr, KeyEventArgs e) public static bool MatchesKey(string keyString, Key key) { // Special symbol aliases + if (keyString == "`" || keyString == "~" || keyString.Equals("Oem3", StringComparison.OrdinalIgnoreCase) || keyString.Equals("OemTilde", StringComparison.OrdinalIgnoreCase)) + { + return key is Key.Oem3 or Key.OemTilde; + } + if (keyString == "," || keyString.Equals("OemComma", StringComparison.OrdinalIgnoreCase)) { return key == Key.OemComma; @@ -277,6 +286,7 @@ public static string KeyEventArgsToGesture(KeyEventArgs e) string keyName = e.Key switch { + Key.Oem3 or Key.OemTilde => "`", Key.OemComma => ",", Key.OemPeriod => ".", Key.OemPlus or Key.Add => "+", diff --git a/Services/SqlCompletionData.cs b/Services/SqlCompletionData.cs index c4b3b56..60641f3 100644 --- a/Services/SqlCompletionData.cs +++ b/Services/SqlCompletionData.cs @@ -1,9 +1,9 @@ -using System; +using System; using AvaloniaEdit.CodeCompletion; using AvaloniaEdit.Document; using AvaloniaEdit.Editing; -namespace Fdb.Services; +namespace Quell.Services; public class SqlCompletionData : ICompletionData { diff --git a/Services/SqlKeywordColorizer.cs b/Services/SqlKeywordColorizer.cs index dc624a0..6afc852 100644 --- a/Services/SqlKeywordColorizer.cs +++ b/Services/SqlKeywordColorizer.cs @@ -1,9 +1,9 @@ -using System.Text.RegularExpressions; +using System.Text.RegularExpressions; using Avalonia.Media; using AvaloniaEdit.Document; using AvaloniaEdit.Rendering; -namespace Fdb.Services; +namespace Quell.Services; public class SqlKeywordColorizer : DocumentColorizingTransformer { diff --git a/Services/SqlKeywordsService.cs b/Services/SqlKeywordsService.cs index 4f29bad..2e6da01 100644 --- a/Services/SqlKeywordsService.cs +++ b/Services/SqlKeywordsService.cs @@ -1,11 +1,11 @@ -using System; +using System; using System.Collections.Generic; using System.Linq; using System.Text.RegularExpressions; using System.Threading.Tasks; -using Fdb.Services.DatabaseProviders; +using Quell.Services.DatabaseProviders; -namespace Fdb.Services; +namespace Quell.Services; public class SqlSnippet { diff --git a/Services/Terminal/DatabaseCliService.cs b/Services/Terminal/DatabaseCliService.cs new file mode 100644 index 0000000..d11515d --- /dev/null +++ b/Services/Terminal/DatabaseCliService.cs @@ -0,0 +1,237 @@ +using System; +using System.Collections.Generic; +using System.IO; +using Quell.Models; +using Quell.Services.DatabaseProviders; + +namespace Quell.Services.Terminal; + +public class DatabaseCliService +{ + private static DatabaseCliService? _instance; + public static DatabaseCliService Instance => _instance ??= new DatabaseCliService(); + + public CliLaunchInfo ResolveCli(DatabaseConnectionConfig config, TerminalSettingsModel? settings = null) + { + var info = new CliLaunchInfo(); + var dbType = (config.DatabaseType ?? "SQLite").ToLowerInvariant(); + + switch (dbType) + { + case "postgresql": + case "postgres": + ConfigurePostgres(config, settings?.PsqlPath, info); + break; + + case "mysql": + case "mariadb": + ConfigureMySql(config, settings?.MySqlPath, info); + break; + + case "sqlite": + ConfigureSqlite(config, settings?.SqlitePath, info); + break; + + case "mssql": + case "sqlserver": + ConfigureSqlServer(config, settings?.SqlCmdPath, info); + break; + + default: + info.IsAvailable = false; + info.MissingToolName = "CLI for " + config.DatabaseType; + info.Instructions = "Dedicated CLI not supported for this database type."; + break; + } + + // Add standard database environment variables + PopulateEnvironmentVariables(config, info.EnvironmentVariables); + + return info; + } + + public Dictionary BuildConnectionEnvironment(DatabaseConnectionConfig? config) + { + var env = new Dictionary(); + if (config != null) + { + PopulateEnvironmentVariables(config, env); + } + return env; + } + + private static void PopulateEnvironmentVariables(DatabaseConnectionConfig config, Dictionary env) + { + var dbName = !string.IsNullOrEmpty(config.Database) ? config.Database : config.DisplayName; + env["FDB_CONNECTED"] = "true"; + env["FDB_DB_TYPE"] = config.DatabaseType ?? "Unknown"; + env["FDB_DB_HOST"] = config.Server ?? "localhost"; + env["FDB_DB_PORT"] = config.Port.ToString(); + env["FDB_DB_NAME"] = dbName; + env["FDB_DB_USER"] = config.Username ?? string.Empty; + + var scheme = (config.DatabaseType ?? "").ToLowerInvariant() switch + { + "postgresql" or "postgres" => "postgresql", + "mysql" or "mariadb" => "mysql", + "mssql" or "sqlserver" => "sqlserver", + "sqlite" => "sqlite", + _ => "db" + }; + + if (scheme == "sqlite") + { + var path = !string.IsNullOrEmpty(config.FilePath) ? config.FilePath : config.Database; + env["DATABASE_URL"] = $"sqlite:///{path}"; + } + else + { + var userPart = !string.IsNullOrEmpty(config.Username) + ? (string.IsNullOrEmpty(config.Password) ? config.Username : $"{config.Username}:{config.Password}") + "@" + : string.Empty; + + env["DATABASE_URL"] = $"{scheme}://{userPart}{config.Server}:{config.Port}/{dbName}"; + } + } + + private static void ConfigurePostgres(DatabaseConnectionConfig config, string? customPath, CliLaunchInfo info) + { + if (!string.IsNullOrEmpty(config.Password)) + { + info.EnvironmentVariables["PGPASSWORD"] = config.Password; + } + + var exec = FindExecutable(customPath, "psql", @"C:\Program Files\PostgreSQL\17\bin\psql.exe", @"C:\Program Files\PostgreSQL\16\bin\psql.exe", @"C:\Program Files\PostgreSQL\15\bin\psql.exe"); + if (string.IsNullOrEmpty(exec)) + { + info.IsAvailable = false; + info.MissingToolName = "PostgreSQL CLI (psql)"; + info.DownloadUrl = "https://www.postgresql.org/download/"; + info.Instructions = "psql was not found in your system PATH. Please install PostgreSQL client tools or configure the executable path in Settings."; + return; + } + + info.IsAvailable = true; + info.ExecutablePath = exec; + var targetDb = !string.IsNullOrEmpty(config.Database) ? config.Database : "postgres"; + info.Arguments.AddRange(new[] + { + "-h", config.Server ?? "localhost", + "-p", config.Port.ToString(), + "-U", string.IsNullOrEmpty(config.Username) ? "postgres" : config.Username, + "-d", targetDb + }); + } + + private static void ConfigureMySql(DatabaseConnectionConfig config, string? customPath, CliLaunchInfo info) + { + if (!string.IsNullOrEmpty(config.Password)) + { + info.EnvironmentVariables["MYSQL_PWD"] = config.Password; + } + + var exec = FindExecutable(customPath, "mysql", @"C:\Program Files\MySQL\MySQL Server 8.4\bin\mysql.exe", @"C:\Program Files\MySQL\MySQL Server 8.0\bin\mysql.exe"); + if (string.IsNullOrEmpty(exec)) + { + info.IsAvailable = false; + info.MissingToolName = "MySQL CLI (mysql)"; + info.DownloadUrl = "https://dev.mysql.com/downloads/"; + info.Instructions = "mysql was not found in your system PATH. Please install MySQL client tools or configure the executable path in Settings."; + return; + } + + info.IsAvailable = true; + info.ExecutablePath = exec; + var targetDb = !string.IsNullOrEmpty(config.Database) ? config.Database : "mysql"; + info.Arguments.AddRange(new[] + { + "-h", config.Server ?? "localhost", + "-P", config.Port.ToString(), + "-u", string.IsNullOrEmpty(config.Username) ? "root" : config.Username, + "-D", targetDb + }); + } + + private static void ConfigureSqlite(DatabaseConnectionConfig config, string? customPath, CliLaunchInfo info) + { + var exec = FindExecutable(customPath, "sqlite3"); + if (string.IsNullOrEmpty(exec)) + { + info.IsAvailable = false; + info.MissingToolName = "SQLite CLI (sqlite3)"; + info.DownloadUrl = "https://www.sqlite.org/download.html"; + info.Instructions = "sqlite3 was not found in your system PATH. Please download the SQLite command-line shell or configure the path in Settings."; + return; + } + + info.IsAvailable = true; + info.ExecutablePath = exec; + var targetPath = !string.IsNullOrEmpty(config.FilePath) ? config.FilePath : config.Database; + if (!string.IsNullOrEmpty(targetPath)) + { + info.Arguments.Add(targetPath); + } + } + + private static void ConfigureSqlServer(DatabaseConnectionConfig config, string? customPath, CliLaunchInfo info) + { + var exec = FindExecutable(customPath, "sqlcmd", @"C:\Program Files\Microsoft SQL Server\Client SDK\ODBC\170\Tools\Binn\sqlcmd.exe"); + if (string.IsNullOrEmpty(exec)) + { + info.IsAvailable = false; + info.MissingToolName = "SQL Server CLI (sqlcmd)"; + info.DownloadUrl = "https://learn.microsoft.com/sql/tools/sqlcmd/"; + info.Instructions = "sqlcmd was not found in your system PATH. Please install SQL Server command-line utilities or configure the path in Settings."; + return; + } + + info.IsAvailable = true; + info.ExecutablePath = exec; + var targetDb = !string.IsNullOrEmpty(config.Database) ? config.Database : "master"; + info.Arguments.AddRange(new[] + { + "-S", $"{config.Server},{config.Port}", + "-d", targetDb + }); + + if (string.Equals(config.AuthType, "Windows", StringComparison.OrdinalIgnoreCase)) + { + info.Arguments.Add("-E"); + } + else + { + if (!string.IsNullOrEmpty(config.Username)) + { + info.Arguments.AddRange(new[] { "-U", config.Username }); + } + if (!string.IsNullOrEmpty(config.Password)) + { + info.Arguments.AddRange(new[] { "-P", config.Password }); + } + } + } + + private static string? FindExecutable(string? customPath, string defaultBinary, params string[] commonLocations) + { + if (!string.IsNullOrWhiteSpace(customPath) && File.Exists(customPath)) + { + return customPath; + } + + var inPath = ShellDetectorService.FindInPath(defaultBinary); + if (!string.IsNullOrEmpty(inPath)) + { + return inPath; + } + + foreach (var loc in commonLocations) + { + if (File.Exists(loc)) + { + return loc; + } + } + + return null; + } +} diff --git a/Services/Terminal/ShellDetectorService.cs b/Services/Terminal/ShellDetectorService.cs new file mode 100644 index 0000000..78e37a2 --- /dev/null +++ b/Services/Terminal/ShellDetectorService.cs @@ -0,0 +1,214 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Runtime.InteropServices; +using Quell.Models; + +namespace Quell.Services.Terminal; + +public class ShellDetectorService +{ + private static ShellDetectorService? _instance; + public static ShellDetectorService Instance => _instance ??= new ShellDetectorService(); + + public List GetAvailableShells() + { + var shells = new List(); + + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + // 1. PowerShell Core (pwsh) + var pwshPath = FindInPath("pwsh.exe"); + if (!string.IsNullOrEmpty(pwshPath)) + { + shells.Add(new ShellProfile + { + Id = "pwsh", + DisplayName = "PowerShell 7+", + ExecutablePath = pwshPath, + IconKey = "PowerShell" + }); + } + + // 2. Windows PowerShell (powershell.exe) + var winPowerShell = Path.Combine( + Environment.GetFolderPath(Environment.SpecialFolder.System), + @"WindowsPowerShell\v1.0\powershell.exe"); + + if (File.Exists(winPowerShell)) + { + shells.Add(new ShellProfile + { + Id = "powershell", + DisplayName = "Windows PowerShell", + ExecutablePath = winPowerShell, + IconKey = "PowerShell" + }); + } + + // 3. Command Prompt (cmd.exe) + var cmdPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.System), "cmd.exe"); + if (File.Exists(cmdPath)) + { + shells.Add(new ShellProfile + { + Id = "cmd", + DisplayName = "Command Prompt", + ExecutablePath = cmdPath, + IconKey = "Cmd" + }); + } + + // 4. Git Bash + var gitBashPaths = new[] + { + @"C:\Program Files\Git\bin\bash.exe", + @"C:\Program Files (x86)\Git\bin\bash.exe", + Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), @"Programs\Git\bin\bash.exe") + }; + + foreach (var path in gitBashPaths) + { + if (File.Exists(path)) + { + shells.Add(new ShellProfile + { + Id = "gitbash", + DisplayName = "Git Bash", + ExecutablePath = path, + Arguments = "--login -i", + IconKey = "Git" + }); + break; + } + } + + // 5. WSL (wsl.exe) + var wslPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.System), "wsl.exe"); + if (File.Exists(wslPath)) + { + shells.Add(new ShellProfile + { + Id = "wsl", + DisplayName = "WSL (Linux)", + ExecutablePath = wslPath, + IconKey = "Linux" + }); + } + } + else + { + // Unix/Linux/macOS + var candidates = new (string id, string name, string path)[] + { + ("zsh", "Zsh", "/bin/zsh"), + ("bash", "Bash", "/bin/bash"), + ("fish", "Fish", "/usr/bin/fish"), + ("pwsh", "PowerShell", "/usr/bin/pwsh") + }; + + foreach (var (id, name, path) in candidates) + { + if (File.Exists(path)) + { + shells.Add(new ShellProfile + { + Id = id, + DisplayName = name, + ExecutablePath = path, + IconKey = "Terminal" + }); + } + else + { + var inPath = FindInPath(id); + if (!string.IsNullOrEmpty(inPath)) + { + shells.Add(new ShellProfile + { + Id = id, + DisplayName = name, + ExecutablePath = inPath, + IconKey = "Terminal" + }); + } + } + } + } + + // Fallback default if none detected + if (shells.Count == 0) + { + var fallback = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? "cmd.exe" : "/bin/sh"; + shells.Add(new ShellProfile + { + Id = "default", + DisplayName = "System Shell", + ExecutablePath = fallback, + IconKey = "Terminal" + }); + } + + return shells; + } + + public ShellProfile GetDefaultShell(string? preferredId = null) + { + var shells = GetAvailableShells(); + if (!string.IsNullOrEmpty(preferredId)) + { + var matched = shells.Find(s => string.Equals(s.Id, preferredId, StringComparison.OrdinalIgnoreCase)); + if (matched != null) return matched; + } + + // Default preference order: pwsh > powershell > zsh > bash > cmd + var preferredOrder = new[] { "pwsh", "powershell", "zsh", "bash", "cmd" }; + foreach (var pref in preferredOrder) + { + var found = shells.Find(s => s.Id == pref); + if (found != null) return found; + } + + return shells[0]; + } + + public static string? FindInPath(string executableName) + { + if (File.Exists(executableName)) return Path.GetFullPath(executableName); + + var pathEnv = Environment.GetEnvironmentVariable("PATH"); + if (string.IsNullOrEmpty(pathEnv)) return null; + + var paths = pathEnv.Split(Path.PathSeparator, StringSplitOptions.RemoveEmptyEntries); + var extensions = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) + ? (Environment.GetEnvironmentVariable("PATHEXT")?.Split(';') ?? new[] { ".exe", ".cmd", ".bat" }) + : new[] { "" }; + + foreach (var dir in paths) + { + try + { + var trimmed = dir.Trim().Trim('"'); + if (!Directory.Exists(trimmed)) continue; + + var directPath = Path.Combine(trimmed, executableName); + if (File.Exists(directPath)) return directPath; + + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows) && !Path.HasExtension(executableName)) + { + foreach (var ext in extensions) + { + var withExt = Path.Combine(trimmed, executableName + ext); + if (File.Exists(withExt)) return withExt; + } + } + } + catch + { + // Ignore path access errors + } + } + + return null; + } +} diff --git a/Services/Terminal/TerminalSettingsService.cs b/Services/Terminal/TerminalSettingsService.cs new file mode 100644 index 0000000..a4f57ba --- /dev/null +++ b/Services/Terminal/TerminalSettingsService.cs @@ -0,0 +1,69 @@ +using System; +using System.Globalization; +using System.Threading.Tasks; +using Quell.Models; + +namespace Quell.Services.Terminal; + +public class TerminalSettingsService +{ + private static TerminalSettingsService? _instance; + public static TerminalSettingsService Instance => _instance ??= new TerminalSettingsService(); + + private readonly DatabaseStorageService _storage = DatabaseStorageService.Instance; + private TerminalSettingsModel _cachedSettings = new(); + + public TerminalSettingsModel CurrentSettings => _cachedSettings; + + public event EventHandler? SettingsChanged; + + public async Task LoadSettingsAsync() + { + var model = new TerminalSettingsModel(); + + model.DefaultShellId = await _storage.GetSettingAsync("Terminal_DefaultShell", "default"); + model.FontFamily = await _storage.GetSettingAsync("Terminal_FontFamily", "Cascadia Mono, Consolas, Courier New, monospace"); + + var fontSizeStr = await _storage.GetSettingAsync("Terminal_FontSize", "13"); + if (double.TryParse(fontSizeStr, NumberStyles.Any, CultureInfo.InvariantCulture, out var fs)) + { + model.FontSize = fs; + } + + model.CursorStyle = await _storage.GetSettingAsync("Terminal_CursorStyle", "Block"); + var blinkStr = await _storage.GetSettingAsync("Terminal_CursorBlink", "true"); + model.CursorBlink = blinkStr == "true" || blinkStr == "1"; + + var scrollStr = await _storage.GetSettingAsync("Terminal_MaxScrollback", "5000"); + if (int.TryParse(scrollStr, out var scroll)) + { + model.MaxScrollback = scroll; + } + + model.PsqlPath = await _storage.GetSettingAsync("Terminal_PsqlPath", ""); + model.MySqlPath = await _storage.GetSettingAsync("Terminal_MySqlPath", ""); + model.SqlitePath = await _storage.GetSettingAsync("Terminal_SqlitePath", ""); + model.SqlCmdPath = await _storage.GetSettingAsync("Terminal_SqlCmdPath", ""); + + _cachedSettings = model; + return model; + } + + public async Task SaveSettingsAsync(TerminalSettingsModel settings) + { + _cachedSettings = settings; + + await _storage.SaveSettingAsync("Terminal_DefaultShell", settings.DefaultShellId); + await _storage.SaveSettingAsync("Terminal_FontFamily", settings.FontFamily); + await _storage.SaveSettingAsync("Terminal_FontSize", settings.FontSize.ToString(CultureInfo.InvariantCulture)); + await _storage.SaveSettingAsync("Terminal_CursorStyle", settings.CursorStyle); + await _storage.SaveSettingAsync("Terminal_CursorBlink", settings.CursorBlink ? "true" : "false"); + await _storage.SaveSettingAsync("Terminal_MaxScrollback", settings.MaxScrollback.ToString()); + await _storage.SaveSettingAsync("Terminal_PsqlPath", settings.PsqlPath); + await _storage.SaveSettingAsync("Terminal_MySqlPath", settings.MySqlPath); + await _storage.SaveSettingAsync("Terminal_SqlitePath", settings.SqlitePath); + await _storage.SaveSettingAsync("Terminal_SqlCmdPath", settings.SqlCmdPath); + + SettingsChanged?.Invoke(this, settings); + } +} diff --git a/Services/ThemeService.cs b/Services/ThemeService.cs index 7e7223b..df4f793 100644 --- a/Services/ThemeService.cs +++ b/Services/ThemeService.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Linq; using Avalonia; @@ -6,7 +6,7 @@ using Avalonia.Styling; using Avalonia.Threading; -namespace Fdb.Services; +namespace Quell.Services; public class KeywordColorPreset { @@ -245,6 +245,10 @@ private void ApplyPanelCustomizationInternal() // 4. Table Alternating Background SetOrRemoveBrushOverride("AppTableAltColBackgroundBrush", custom.TableBackground); + + // 5. Terminal Multi-part Customization + SetOrRemoveBrushOverride("AppTerminalBackgroundBrush", custom.TerminalBackground); + SetOrRemoveBrushOverride("AppTerminalForegroundBrush", custom.TerminalForeground); } public void ApplyTableCustomization(TableCustomizationRecord custom) diff --git a/Themes/DarkTheme.axaml b/Themes/DarkTheme.axaml index 74ecf7b..39d4f1b 100644 --- a/Themes/DarkTheme.axaml +++ b/Themes/DarkTheme.axaml @@ -1,13 +1,14 @@ - - + #1E293B + #1E293B #0F172A #0F172A #334155 @@ -63,6 +64,20 @@ + + + + + + + + + + + + + + @@ -107,4 +122,20 @@ + + + + + + 0 + 0 + + + + + + 1 + 6 + 6 + diff --git a/Themes/LightTheme.axaml b/Themes/LightTheme.axaml index 385d69e..ea7647c 100644 --- a/Themes/LightTheme.axaml +++ b/Themes/LightTheme.axaml @@ -1,13 +1,14 @@ - - + #F8FAFC + #F8FAFC #FFFFFF #FFFFFF #E2E8F0 @@ -63,6 +64,20 @@ + + + + + + + + + + + + + + @@ -107,4 +122,20 @@ + + + + + + 0 + 0 + + + + + + 1 + 6 + 6 + diff --git a/Themes/ThemeTokens.axaml b/Themes/ThemeTokens.axaml index 851e4d7..9b57a4f 100644 --- a/Themes/ThemeTokens.axaml +++ b/Themes/ThemeTokens.axaml @@ -1,8 +1,8 @@ - - + diff --git a/ViewLocator.cs b/ViewLocator.cs index 2e59905..62d93a4 100644 --- a/ViewLocator.cs +++ b/ViewLocator.cs @@ -1,10 +1,10 @@ -using System; +using System; using System.Diagnostics.CodeAnalysis; using Avalonia.Controls; using Avalonia.Controls.Templates; -using Fdb.ViewModels; +using Quell.ViewModels; -namespace Fdb; +namespace Quell; /// /// Given a view model, returns the corresponding view if possible. diff --git a/ViewModels/ConfirmActionModalViewModel.cs b/ViewModels/ConfirmActionModalViewModel.cs index 332bf08..507579a 100644 --- a/ViewModels/ConfirmActionModalViewModel.cs +++ b/ViewModels/ConfirmActionModalViewModel.cs @@ -1,8 +1,8 @@ -using System; +using System; using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; -namespace Fdb.ViewModels; +namespace Quell.ViewModels; public partial class ConfirmActionModalViewModel : ViewModelBase { diff --git a/ViewModels/ConnectDatabaseModalViewModel.cs b/ViewModels/ConnectDatabaseModalViewModel.cs index 82faaf7..83faa97 100644 --- a/ViewModels/ConnectDatabaseModalViewModel.cs +++ b/ViewModels/ConnectDatabaseModalViewModel.cs @@ -1,13 +1,13 @@ -using System; +using System; using System.Collections.ObjectModel; using System.Linq; using System.Threading.Tasks; using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; -using Fdb.Services; -using Fdb.Services.DatabaseProviders; +using Quell.Services; +using Quell.Services.DatabaseProviders; -namespace Fdb.ViewModels; +namespace Quell.ViewModels; public class DatabaseEngineItem : ObservableObject { @@ -190,7 +190,7 @@ public async Task LoadSavedConnectionsAsync() { var conns = await DatabaseStorageService.Instance.GetRecentConnectionsAsync(); SavedConnections.Clear(); - foreach (var c in conns.Where(c => !c.Server.Contains("fdb_studio", StringComparison.OrdinalIgnoreCase) && !c.DatabaseName.Contains("fdb_studio", StringComparison.OrdinalIgnoreCase))) + foreach (var c in conns.Where(c => !c.Server.Contains("quell", StringComparison.OrdinalIgnoreCase) && !c.DatabaseName.Contains("quell", StringComparison.OrdinalIgnoreCase) && !c.Server.Contains("fdb_studio", StringComparison.OrdinalIgnoreCase) && !c.DatabaseName.Contains("fdb_studio", StringComparison.OrdinalIgnoreCase))) { SavedConnections.Add(c); } diff --git a/ViewModels/DatabaseDiagramModalViewModel.cs b/ViewModels/DatabaseDiagramModalViewModel.cs new file mode 100644 index 0000000..1794242 --- /dev/null +++ b/ViewModels/DatabaseDiagramModalViewModel.cs @@ -0,0 +1,277 @@ +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Avalonia; +using Avalonia.Input.Platform; +using CommunityToolkit.Mvvm.ComponentModel; +using CommunityToolkit.Mvvm.Input; +using Quell.Services.DatabaseProviders; + +namespace Quell.ViewModels; + +public class DiagramColumnItem +{ + public string ColumnName { get; set; } = string.Empty; + public string DataType { get; set; } = string.Empty; + public bool IsPrimaryKey { get; set; } + public bool IsForeignKey { get; set; } + public string? ReferencedTable { get; set; } + public string? ReferencedColumn { get; set; } + + public string BadgeText => IsPrimaryKey ? "PK" : (IsForeignKey ? "FK" : string.Empty); + public string FullDisplay => $"{ColumnName} : {DataType}"; +} + +public partial class DiagramTableItem : ObservableObject +{ + public string TableName { get; set; } = string.Empty; + public string Schema { get; set; } = "dbo"; + public string DatabaseType { get; set; } = "SQL"; + public string FullName => string.IsNullOrWhiteSpace(Schema) ? TableName : $"{Schema}.{TableName}"; + + public ObservableCollection Columns { get; } = new(); + public ObservableCollection OutgoingRelations { get; } = new(); + + public int PrimaryKeysCount => Columns.Count(c => c.IsPrimaryKey); + public int ForeignKeysCount => Columns.Count(c => c.IsForeignKey); + public int TotalColumnsCount => Columns.Count; +} + +public partial class DatabaseDiagramModalViewModel : ViewModelBase +{ + [ObservableProperty] + private bool _isOpen; + + [ObservableProperty] + private string _databaseTitle = "Active Database"; + + [ObservableProperty] + private string _databaseType = "SQL"; + + [ObservableProperty] + private string _searchFilter = string.Empty; + + [ObservableProperty] + private string _filterMode = "All"; // "All", "Relations", "PK" + + [ObservableProperty] + private double _zoomLevel = 1.0; // 0.7 .. 1.5 + + [ObservableProperty] + private string _statusMessage = string.Empty; + + [ObservableProperty] + private bool _hasStatusMessage; + + [ObservableProperty] + [NotifyPropertyChangedFor(nameof(IsIndeterminate))] + [NotifyPropertyChangedFor(nameof(IsEmptyState))] + private bool _isLoading; + + [ObservableProperty] + private string _loadingMessage = "Analyzing database schema & relationships..."; + + [ObservableProperty] + private string _loadingDetailText = "Scanning tables, primary keys, and foreign keys..."; + + [ObservableProperty] + [NotifyPropertyChangedFor(nameof(IsIndeterminate))] + private double _loadingProgress; + + public bool IsIndeterminate => LoadingProgress <= 0.0; + + public bool IsEmptyState => !IsLoading && FilteredTables.Count == 0; + + public ObservableCollection AllTables { get; } = new(); + public ObservableCollection FilteredTables { get; } = new(); + + public int TotalTablesCount => AllTables.Count; + public int TotalRelationsCount => AllTables.Sum(t => t.OutgoingRelations.Count); + public int TablesWithRelationsCount => AllTables.Count(t => t.OutgoingRelations.Count > 0 || t.Columns.Any(c => c.IsForeignKey)); + public int TablesWithPkCount => AllTables.Count(t => t.Columns.Any(c => c.IsPrimaryKey)); + + public Func? OpenTableDataCallback { get; set; } + public Action? GenerateDdlCallback { get; set; } + public Func? RefreshDiagramCallback { get; set; } + + public DatabaseDiagramModalViewModel() + { + } + + [RelayCommand] + public void Open() + { + IsOpen = true; + ApplyFilter(); + } + + [RelayCommand] + public void Close() + { + IsOpen = false; + IsLoading = false; + } + + [RelayCommand] + public void SetFilterMode(string mode) + { + FilterMode = mode; + ApplyFilter(); + } + + [RelayCommand] + public void ZoomIn() + { + ZoomLevel = Math.Min(1.5, Math.Round(ZoomLevel + 0.1, 1)); + } + + [RelayCommand] + public void ZoomOut() + { + ZoomLevel = Math.Max(0.7, Math.Round(ZoomLevel - 0.1, 1)); + } + + [RelayCommand] + public void ResetZoom() + { + ZoomLevel = 1.0; + } + + [RelayCommand] + public async Task RefreshDiagramAsync() + { + if (RefreshDiagramCallback != null) + { + await RefreshDiagramCallback(); + } + } + + partial void OnSearchFilterChanged(string value) + { + ApplyFilter(); + } + + partial void OnFilterModeChanged(string value) + { + ApplyFilter(); + } + + public void ApplyFilter() + { + FilteredTables.Clear(); + var filter = SearchFilter?.Trim(); + + foreach (var table in AllTables) + { + if (FilterMode == "Relations" && table.OutgoingRelations.Count == 0 && !table.Columns.Any(c => c.IsForeignKey)) + { + continue; + } + if (FilterMode == "PK" && !table.Columns.Any(c => c.IsPrimaryKey)) + { + continue; + } + + if (string.IsNullOrWhiteSpace(filter) || + table.TableName.Contains(filter, StringComparison.OrdinalIgnoreCase) || + table.Columns.Any(c => c.ColumnName.Contains(filter, StringComparison.OrdinalIgnoreCase))) + { + FilteredTables.Add(table); + } + } + + OnPropertyChanged(nameof(TotalTablesCount)); + OnPropertyChanged(nameof(TotalRelationsCount)); + OnPropertyChanged(nameof(TablesWithRelationsCount)); + OnPropertyChanged(nameof(TablesWithPkCount)); + OnPropertyChanged(nameof(IsEmptyState)); + } + + public void LoadTables(IEnumerable tables, string databaseName, string dbType) + { + DatabaseTitle = databaseName; + DatabaseType = dbType; + AllTables.Clear(); + + foreach (var t in tables) + { + AllTables.Add(t); + } + + ApplyFilter(); + IsLoading = false; + } + + [RelayCommand] + public async Task CopyMermaidDiagramAsync() + { + var sb = new StringBuilder(); + sb.AppendLine("erDiagram"); + + foreach (var table in AllTables) + { + sb.AppendLine($" {table.TableName} {{"); + foreach (var col in table.Columns) + { + string keyType = col.IsPrimaryKey ? "PK" : (col.IsForeignKey ? "FK" : ""); + string cleanType = col.DataType.Replace(" ", "_").Replace("(", "_").Replace(")", "_"); + if (string.IsNullOrEmpty(cleanType)) cleanType = "string"; + sb.AppendLine($" {cleanType} {col.ColumnName} {keyType}".TrimEnd()); + } + sb.AppendLine(" }"); + + foreach (var rel in table.OutgoingRelations) + { + var parts = rel.Split(new[] { "->" }, StringSplitOptions.RemoveEmptyEntries); + if (parts.Length == 2) + { + string target = parts[1].Trim().Split(' ')[0]; + sb.AppendLine($" {table.TableName} }}o--|| {target} : references"); + } + } + } + + string mermaid = sb.ToString(); + + try + { + if (Application.Current?.ApplicationLifetime is Avalonia.Controls.ApplicationLifetimes.IClassicDesktopStyleApplicationLifetime desktop) + { + var clipboard = desktop.MainWindow?.Clipboard; + if (clipboard != null) + { + await clipboard.SetTextAsync(mermaid); + StatusMessage = "Mermaid ER Diagram copied to clipboard!"; + HasStatusMessage = true; + return; + } + } + StatusMessage = "Mermaid ER Diagram generated!"; + HasStatusMessage = true; + } + catch + { + StatusMessage = "Mermaid ER Diagram generated!"; + HasStatusMessage = true; + } + } + + [RelayCommand] + public async Task ViewTableData(DiagramTableItem? table) + { + if (table == null || OpenTableDataCallback == null) return; + IsOpen = false; + await OpenTableDataCallback.Invoke(table.FullName); + } + + [RelayCommand] + public void GenerateDdl(DiagramTableItem? table) + { + if (table == null || GenerateDdlCallback == null) return; + IsOpen = false; + GenerateDdlCallback.Invoke(table.FullName); + } +} diff --git a/ViewModels/EditCellModalViewModel.cs b/ViewModels/EditCellModalViewModel.cs index 13678ae..8de1159 100644 --- a/ViewModels/EditCellModalViewModel.cs +++ b/ViewModels/EditCellModalViewModel.cs @@ -1,11 +1,11 @@ -using System; +using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; -namespace Fdb.ViewModels; +namespace Quell.ViewModels; public partial class EditCellModalViewModel : ViewModelBase { diff --git a/ViewModels/EditorViewModel.cs b/ViewModels/EditorViewModel.cs index 9d72654..cf2dbc5 100644 --- a/ViewModels/EditorViewModel.cs +++ b/ViewModels/EditorViewModel.cs @@ -4,7 +4,7 @@ using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; -namespace Fdb.ViewModels; +namespace Quell.ViewModels; public partial class EditorTabItem : ObservableObject { @@ -14,6 +14,9 @@ public partial class EditorTabItem : ObservableObject [ObservableProperty] private bool _isActive; + [ObservableProperty] + private string _selectedText = string.Empty; + private string _rawText = string.Empty; public TextDocument Document { get; } @@ -34,6 +37,7 @@ public string Text set { _rawText = value ?? string.Empty; + SelectedText = string.Empty; try { if (Document.Text != value) @@ -70,6 +74,32 @@ public partial class EditorViewModel : ViewModelBase [ObservableProperty] private EditorTabItem? _selectedTab; + /// + /// Optional live delegate returning the active editor's highlighted/selected text (e.g. from AvaloniaEdit.TextEditor.SelectedText). + /// + public System.Func? GetActiveSelection { get; set; } + + /// + /// Resolves the SQL query to execute: returns the selected/highlighted text if any; + /// otherwise falls back to the full text of the currently active editor tab. + /// + public string GetQueryToExecute() + { + var selection = GetActiveSelection?.Invoke(); + if (!string.IsNullOrWhiteSpace(selection)) + { + return selection.Trim(); + } + + if (!string.IsNullOrWhiteSpace(SelectedTab?.SelectedText)) + { + return SelectedTab.SelectedText.Trim(); + } + + string full = SelectedTab?.Text ?? string.Empty; + return !string.IsNullOrWhiteSpace(full) ? full : "SELECT * FROM dbo.Users;"; + } + public EditorViewModel() { if (Tabs.Count > 0) diff --git a/ViewModels/ExportDataModalViewModel.cs b/ViewModels/ExportDataModalViewModel.cs index 373773f..f89e154 100644 --- a/ViewModels/ExportDataModalViewModel.cs +++ b/ViewModels/ExportDataModalViewModel.cs @@ -1,11 +1,16 @@ +using System; +using System.Collections.Generic; using System.Collections.ObjectModel; +using System.IO; +using System.Linq; +using System.Text; using System.Threading.Tasks; using Avalonia.Controls; using Avalonia.Platform.Storage; using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; -namespace Fdb.ViewModels; +namespace Quell.ViewModels; public partial class ExportDataModalViewModel : ViewModelBase { @@ -13,14 +18,32 @@ public partial class ExportDataModalViewModel : ViewModelBase private bool _isOpen; [ObservableProperty] - private string _selectedFormat = "CSV (Comma Separated Values)"; + private string _exportSource = "Specific Database Table"; + + public ObservableCollection ExportSources { get; } = new() + { + "Specific Database Table", + "Active Query Results" + }; + + [ObservableProperty] + private string? _selectedTable; + + public ObservableCollection AvailableTables { get; } = new(); + + public bool IsTableSourceSelected => ExportSource == "Specific Database Table"; + + [ObservableProperty] + private string _selectedFormat = "Excel Spreadsheet (*.xlsx)"; public ObservableCollection ExportFormats { get; } = new() { + "Excel Spreadsheet (*.xlsx)", "CSV (Comma Separated Values)", "JSON (JavaScript Object Notation)", "SQL (INSERT INTO statements)", - "TSV (Tab Separated Values)" + "TSV (Tab Separated Values)", + "HTML Document (*.html)" }; [ObservableProperty] @@ -30,11 +53,91 @@ public partial class ExportDataModalViewModel : ViewModelBase private string _delimiter = ","; [ObservableProperty] - private string _filePath = "C:\\Users\\ehsan\\Desktop\\export_results.csv"; + private string _filePath = string.Empty; + + [ObservableProperty] + private string? _statusMessage; + + [ObservableProperty] + private bool _hasStatusMessage; + + [ObservableProperty] + private bool _isSuccess; + + public Func>? PerformExportCallback { get; set; } + + public ExportDataModalViewModel() + { + UpdateSuggestedFilePath(); + } + + partial void OnExportSourceChanged(string value) + { + OnPropertyChanged(nameof(IsTableSourceSelected)); + UpdateSuggestedFilePath(); + } + + partial void OnSelectedTableChanged(string? value) + { + UpdateSuggestedFilePath(); + } + + partial void OnSelectedFormatChanged(string value) + { + UpdateSuggestedFilePath(); + } + + public void UpdateSuggestedFilePath() + { + string desktopPath = Environment.GetFolderPath(Environment.SpecialFolder.DesktopDirectory); + if (string.IsNullOrEmpty(desktopPath)) + { + desktopPath = Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments); + } + + string baseName = "export_results"; + if (IsTableSourceSelected && !string.IsNullOrWhiteSpace(SelectedTable)) + { + baseName = SelectedTable.Replace(".", "_").Replace("[", "").Replace("]", "").Replace("\"", ""); + } + + string ext = ".xlsx"; + if (SelectedFormat.Contains("CSV", StringComparison.OrdinalIgnoreCase)) ext = ".csv"; + else if (SelectedFormat.Contains("JSON", StringComparison.OrdinalIgnoreCase)) ext = ".json"; + else if (SelectedFormat.Contains("SQL", StringComparison.OrdinalIgnoreCase)) ext = ".sql"; + else if (SelectedFormat.Contains("TSV", StringComparison.OrdinalIgnoreCase)) ext = ".tsv"; + else if (SelectedFormat.Contains("HTML", StringComparison.OrdinalIgnoreCase)) ext = ".html"; + else if (SelectedFormat.Contains("Excel", StringComparison.OrdinalIgnoreCase)) ext = ".xlsx"; + + FilePath = Path.Combine(desktopPath, $"{baseName}{ext}"); + } + + public void SetAvailableTables(IEnumerable tables, string? defaultSelected = null) + { + AvailableTables.Clear(); + foreach (var t in tables) + { + AvailableTables.Add(t); + } + + if (!string.IsNullOrWhiteSpace(defaultSelected) && AvailableTables.Contains(defaultSelected)) + { + SelectedTable = defaultSelected; + } + else if (AvailableTables.Count > 0) + { + SelectedTable = AvailableTables[0]; + } + + UpdateSuggestedFilePath(); + } [RelayCommand] public void Open() { + StatusMessage = null; + HasStatusMessage = false; + UpdateSuggestedFilePath(); IsOpen = true; } @@ -45,10 +148,26 @@ public void Close() } [RelayCommand] - public void Export() + public async Task ExportAsync() { - // Ready for streaming export execution - IsOpen = false; + StatusMessage = "Exporting data..."; + HasStatusMessage = true; + + if (PerformExportCallback != null) + { + var res = await PerformExportCallback.Invoke(ExportSource, SelectedTable ?? string.Empty, IncludeHeaders, FilePath); + IsSuccess = res.Success; + StatusMessage = res.Message; + if (res.Success) + { + await Task.Delay(600); + IsOpen = false; + } + } + else + { + IsOpen = false; + } } [RelayCommand] @@ -56,11 +175,21 @@ public async Task BrowseFile(TopLevel? topLevel) { if (topLevel?.StorageProvider == null) return; + string ext = "xlsx"; + if (SelectedFormat.Contains("CSV", StringComparison.OrdinalIgnoreCase)) ext = "csv"; + else if (SelectedFormat.Contains("JSON", StringComparison.OrdinalIgnoreCase)) ext = "json"; + else if (SelectedFormat.Contains("SQL", StringComparison.OrdinalIgnoreCase)) ext = "sql"; + else if (SelectedFormat.Contains("TSV", StringComparison.OrdinalIgnoreCase)) ext = "tsv"; + else if (SelectedFormat.Contains("HTML", StringComparison.OrdinalIgnoreCase)) ext = "html"; + + string suggestedName = Path.GetFileNameWithoutExtension(FilePath); + if (string.IsNullOrEmpty(suggestedName)) suggestedName = "export_results"; + var file = await topLevel.StorageProvider.SaveFilePickerAsync(new FilePickerSaveOptions { Title = "Save Export File", - SuggestedFileName = "export_results", - DefaultExtension = "csv" + SuggestedFileName = suggestedName, + DefaultExtension = ext }); if (file != null) diff --git a/ViewModels/FooterViewModel.cs b/ViewModels/FooterViewModel.cs index 78b883a..954592b 100644 --- a/ViewModels/FooterViewModel.cs +++ b/ViewModels/FooterViewModel.cs @@ -1,9 +1,21 @@ -using CommunityToolkit.Mvvm.ComponentModel; -using CommunityToolkit.Mvvm.Input; +using System; using System.Collections.ObjectModel; using System.Linq; +using CommunityToolkit.Mvvm.ComponentModel; +using CommunityToolkit.Mvvm.Input; + +namespace Quell.ViewModels; -namespace Fdb.ViewModels; +public partial class FooterTabItem : ObservableObject +{ + public string Name { get; set; } = string.Empty; + public string Icon { get; set; } = "Document"; + public bool IsResults => string.Equals(Name, "Results", StringComparison.OrdinalIgnoreCase); + public bool IsTerminal => string.Equals(Name, "Terminal", StringComparison.OrdinalIgnoreCase); + + [ObservableProperty] + private bool _isActive; +} public partial class FooterViewModel : ViewModelBase { @@ -14,14 +26,69 @@ public partial class FooterViewModel : ViewModelBase private bool _isMaximized; [ObservableProperty] - private string _selectedTab = string.Empty; + [NotifyPropertyChangedFor(nameof(IsResultsTabSelected))] + [NotifyPropertyChangedFor(nameof(IsTerminalTabSelected))] + private string _selectedTab = "Results"; + + public bool IsResultsTabSelected => string.Equals(SelectedTab, "Results", StringComparison.OrdinalIgnoreCase); + public bool IsTerminalTabSelected => string.Equals(SelectedTab, "Terminal", StringComparison.OrdinalIgnoreCase); public ObservableCollection Tabs { get; } = new(); + public ObservableCollection TabItems { get; } = new(); + + public FooterViewModel() + { + Tabs.CollectionChanged += (s, e) => SyncTabItems(); + SyncTabItems(); + } + + partial void OnSelectedTabChanged(string value) + { + UpdateActiveStates(); + } + + private void SyncTabItems() + { + TabItems.Clear(); + foreach (var tab in Tabs) + { + TabItems.Add(new FooterTabItem + { + Name = tab, + Icon = GetTabIcon(tab), + IsActive = string.Equals(tab, SelectedTab, StringComparison.OrdinalIgnoreCase) + }); + } + } + + private void UpdateActiveStates() + { + foreach (var item in TabItems) + { + item.IsActive = string.Equals(item.Name, SelectedTab, StringComparison.OrdinalIgnoreCase); + } + } + + private static string GetTabIcon(string tabName) + { + return tabName.ToLowerInvariant() switch + { + "terminal" => "Play", + "results" => "Document", + _ => "Document" + }; + } + + public event Action? TerminalTabActivated; [RelayCommand] private void SelectTab(string tabName) { SelectedTab = tabName; + if (IsTerminalTabSelected) + { + TerminalTabActivated?.Invoke(); + } } [RelayCommand] diff --git a/ViewModels/MainViewModel.cs b/ViewModels/MainViewModel.cs index d485a73..d3dce2a 100644 --- a/ViewModels/MainViewModel.cs +++ b/ViewModels/MainViewModel.cs @@ -5,10 +5,10 @@ using System.Threading.Tasks; using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; -using Fdb.Services; -using Fdb.Services.DatabaseProviders; +using Quell.Services; +using Quell.Services.DatabaseProviders; -namespace Fdb.ViewModels; +namespace Quell.ViewModels; public partial class MainViewModel : ViewModelBase { @@ -24,6 +24,9 @@ public partial class MainViewModel : ViewModelBase [ObservableProperty] private FooterViewModel _footer; + [ObservableProperty] + private TerminalViewModel _terminal; + [ObservableProperty] private ConnectDatabaseModalViewModel _connectModal; @@ -33,6 +36,9 @@ public partial class MainViewModel : ViewModelBase [ObservableProperty] private ExportDataModalViewModel _exportModal; + [ObservableProperty] + private DatabaseDiagramModalViewModel _diagramModal; + [ObservableProperty] private SettingsModalViewModel _settingsModal; @@ -53,13 +59,44 @@ public MainViewModel() _sidebar = new SidebarViewModel(); _editor = new EditorViewModel(); _footer = new FooterViewModel(); + _terminal = new TerminalViewModel(); _connectModal = new ConnectDatabaseModalViewModel(); _confirmModal = new ConfirmActionModalViewModel(); _exportModal = new ExportDataModalViewModel(); + _diagramModal = new DatabaseDiagramModalViewModel(); _settingsModal = new SettingsModalViewModel(); _sampleQueryResult = new QueryResultTableViewModel(); _sampleSchema = new SchemaPropertiesTableViewModel(); + _exportModal.PerformExportCallback = async (source, tableName, includeHeaders, filePath) => + { + return await ExecuteDataExportAsync(source, tableName, includeHeaders, filePath); + }; + + _diagramModal.OpenTableDataCallback = async tableName => + { + string q = $"SELECT * FROM {tableName} LIMIT 100;"; + if (Editor.SelectedTab != null) Editor.SelectedTab.Text = q; + await ExecuteQuery(); + }; + + _diagramModal.GenerateDdlCallback = tableName => + { + GenerateTableDdlFor(tableName); + }; + _diagramModal.RefreshDiagramCallback = async () => + { + await OpenDiagramModal(); + }; + + _footer.TerminalTabActivated += () => + { + if (_terminal.Sessions.Count == 0) + { + _terminal.NewSession(); + } + }; + _sampleQueryResult.EditCellModal.ExecuteQueryDetailedCallback = async sql => { string? key = SelectedTargetDatabase?.Key ?? DatabaseSessionService.Instance.ActiveSession?.Key; @@ -70,7 +107,7 @@ public MainViewModel() var res = await DatabaseSessionService.Instance.ExecuteQueryAsync(sql, key); if (res.Success) { - await Fdb.Services.DatabaseStorageService.Instance.SaveQueryHistoryAsync( + await Quell.Services.DatabaseStorageService.Instance.SaveQueryHistoryAsync( sql, res.ExecutionTime, res.RowCount > 0 ? res.RowCount : 1, "Success"); return (true, null); } @@ -168,6 +205,7 @@ await Fdb.Services.DatabaseStorageService.Instance.SaveQueryHistoryAsync( DatabaseSessionService.Instance.ConnectionChanged += () => { Navbar.UpdateConnectionStatus(); + Terminal.UpdateConnectionContext(DatabaseSessionService.Instance.ActiveConfig); _ = RefreshAvailableDatabasesAsync(); }; @@ -177,7 +215,7 @@ await Fdb.Services.DatabaseStorageService.Instance.SaveQueryHistoryAsync( private async Task InitializeStartupAsync() { - await Fdb.Services.DatabaseStorageService.Instance.InitializeAsync(); + await Quell.Services.DatabaseStorageService.Instance.InitializeAsync(); await RefreshAvailableDatabasesAsync(); await Sidebar.RefreshAsync(); } @@ -186,8 +224,8 @@ private async Task InitializeStartupAsync() public async Task RefreshAvailableDatabasesAsync() { - var conns = (await Fdb.Services.DatabaseStorageService.Instance.GetRecentConnectionsAsync()) - .Where(c => !c.Server.Contains("fdb_studio", StringComparison.OrdinalIgnoreCase) && !c.DatabaseName.Contains("fdb_studio", StringComparison.OrdinalIgnoreCase)) + var conns = (await Quell.Services.DatabaseStorageService.Instance.GetRecentConnectionsAsync()) + .Where(c => !c.Server.Contains("quell", StringComparison.OrdinalIgnoreCase) && !c.DatabaseName.Contains("quell", StringComparison.OrdinalIgnoreCase) && !c.Server.Contains("fdb_studio", StringComparison.OrdinalIgnoreCase) && !c.DatabaseName.Contains("fdb_studio", StringComparison.OrdinalIgnoreCase)) .ToList(); var currentSelectedKey = SelectedTargetDatabase?.Key; @@ -312,7 +350,7 @@ public async Task ActivateDatabaseConnectionAsync(ConnectionRecord record) [RelayCommand] public async Task ExecuteQuery() { - string query = Editor.SelectedTab?.Text ?? "SELECT * FROM dbo.Users;"; + string query = Editor.GetQueryToExecute(); string execTime = "0 ms"; int rowCount = 0; string status = "Success"; @@ -345,7 +383,7 @@ public async Task ExecuteQuery() }, "0 ms"); SampleQueryResult.TargetDatabaseName = "(No Connection)"; - ShowFooter(); + ShowQueryResultFooter(); return; } @@ -382,7 +420,7 @@ public async Task ExecuteQuery() }, "0 ms"); SampleQueryResult.TargetDatabaseName = targetDbName; - ShowFooter(); + ShowQueryResultFooter(); return; } } @@ -457,11 +495,11 @@ public async Task ExecuteQuery() SampleQueryResult.TargetDatabaseName = targetDbName; - await Fdb.Services.DatabaseStorageService.Instance.SaveQueryHistoryAsync( + await Quell.Services.DatabaseStorageService.Instance.SaveQueryHistoryAsync( query, execTime, rowCount, status); _ = Sidebar.RefreshAsync(); - ShowFooter(); + ShowQueryResultFooter(); } public async Task NavigateToForeignKeyTableAsync(string targetTable, string targetColumn, string? cellValue, string? sourceColumn) @@ -475,9 +513,9 @@ public async Task NavigateToForeignKeyTableAsync(string targetTable, string targ { whereClause = string.Empty; } - else if (targetTable.Equals("Users", StringComparison.OrdinalIgnoreCase) || targetTable.Equals("کاربران", StringComparison.OrdinalIgnoreCase)) + else if (targetTable.Equals("Users", StringComparison.OrdinalIgnoreCase)) { - if (sourceColumn != null && (sourceColumn.Equals("دوره", StringComparison.OrdinalIgnoreCase) || sourceColumn.Equals("Course", StringComparison.OrdinalIgnoreCase) || sourceColumn.Equals("TITLE", StringComparison.OrdinalIgnoreCase) || sourceColumn.Equals("CourseTitle", StringComparison.OrdinalIgnoreCase))) + if (sourceColumn != null && (sourceColumn.Equals("Course", StringComparison.OrdinalIgnoreCase) || sourceColumn.Equals("TITLE", StringComparison.OrdinalIgnoreCase) || sourceColumn.Equals("CourseTitle", StringComparison.OrdinalIgnoreCase))) { whereClause = isNumeric ? $"WHERE Id = {cellValue} OR Course = '{cleanVal}'" : $"WHERE Course = '{cleanVal}'"; } @@ -540,7 +578,7 @@ public async Task NavigateToForeignKeyTableAsync(string targetTable, string targ catch { } }); - ShowFooter(); + ShowQueryResultFooter(); return; } } @@ -550,7 +588,7 @@ public async Task NavigateToForeignKeyTableAsync(string targetTable, string targ string matchedInfo = SampleQueryResult.SelectedRow != null ? $" (User: {SampleQueryResult.SelectedRow.Name})" : ""; SampleQueryResult.LastQueryMessage = $"Navigated to {targetTable} via FK [{sourceColumn} -> {targetTable}]{matchedInfo} ({SampleQueryResult.TotalRows} rows)"; SampleQueryResult.HasLastQueryMessage = true; - ShowFooter(); + ShowQueryResultFooter(); } public async Task RefreshQueryResultAsync() @@ -623,7 +661,7 @@ public async Task SaveQuery() { if (Editor.SelectedTab != null) { - await Fdb.Services.DatabaseStorageService.Instance.SaveQueryHistoryAsync( + await Quell.Services.DatabaseStorageService.Instance.SaveQueryHistoryAsync( Editor.SelectedTab.Text, "0 ms", 0, "Saved"); _ = Sidebar.RefreshAsync(); } @@ -692,16 +730,96 @@ public void ToggleFooterMaximized() Footer.ToggleMaximizeCommand.Execute(null); } + [RelayCommand] + public void ToggleTerminal() + { + if (!Footer.IsVisible) + { + Footer.IsVisible = true; + + if (!Footer.Tabs.Contains("Terminal")) + { + Footer.Tabs.Add("Terminal"); + } + Footer.SelectedTab = "Terminal"; + if (Terminal.Sessions.Count == 0) + { + Terminal.NewSession(); + } + } + else if (Footer.SelectedTab == "Terminal") + { + Footer.IsVisible = false; + } + else + { + if (!Footer.Tabs.Contains("Terminal")) + { + Footer.Tabs.Add("Terminal"); + } + Footer.SelectedTab = "Terminal"; + if (Terminal.Sessions.Count == 0) + { + Terminal.NewSession(); + } + } + } + + [RelayCommand] + public void RunSelectionInTerminal(string? queryText = null) + { + var textToSend = queryText; + if (string.IsNullOrWhiteSpace(textToSend)) + { + textToSend = Editor.SelectedTab?.Text; + } + if (string.IsNullOrWhiteSpace(textToSend)) return; + + Footer.IsVisible = true; + + if (!Footer.Tabs.Contains("Terminal")) + { + Footer.Tabs.Add("Terminal"); + } + Footer.SelectedTab = "Terminal"; + + Terminal.SendTextToActiveTerminal(textToSend); + } + + [RelayCommand] + public void LaunchCliInTerminal(Quell.Services.DatabaseProviders.DatabaseConnectionConfig? config = null) + { + Footer.IsVisible = true; + + if (!Footer.Tabs.Contains("Terminal")) + { + Footer.Tabs.Add("Terminal"); + } + Footer.SelectedTab = "Terminal"; + Terminal.LaunchDatabaseCli(config ?? Quell.Services.DatabaseProviders.DatabaseSessionService.Instance.ActiveConfig); + } + [RelayCommand] public void ShowFooter() { Footer.IsVisible = true; } + public void ShowQueryResultFooter() + { + Footer.IsVisible = true; + if (!Footer.Tabs.Contains("Results")) + { + Footer.Tabs.Insert(0, "Results"); + } + Footer.SelectedTab = "Results"; + } + [RelayCommand] public void RestoreFooterTab(string tabName) { Footer.IsVisible = true; + if (!Footer.Tabs.Contains(tabName)) { Footer.Tabs.Add(tabName); @@ -733,6 +851,7 @@ public void ResetLayout() // Restore Footer Footer.IsVisible = true; + ResetFooterTabs(); // Restore Editor @@ -746,6 +865,7 @@ private void ResetFooterTabs() { Footer.Tabs.Clear(); Footer.Tabs.Add("Results"); + Footer.Tabs.Add("Terminal"); Footer.SelectedTab = "Results"; } @@ -763,6 +883,589 @@ private void ResetFooterTabs() return null; } + #region Database Management, Backup, Export & Diagram Commands + + public void PopulateExportModalTables(string? defaultTable = null) + { + var tables = new List(); + var cached = DatabaseSessionService.Instance.GetActiveCachedTables(); + foreach (var t in cached) + { + tables.Add(t.FullName); + } + + var activeDb = Sidebar.DatabaseNodes.FirstOrDefault(n => n.IsActive) ?? Sidebar.DatabaseNodes.FirstOrDefault(n => n.IsConnected); + if (tables.Count == 0 && activeDb != null) + { + foreach (var t in activeDb.AllTableNodes) + { + tables.Add(t.FullName); + } + } + + if (tables.Count == 0) + { + tables.AddRange(new[] { "Users", "Orders", "Products", "Roles" }); + } + + string? selected = defaultTable ?? Sidebar.SelectedTable?.FullName ?? SampleQueryResult.CurrentTableName; + ExportModal.SetAvailableTables(tables, selected); + } + + [RelayCommand] + public void OpenExportTableModal() + { + PopulateExportModalTables(); + ExportModal.Open(); + } + + [RelayCommand] + public void QuickExportExcel() + { + PopulateExportModalTables(); + ExportModal.SelectedFormat = "Excel Spreadsheet (*.xlsx)"; + ExportModal.ExportSource = "Specific Database Table"; + ExportModal.Open(); + } + + [RelayCommand] + public async Task BackupDatabase() + { + await ExecuteDatabaseBackupAsync(); + } + + [RelayCommand] + public async Task RestoreDatabase() + { + await ExecuteDatabaseRestoreAsync(); + } + + [RelayCommand] + public async Task OpenDiagramModal() + { + var activeSession = DatabaseSessionService.Instance.ActiveSession; + string dbName = activeSession?.DisplayName ?? SelectedTargetDatabase?.DisplayName ?? "Quell Studio DB"; + string dbType = activeSession?.DatabaseType ?? SelectedTargetDatabase?.DatabaseType ?? "SQLite"; + + // 1. Immediately open the modal and display loading state + DiagramModal.DatabaseTitle = dbName; + DiagramModal.DatabaseType = dbType; + DiagramModal.IsLoading = true; + DiagramModal.LoadingMessage = "Discovering database tables and relationships..."; + DiagramModal.LoadingDetailText = "Querying schema information..."; + DiagramModal.LoadingProgress = 0.0; + DiagramModal.AllTables.Clear(); + DiagramModal.FilteredTables.Clear(); + DiagramModal.Open(); + + await Task.Delay(250); + + try + { + var cachedTables = DatabaseSessionService.Instance.GetActiveCachedTables(); + var activeDb = Sidebar.DatabaseNodes.FirstOrDefault(n => n.IsActive) ?? Sidebar.DatabaseNodes.FirstOrDefault(n => n.IsConnected); + if (cachedTables.Count == 0 && activeDb != null) + { + cachedTables = activeDb.AllTableNodes.Select(t => t.Table).ToList(); + } + + if (cachedTables.Count == 0) + { + var sampleTables = GetSampleDiagramTables(); + DiagramModal.LoadTables(sampleTables, dbName, dbType); + return; + } + + var tables = new System.Collections.Concurrent.ConcurrentBag(); + int total = cachedTables.Count; + int processed = 0; + + using var semaphore = new System.Threading.SemaphoreSlim(4); + var tasks = cachedTables.Select(async t => + { + await semaphore.WaitAsync(); + try + { + var pksTask = DatabaseSessionService.Instance.GetPrimaryKeyColumnsAsync(t.FullName); + var fksTask = DatabaseSessionService.Instance.GetForeignKeyRelationsAsync(t.FullName); + var colsTask = DatabaseSessionService.Instance.GetColumnsAsync(t.FullName); + + await Task.WhenAll(pksTask, fksTask, colsTask); + + var pks = await pksTask; + var fks = await fksTask; + var cols = await colsTask; + + var item = new DiagramTableItem + { + TableName = t.TableName, + Schema = t.SchemaName, + DatabaseType = dbType + }; + + foreach (var c in cols) + { + bool isPk = pks.Contains(c.ColumnName, StringComparer.OrdinalIgnoreCase); + var fk = fks.FirstOrDefault(f => f.ColumnName.Equals(c.ColumnName, StringComparison.OrdinalIgnoreCase)); + + item.Columns.Add(new DiagramColumnItem + { + ColumnName = c.ColumnName, + DataType = c.DataType, + IsPrimaryKey = isPk, + IsForeignKey = fk != null, + ReferencedTable = fk?.TargetTable, + ReferencedColumn = fk?.TargetColumn + }); + + if (fk != null) + { + item.OutgoingRelations.Add($"{item.TableName}.{c.ColumnName} -> {fk.TargetTable}.{fk.TargetColumn}"); + } + } + + tables.Add(item); + + int current = System.Threading.Interlocked.Increment(ref processed); + double progress = (double)current / total * 100.0; + + void UpdateProgress() + { + DiagramModal.LoadingMessage = $"Analyzing schema: {t.TableName} ({current} of {total})"; + DiagramModal.LoadingDetailText = $"Loaded {cols.Count} columns for {t.TableName}"; + DiagramModal.LoadingProgress = progress; + } + + if (Avalonia.Threading.Dispatcher.UIThread.CheckAccess()) + { + UpdateProgress(); + } + else + { + Avalonia.Threading.Dispatcher.UIThread.Post(UpdateProgress); + } + } + finally + { + semaphore.Release(); + } + }); + + await Task.WhenAll(tasks); + + var sortedTables = tables.OrderBy(t => t.TableName).ToList(); + if (sortedTables.Count == 0) + { + sortedTables = GetSampleDiagramTables(); + } + + DiagramModal.LoadTables(sortedTables, dbName, dbType); + } + catch (Exception ex) + { + DiagramModal.StatusMessage = $"Schema analysis warning: {ex.Message}"; + DiagramModal.HasStatusMessage = true; + if (DiagramModal.AllTables.Count == 0) + { + DiagramModal.LoadTables(GetSampleDiagramTables(), dbName, dbType); + } + } + finally + { + DiagramModal.IsLoading = false; + } + } + + [RelayCommand] + public void GenerateTableDdl() + { + string target = Sidebar.SelectedTable?.FullName ?? SampleQueryResult.CurrentTableName ?? "Users"; + GenerateTableDdlFor(target); + } + + [RelayCommand] + public async Task ShowTableProperties() + { + string target = Sidebar.SelectedTable?.FullName ?? SampleQueryResult.CurrentTableName ?? "Users"; + await LoadTablePropertiesAsync(target); + } + + [RelayCommand] + public async Task OptimizeDatabase() + { + string key = SelectedTargetDatabase?.Key ?? DatabaseSessionService.Instance.ActiveSession?.Key ?? string.Empty; + string dbType = SelectedTargetDatabase?.DatabaseType ?? "SQLite"; + string sql = dbType.Equals("PostgreSQL", StringComparison.OrdinalIgnoreCase) || dbType.Equals("SQLite", StringComparison.OrdinalIgnoreCase) + ? "VACUUM;" + : (dbType.Equals("MySQL", StringComparison.OrdinalIgnoreCase) ? "OPTIMIZE TABLE Users;" : "CHECKPOINT;"); + + var res = await DatabaseSessionService.Instance.ExecuteQueryAsync(sql, key); + SampleQueryResult.LastQueryMessage = $"Database optimization executed ({res.ExecutionTime})"; + SampleQueryResult.HasLastQueryMessage = true; + ShowQueryResultFooter(); + } + + [RelayCommand] + public async Task RefreshDatabaseSchema() + { + await Sidebar.RefreshAsync(); + SampleQueryResult.LastQueryMessage = "Database schema & tables refreshed"; + SampleQueryResult.HasLastQueryMessage = true; + } + + private async Task<(bool Success, string Message, int RowCount)> ExecuteDataExportAsync( + string source, string tableName, bool includeHeaders, string filePath) + { + try + { + List columns = new(); + List> rows = new(); + + if (source.Contains("Table", StringComparison.OrdinalIgnoreCase) && !string.IsNullOrWhiteSpace(tableName)) + { + string key = SelectedTargetDatabase?.Key ?? DatabaseSessionService.Instance.ActiveSession?.Key ?? string.Empty; + var res = await DatabaseSessionService.Instance.ExecuteQueryAsync($"SELECT * FROM {tableName};", key); + if (!res.Success) + { + return (false, $"Failed to query table {tableName}: {res.Message}", 0); + } + columns = res.ColumnNames; + foreach (var r in res.Rows) + { + var dict = new Dictionary(); + foreach (var c in columns) + { + dict[c] = r[c]; + } + rows.Add(dict); + } + } + else + { + columns = SampleQueryResult.LastColumns?.ToList() ?? SampleQueryResult.Rows.FirstOrDefault()?.Values.Keys.ToList() ?? new List(); + foreach (var r in SampleQueryResult.Rows) + { + var dict = new Dictionary(); + foreach (var c in columns) + { + dict[c] = r[c]; + } + rows.Add(dict); + } + } + + if (columns.Count == 0 && rows.Count == 0) + { + return (false, "No data available to export.", 0); + } + + string dir = System.IO.Path.GetDirectoryName(filePath) ?? ""; + if (!string.IsNullOrEmpty(dir) && !System.IO.Directory.Exists(dir)) + { + System.IO.Directory.CreateDirectory(dir); + } + + string ext = System.IO.Path.GetExtension(filePath).ToLowerInvariant(); + if (ext is ".xlsx" or ".xml") + { + string excelContent = GenerateExcelXml(columns, rows); + await System.IO.File.WriteAllTextAsync(filePath, excelContent, System.Text.Encoding.UTF8); + } + else if (ext == ".json") + { + var sb = new System.Text.StringBuilder(); + sb.AppendLine("["); + for (int i = 0; i < rows.Count; i++) + { + sb.Append(" { "); + var pairs = rows[i].Select(kv => $"\"{kv.Key.Replace("\"", "\\\"")}\": \"{kv.Value.Replace("\"", "\\\"")}\""); + sb.Append(string.Join(", ", pairs)); + sb.AppendLine(i < rows.Count - 1 ? " }," : " }"); + } + sb.AppendLine("]"); + await System.IO.File.WriteAllTextAsync(filePath, sb.ToString(), System.Text.Encoding.UTF8); + } + else if (ext == ".sql") + { + string target = !string.IsNullOrWhiteSpace(tableName) ? tableName : "ExportedTable"; + var sb = new System.Text.StringBuilder(); + sb.AppendLine($"-- Exported data for table {target}"); + foreach (var row in rows) + { + var colNames = string.Join(", ", columns.Select(c => $"[{c}]")); + var values = string.Join(", ", columns.Select(c => $"'{row.GetValueOrDefault(c, "").Replace("'", "''")}'")); + sb.AppendLine($"INSERT INTO {target} ({colNames}) VALUES ({values});"); + } + await System.IO.File.WriteAllTextAsync(filePath, sb.ToString(), System.Text.Encoding.UTF8); + } + else if (ext == ".html") + { + var sb = new System.Text.StringBuilder(); + sb.AppendLine(""); + sb.AppendLine(""); + foreach (var col in columns) sb.Append($""); + sb.AppendLine(""); + foreach (var row in rows) + { + sb.Append(""); + foreach (var col in columns) sb.Append($""); + sb.AppendLine(""); + } + sb.AppendLine("
{System.Net.WebUtility.HtmlEncode(col)}
{System.Net.WebUtility.HtmlEncode(row.GetValueOrDefault(col, ""))}
"); + await System.IO.File.WriteAllTextAsync(filePath, sb.ToString(), System.Text.Encoding.UTF8); + } + else + { + char delim = ext == ".tsv" ? '\t' : ','; + var sb = new System.Text.StringBuilder(); + if (includeHeaders) + { + sb.AppendLine(string.Join(delim, columns.Select(c => EscapeCsv(c, delim)))); + } + foreach (var row in rows) + { + sb.AppendLine(string.Join(delim, columns.Select(c => EscapeCsv(row.GetValueOrDefault(c, ""), delim)))); + } + await System.IO.File.WriteAllTextAsync(filePath, sb.ToString(), new System.Text.UTF8Encoding(true)); + } + + return (true, $"Exported {rows.Count} rows to {System.IO.Path.GetFileName(filePath)}", rows.Count); + } + catch (Exception ex) + { + return (false, $"Export error: {ex.Message}", 0); + } + } + + private static string EscapeCsv(string val, char delimiter) + { + if (string.IsNullOrEmpty(val)) return ""; + if (val.Contains(delimiter) || val.Contains('"') || val.Contains('\n') || val.Contains('\r')) + { + return $"\"{val.Replace("\"", "\"\"")}\""; + } + return val; + } + + private static string GenerateExcelXml(List columns, List> rows) + { + var sb = new System.Text.StringBuilder(); + sb.AppendLine(""); + sb.AppendLine(""); + sb.AppendLine(""); + sb.AppendLine(" "); + sb.AppendLine(" "); + sb.AppendLine(" "); + sb.AppendLine(" "); + sb.AppendLine(" "); + + sb.AppendLine(" "); + foreach (var col in columns) + { + sb.AppendLine($" {System.Security.SecurityElement.Escape(col)}"); + } + sb.AppendLine(" "); + + foreach (var row in rows) + { + sb.AppendLine(" "); + foreach (var col in columns) + { + string val = row.GetValueOrDefault(col, ""); + string type = double.TryParse(val, out _) && !val.StartsWith("0") && val.Length < 11 ? "Number" : "String"; + sb.AppendLine($" {System.Security.SecurityElement.Escape(val)}"); + } + sb.AppendLine(" "); + } + + sb.AppendLine("
"); + sb.AppendLine("
"); + sb.AppendLine("
"); + return sb.ToString(); + } + + private async Task ExecuteDatabaseBackupAsync() + { + try + { + var activeSession = DatabaseSessionService.Instance.ActiveSession; + string dbType = activeSession?.DatabaseType ?? SelectedTargetDatabase?.DatabaseType ?? "SQLite"; + string dbName = activeSession?.DisplayName ?? SelectedTargetDatabase?.DisplayName ?? "quell_db"; + string cleanName = dbName.Replace(" ", "_").Replace(":", "_").Replace("/", "_").Replace("\\", "_"); + string timestamp = DateTime.Now.ToString("yyyyMMdd_HHmmss"); + + string desktopPath = Environment.GetFolderPath(Environment.SpecialFolder.DesktopDirectory); + if (string.IsNullOrEmpty(desktopPath)) desktopPath = Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments); + + string key = SelectedTargetDatabase?.Key ?? activeSession?.Key ?? string.Empty; + + var tables = await DatabaseSessionService.Instance.GetTablesAsync(key); + var activeDb = Sidebar.DatabaseNodes.FirstOrDefault(n => n.IsActive) ?? Sidebar.DatabaseNodes.FirstOrDefault(n => n.IsConnected); + if (tables.Count == 0 && activeDb != null) + { + tables = activeDb.AllTableNodes.Select(t => t.Table).ToList(); + } + + string backupFile = System.IO.Path.Combine(desktopPath, $"{cleanName}_backup_{timestamp}.sql"); + + var sb = new System.Text.StringBuilder(); + sb.AppendLine($"-- ============================================="); + sb.AppendLine($"-- Quell Database Backup"); + sb.AppendLine($"-- Database: {dbName} ({dbType})"); + sb.AppendLine($"-- Backup Date: {DateTime.Now:yyyy-MM-dd HH:mm:ss}"); + sb.AppendLine($"-- Total Tables: {tables.Count}"); + sb.AppendLine($"-- =============================================\n"); + + foreach (var t in tables) + { + sb.AppendLine($"-- Table: {t.FullName}"); + var queryRes = await DatabaseSessionService.Instance.ExecuteQueryAsync($"SELECT * FROM {t.FullName};", key); + if (queryRes.Success && queryRes.ColumnNames.Count > 0) + { + string colList = string.Join(", ", queryRes.ColumnNames.Select(c => $"[{c}]")); + foreach (var row in queryRes.Rows) + { + var valList = string.Join(", ", queryRes.ColumnNames.Select(c => $"'{row[c].Replace("'", "''")}'")); + sb.AppendLine($"INSERT INTO {t.FullName} ({colList}) VALUES ({valList});"); + } + } + sb.AppendLine(); + } + + await System.IO.File.WriteAllTextAsync(backupFile, sb.ToString(), System.Text.Encoding.UTF8); + + SampleQueryResult.LastQueryMessage = $"Backup saved: {System.IO.Path.GetFileName(backupFile)} ({tables.Count} tables)"; + SampleQueryResult.HasLastQueryMessage = true; + ShowQueryResultFooter(); + } + catch (Exception ex) + { + SampleQueryResult.LastQueryMessage = $"Backup failed: {ex.Message}"; + SampleQueryResult.HasLastQueryMessage = true; + } + } + + private async Task ExecuteDatabaseRestoreAsync() + { + SampleQueryResult.LastQueryMessage = "Ready to restore. Open a .sql backup script in the Editor and click Run (F5)."; + SampleQueryResult.HasLastQueryMessage = true; + ShowQueryResultFooter(); + } + + private void GenerateTableDdlFor(string tableName) + { + string clean = tableName.Trim('[', ']', '"'); + var sb = new System.Text.StringBuilder(); + sb.AppendLine($"-- DDL Script generated by Quell for {clean}"); + sb.AppendLine($"CREATE TABLE {clean} ("); + + var cols = DatabaseSessionService.Instance.GetColumnsAsync(clean).GetAwaiter().GetResult(); + var pks = DatabaseSessionService.Instance.GetPrimaryKeyColumnsAsync(clean).GetAwaiter().GetResult(); + + if (cols.Count > 0) + { + for (int i = 0; i < cols.Count; i++) + { + var col = cols[i]; + string pkStr = pks.Contains(col.ColumnName, StringComparer.OrdinalIgnoreCase) ? " PRIMARY KEY" : ""; + string comma = (i < cols.Count - 1) ? "," : ""; + sb.AppendLine($" [{col.ColumnName}] {col.DataType}{pkStr}{comma}"); + } + } + else + { + sb.AppendLine(" [Id] INT PRIMARY KEY IDENTITY(1,1),"); + sb.AppendLine(" [Name] NVARCHAR(100) NOT NULL,"); + sb.AppendLine(" [CreatedAt] DATETIME DEFAULT CURRENT_TIMESTAMP"); + } + + sb.AppendLine(");"); + + Editor.AddTab(); + if (Editor.SelectedTab != null) + { + Editor.SelectedTab.Title = $"{clean}_DDL.sql"; + Editor.SelectedTab.Text = sb.ToString(); + } + } + + private async Task LoadTablePropertiesAsync(string tableName) + { + string clean = tableName.Trim('[', ']', '"'); + var cols = await DatabaseSessionService.Instance.GetColumnsAsync(clean); + var pks = await DatabaseSessionService.Instance.GetPrimaryKeyColumnsAsync(clean); + var fks = await DatabaseSessionService.Instance.GetForeignKeyRelationsAsync(clean); + + if (cols.Count > 0) + { + SampleSchema.Columns.Clear(); + int ord = 1; + foreach (var col in cols) + { + bool isPk = pks.Contains(col.ColumnName, StringComparer.OrdinalIgnoreCase); + var fk = fks.FirstOrDefault(f => f.ColumnName.Equals(col.ColumnName, StringComparison.OrdinalIgnoreCase)); + + SampleSchema.Columns.Add(new Quell.Models.TableColumnItem + { + Name = col.ColumnName, + DataType = col.DataType, + IsNullable = !isPk, + IsPrimaryKey = isPk, + IsForeignKey = fk != null, + DefaultValue = string.Empty, + Ordinal = ord++ + }); + } + } + + ShowQueryResultFooter(); + } + + private List GetSampleDiagramTables() + { + var list = new List(); + + var users = new DiagramTableItem { TableName = "Users", Schema = "dbo", DatabaseType = "SQL" }; + users.Columns.Add(new DiagramColumnItem { ColumnName = "Id", DataType = "INT", IsPrimaryKey = true }); + users.Columns.Add(new DiagramColumnItem { ColumnName = "Name", DataType = "VARCHAR(100)" }); + users.Columns.Add(new DiagramColumnItem { ColumnName = "Email", DataType = "VARCHAR(150)" }); + users.Columns.Add(new DiagramColumnItem { ColumnName = "RoleId", DataType = "INT", IsForeignKey = true, ReferencedTable = "Roles", ReferencedColumn = "Id" }); + users.Columns.Add(new DiagramColumnItem { ColumnName = "CreatedAt", DataType = "DATETIME" }); + users.OutgoingRelations.Add("Users.RoleId -> Roles.Id"); + list.Add(users); + + var roles = new DiagramTableItem { TableName = "Roles", Schema = "dbo", DatabaseType = "SQL" }; + roles.Columns.Add(new DiagramColumnItem { ColumnName = "Id", DataType = "INT", IsPrimaryKey = true }); + roles.Columns.Add(new DiagramColumnItem { ColumnName = "Title", DataType = "VARCHAR(50)" }); + roles.Columns.Add(new DiagramColumnItem { ColumnName = "Description", DataType = "VARCHAR(255)" }); + list.Add(roles); + + var orders = new DiagramTableItem { TableName = "Orders", Schema = "dbo", DatabaseType = "SQL" }; + orders.Columns.Add(new DiagramColumnItem { ColumnName = "Id", DataType = "INT", IsPrimaryKey = true }); + orders.Columns.Add(new DiagramColumnItem { ColumnName = "UserId", DataType = "INT", IsForeignKey = true, ReferencedTable = "Users", ReferencedColumn = "Id" }); + orders.Columns.Add(new DiagramColumnItem { ColumnName = "TotalAmount", DataType = "DECIMAL(10,2)" }); + orders.Columns.Add(new DiagramColumnItem { ColumnName = "OrderDate", DataType = "DATETIME" }); + orders.OutgoingRelations.Add("Orders.UserId -> Users.Id"); + list.Add(orders); + + var products = new DiagramTableItem { TableName = "Products", Schema = "dbo", DatabaseType = "SQL" }; + products.Columns.Add(new DiagramColumnItem { ColumnName = "Id", DataType = "INT", IsPrimaryKey = true }); + products.Columns.Add(new DiagramColumnItem { ColumnName = "Name", DataType = "VARCHAR(100)" }); + products.Columns.Add(new DiagramColumnItem { ColumnName = "Price", DataType = "DECIMAL(10,2)" }); + products.Columns.Add(new DiagramColumnItem { ColumnName = "StockQuantity", DataType = "INT" }); + list.Add(products); + + return list; + } + + #endregion + [RelayCommand] public void Exit() { @@ -772,3 +1475,4 @@ public void Exit() } } } + diff --git a/ViewModels/NavbarViewModel.cs b/ViewModels/NavbarViewModel.cs index 065bb20..ba9cef4 100644 --- a/ViewModels/NavbarViewModel.cs +++ b/ViewModels/NavbarViewModel.cs @@ -1,13 +1,13 @@ -using CommunityToolkit.Mvvm.ComponentModel; +using CommunityToolkit.Mvvm.ComponentModel; using System.Collections.ObjectModel; -using Fdb.Services.DatabaseProviders; +using Quell.Services.DatabaseProviders; -namespace Fdb.ViewModels; +namespace Quell.ViewModels; public partial class NavbarViewModel : ViewModelBase { [ObservableProperty] - private string _title = "Fdb"; + private string _title = "Quell"; [ObservableProperty] private string _connectionStatusText = "master [localhost:1433]"; diff --git a/ViewModels/QueryResultTableViewModel.cs b/ViewModels/QueryResultTableViewModel.cs index 2b78ff9..ce00915 100644 --- a/ViewModels/QueryResultTableViewModel.cs +++ b/ViewModels/QueryResultTableViewModel.cs @@ -1,4 +1,4 @@ -using Fdb.Services.DatabaseProviders; +using Quell.Services.DatabaseProviders; using System; using System.Collections.Generic; using System.Collections.ObjectModel; @@ -10,7 +10,7 @@ using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; -namespace Fdb.ViewModels; +namespace Quell.ViewModels; public partial class QueryResultRow : ObservableObject { @@ -55,7 +55,7 @@ public string this[string key] "NAME" => Name, "EMAIL" => Email, "ROLE" => Role, - "COURSE" or "دوره" => Course, + "COURSE" => Course, "STATUS" => Status, "CREATED AT" or "CREATEDAT" => CreatedAt, _ => string.Empty @@ -78,7 +78,7 @@ public string this[string key] case "ROLE": Role = value; break; - case "COURSE" or "دوره": + case "COURSE": Course = value; break; case "STATUS": @@ -276,11 +276,10 @@ public void SetForeignKeyRelations(IEnumerable fks) } } - // Persian & English: Course (دوره) and User (کاربر / دانشجو) relations - if (lower == "دوره" || lower == "دوره_ها" || lower == "کد_دوره" || lower == "نام_دوره" || - lower == "course" || lower == "course_name" || lower == "coursetitle" || lower == "course_title") + // Course and User relations + if (lower == "course" || lower == "course_name" || lower == "coursetitle" || lower == "course_title") { - if (!string.IsNullOrEmpty(tableName) && (tableName.Equals("Users", StringComparison.OrdinalIgnoreCase) || tableName.Equals("کاربران", StringComparison.OrdinalIgnoreCase))) + if (!string.IsNullOrEmpty(tableName) && tableName.Equals("Users", StringComparison.OrdinalIgnoreCase)) { return new ForeignKeyRelation { ColumnName = c, TargetTable = "Courses", TargetColumn = "Title" }; } @@ -288,25 +287,23 @@ public void SetForeignKeyRelations(IEnumerable fks) } if (lower == "course_id" || lower == "courseid") { - if (!string.IsNullOrEmpty(tableName) && (tableName.Equals("Users", StringComparison.OrdinalIgnoreCase) || tableName.Equals("کاربران", StringComparison.OrdinalIgnoreCase))) + if (!string.IsNullOrEmpty(tableName) && tableName.Equals("Users", StringComparison.OrdinalIgnoreCase)) { return new ForeignKeyRelation { ColumnName = c, TargetTable = "Courses", TargetColumn = "Id" }; } return new ForeignKeyRelation { ColumnName = c, TargetTable = "Users", TargetColumn = "Id" }; } - if (lower == "user" || lower == "user_name" || lower == "username" || lower == "student" || lower == "student_name" || - lower == "کاربر" || lower == "کاربران" || lower == "نام_کاربر" || lower == "دانشجو" || lower == "دانشجویان" || lower == "نام_دانشجو" || lower == "نام_دانشجو") + if (lower == "user" || lower == "user_name" || lower == "username" || lower == "student" || lower == "student_name") { return new ForeignKeyRelation { ColumnName = c, TargetTable = "Users", TargetColumn = "Name" }; } - if (lower == "user_id" || lower == "userid" || lower == "student_id" || lower == "studentid" || - lower == "کد_کاربر" || lower == "کاربر_id" || lower == "کد_دانشجو") + if (lower == "user_id" || lower == "userid" || lower == "student_id" || lower == "studentid") { return new ForeignKeyRelation { ColumnName = c, TargetTable = "Users", TargetColumn = "Id" }; } - if (lower == "role" || lower == "role_name" || lower == "نقش" || lower == "نام_نقش") + if (lower == "role" || lower == "role_name") { return new ForeignKeyRelation { ColumnName = c, TargetTable = "Roles", TargetColumn = "Title" }; } @@ -382,6 +379,7 @@ public void SetPrimaryKeys(IEnumerable pks) } private IReadOnlyList? _lastColumns; + public IReadOnlyList? LastColumns => _lastColumns; [ObservableProperty] private string _lastQueryMessage = string.Empty; @@ -1061,7 +1059,7 @@ public void LoadSampleReferencedTable(string tableName, string? filterColumn = n ForeignKeyRelations.Clear(); ColumnsChanged?.Invoke(cols); } - else if (tableName.Equals("Users", StringComparison.OrdinalIgnoreCase) || tableName.Equals("کاربران", StringComparison.OrdinalIgnoreCase)) + else if (tableName.Equals("Users", StringComparison.OrdinalIgnoreCase)) { var userRows = new List { @@ -1079,7 +1077,6 @@ public void LoadSampleReferencedTable(string tableName, string? filterColumn = n r.Values["EMAIL"] = r.Email; r.Values["ROLE"] = r.Role; r.Values["COURSE"] = r.Course; - r.Values["دوره"] = r.Course; r.Values["STATUS"] = r.Status; r.Values["CREATED AT"] = r.CreatedAt; @@ -1095,7 +1092,6 @@ public void LoadSampleReferencedTable(string tableName, string? filterColumn = n ForeignKeyRelations.Clear(); ForeignKeyRelations["Role"] = new ForeignKeyRelation { ColumnName = "ROLE", TargetTable = "Roles", TargetColumn = "Title" }; ForeignKeyRelations["Course"] = new ForeignKeyRelation { ColumnName = "COURSE", TargetTable = "Courses", TargetColumn = "Title" }; - ForeignKeyRelations["دوره"] = new ForeignKeyRelation { ColumnName = "دوره", TargetTable = "Courses", TargetColumn = "Title" }; ColumnsChanged?.Invoke(cols); if (!string.IsNullOrWhiteSpace(filterValue)) @@ -1104,8 +1100,7 @@ public void LoadSampleReferencedTable(string tableName, string? filterColumn = n r.Id.ToString() == filterValue || r.Name.Equals(filterValue, StringComparison.OrdinalIgnoreCase) || r.Course.Equals(filterValue, StringComparison.OrdinalIgnoreCase) || - (r.Values.TryGetValue("COURSE", out var cVal) && cVal.Equals(filterValue, StringComparison.OrdinalIgnoreCase)) || - (r.Values.TryGetValue("دوره", out var dVal) && dVal.Equals(filterValue, StringComparison.OrdinalIgnoreCase)) + (r.Values.TryGetValue("COURSE", out var cVal) && cVal.Equals(filterValue, StringComparison.OrdinalIgnoreCase)) ); if (match != null) { @@ -1113,7 +1108,7 @@ public void LoadSampleReferencedTable(string tableName, string? filterColumn = n } } } - else if (tableName.Equals("Courses", StringComparison.OrdinalIgnoreCase) || tableName.Equals("دوره_ها", StringComparison.OrdinalIgnoreCase) || tableName.Equals("دوره", StringComparison.OrdinalIgnoreCase)) + else if (tableName.Equals("Courses", StringComparison.OrdinalIgnoreCase)) { var courseRows = new List { @@ -1128,11 +1123,8 @@ public void LoadSampleReferencedTable(string tableName, string? filterColumn = n { r.Values["ID"] = r.Id.ToString(); r.Values["TITLE"] = r.Name; - r.Values["دوره"] = r.Name; r.Values["USER_ID"] = r.Role; - r.Values["کد_کاربر"] = r.Role; r.Values["STUDENT_NAME"] = r.Email; - r.Values["نام_دانشجو"] = r.Email; r.Values["DURATION"] = r.CreatedAt; r.Values["STATUS"] = r.Status; @@ -1147,11 +1139,8 @@ public void LoadSampleReferencedTable(string tableName, string? filterColumn = n SetPrimaryKeys(new[] { "ID" }); ForeignKeyRelations.Clear(); ForeignKeyRelations["USER_ID"] = new ForeignKeyRelation { ColumnName = "USER_ID", TargetTable = "Users", TargetColumn = "Id" }; - ForeignKeyRelations["کد_کاربر"] = new ForeignKeyRelation { ColumnName = "کد_کاربر", TargetTable = "Users", TargetColumn = "Id" }; ForeignKeyRelations["STUDENT_NAME"] = new ForeignKeyRelation { ColumnName = "STUDENT_NAME", TargetTable = "Users", TargetColumn = "Name" }; - ForeignKeyRelations["نام_دانشجو"] = new ForeignKeyRelation { ColumnName = "نام_دانشجو", TargetTable = "Users", TargetColumn = "Name" }; ForeignKeyRelations["TITLE"] = new ForeignKeyRelation { ColumnName = "TITLE", TargetTable = "Users", TargetColumn = "Course" }; - ForeignKeyRelations["دوره"] = new ForeignKeyRelation { ColumnName = "دوره", TargetTable = "Users", TargetColumn = "Course" }; ColumnsChanged?.Invoke(cols); if (!string.IsNullOrWhiteSpace(filterValue)) diff --git a/ViewModels/SchemaPropertiesTableViewModel.cs b/ViewModels/SchemaPropertiesTableViewModel.cs index e24bd04..e6062ca 100644 --- a/ViewModels/SchemaPropertiesTableViewModel.cs +++ b/ViewModels/SchemaPropertiesTableViewModel.cs @@ -1,8 +1,8 @@ -using System.Collections.ObjectModel; +using System.Collections.ObjectModel; using CommunityToolkit.Mvvm.ComponentModel; -using Fdb.Models; +using Quell.Models; -namespace Fdb.ViewModels; +namespace Quell.ViewModels; public partial class SchemaPropertiesTableViewModel : ViewModelBase { diff --git a/ViewModels/SettingsModalViewModel.cs b/ViewModels/SettingsModalViewModel.cs index 5fbd024..aa87e7a 100644 --- a/ViewModels/SettingsModalViewModel.cs +++ b/ViewModels/SettingsModalViewModel.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Collections.ObjectModel; using System.Linq; @@ -6,9 +6,9 @@ using Avalonia.Media; using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; -using Fdb.Services; +using Quell.Services; -namespace Fdb.ViewModels; +namespace Quell.ViewModels; public partial class SettingsModalViewModel : ObservableObject { @@ -20,12 +20,16 @@ public partial class SettingsModalViewModel : ObservableObject [NotifyPropertyChangedFor(nameof(IsTableCategory))] [NotifyPropertyChangedFor(nameof(IsCustomizationCategory))] [NotifyPropertyChangedFor(nameof(IsShortcutsCategory))] + [NotifyPropertyChangedFor(nameof(IsPerformanceCategory))] + [NotifyPropertyChangedFor(nameof(IsTerminalCategory))] private string _selectedCategory = "Appearance"; public bool IsAppearanceCategory => string.Equals(SelectedCategory, "Appearance", StringComparison.OrdinalIgnoreCase); public bool IsTableCategory => string.Equals(SelectedCategory, "Table", StringComparison.OrdinalIgnoreCase); public bool IsCustomizationCategory => string.Equals(SelectedCategory, "Customization", StringComparison.OrdinalIgnoreCase); public bool IsShortcutsCategory => string.Equals(SelectedCategory, "Shortcuts", StringComparison.OrdinalIgnoreCase); + public bool IsPerformanceCategory => string.Equals(SelectedCategory, "Performance", StringComparison.OrdinalIgnoreCase); + public bool IsTerminalCategory => string.Equals(SelectedCategory, "Terminal", StringComparison.OrdinalIgnoreCase); #region Appearance & Theme Properties @@ -120,13 +124,21 @@ public partial class SettingsModalViewModel : ObservableObject [NotifyPropertyChangedFor(nameof(PreviewSplitterBrush))] private string _splitterColor = "Default"; + [ObservableProperty] + [NotifyPropertyChangedFor(nameof(PreviewTerminalBackgroundBrush))] + private string _terminalBackground = "Default"; + + [ObservableProperty] + [NotifyPropertyChangedFor(nameof(PreviewTerminalForegroundBrush))] + private string _terminalForeground = "Default"; + [ObservableProperty] private double _editorFontSize = 13.0; [ObservableProperty] private bool _editorShowLineNumbers = true; - #region Color Maker ("رنگ‌ساز") Properties + #region Color Maker Properties [ObservableProperty] [NotifyPropertyChangedFor(nameof(ColorMakerPreviewBrush))] @@ -174,7 +186,9 @@ public partial class SettingsModalViewModel : ObservableObject "Table: Alternating Row", "Table: Header Background", "Table: Grid Lines", - "Table: Cell Text" + "Table: Cell Text", + "Terminal: Background", + "Terminal: Text" }; public IBrush ColorMakerPreviewBrush @@ -212,6 +226,26 @@ public IBrush PreviewEditorBackgroundBrush } } + public IBrush PreviewTerminalBackgroundBrush + { + get + { + if (!string.IsNullOrWhiteSpace(TerminalBackground) && !TerminalBackground.Equals("Default", StringComparison.OrdinalIgnoreCase) && Color.TryParse(TerminalBackground, out var color)) + return new SolidColorBrush(color); + return IsDarkTheme ? new SolidColorBrush(Color.Parse("#0F172A")) : new SolidColorBrush(Color.Parse("#FFFFFF")); + } + } + + public IBrush PreviewTerminalForegroundBrush + { + get + { + if (!string.IsNullOrWhiteSpace(TerminalForeground) && !TerminalForeground.Equals("Default", StringComparison.OrdinalIgnoreCase) && Color.TryParse(TerminalForeground, out var color)) + return new SolidColorBrush(color); + return IsDarkTheme ? new SolidColorBrush(Color.Parse("#F8FAFC")) : new SolidColorBrush(Color.Parse("#0F172A")); + } + } + public IBrush PreviewEditorForegroundBrush { get @@ -510,6 +544,42 @@ public IBrush PreviewTableSelectionBrush #endregion + #region Terminal Settings Properties + + [ObservableProperty] + private string _terminalDefaultShell = "default"; + + [ObservableProperty] + private string _terminalFontFamily = "Cascadia Code, Consolas, Courier New, monospace"; + + [ObservableProperty] + private double _terminalFontSize = 13.0; + + [ObservableProperty] + private string _terminalCursorStyle = "Block"; + + [ObservableProperty] + private bool _terminalCursorBlink = true; + + [ObservableProperty] + private int _terminalMaxScrollback = 5000; + + [ObservableProperty] + private string _terminalPsqlPath = string.Empty; + + [ObservableProperty] + private string _terminalMySqlPath = string.Empty; + + [ObservableProperty] + private string _terminalSqlitePath = string.Empty; + + [ObservableProperty] + private string _terminalSqlCmdPath = string.Empty; + + public System.Collections.ObjectModel.ObservableCollection AvailableTerminalShells { get; } = new(); + + #endregion + public SettingsModalViewModel() { _selectedTheme = ThemeService.Instance.CurrentTheme; @@ -552,6 +622,8 @@ public async Task OpenAsync() SplitterColor = _initialPanelCustomization.SplitterColor; EditorFontSize = _initialPanelCustomization.EditorFontSize; EditorShowLineNumbers = _initialPanelCustomization.EditorShowLineNumbers; + TerminalBackground = _initialPanelCustomization.TerminalBackground; + TerminalForeground = _initialPanelCustomization.TerminalForeground; // 3. Load Table Customizations from SQLite _initialTableCustomization = await DatabaseStorageService.Instance.GetTableCustomizationAsync().ConfigureAwait(false); @@ -574,6 +646,34 @@ public async Task OpenAsync() // 4. Load Dynamic Shortcuts from SQLite await ShortcutService.Instance.InitializeAsync().ConfigureAwait(false); LoadShortcutsList(); + + // 5. Load Performance & Graphics Configuration from SQLite + _initialRenderingMode = await DatabaseStorageService.Instance.GetGraphicsRenderingModeAsync().ConfigureAwait(false); + SelectedRenderingMode = _initialRenderingMode; + GpuAdapterName = HardwareGraphicsService.Instance.GpuAdapterName; + IsGpuDetected = HardwareGraphicsService.Instance.IsGpuDetected; + ActiveRendererSummary = HardwareGraphicsService.Instance.ActiveRendererSummary; + VramCacheMb = HardwareGraphicsService.Instance.VramCacheMb; + OnPropertyChanged(nameof(EffectiveModeDescription)); + OnPropertyChanged(nameof(RequiresGraphicsRestart)); + // 5. Load Terminal Settings from SQLite + var termSettings = await Quell.Services.Terminal.TerminalSettingsService.Instance.LoadSettingsAsync().ConfigureAwait(false); + TerminalDefaultShell = termSettings.DefaultShellId; + TerminalFontFamily = termSettings.FontFamily; + TerminalFontSize = termSettings.FontSize; + TerminalCursorStyle = termSettings.CursorStyle; + TerminalCursorBlink = termSettings.CursorBlink; + TerminalMaxScrollback = termSettings.MaxScrollback; + TerminalPsqlPath = termSettings.PsqlPath; + TerminalMySqlPath = termSettings.MySqlPath; + TerminalSqlitePath = termSettings.SqlitePath; + TerminalSqlCmdPath = termSettings.SqlCmdPath; + + AvailableTerminalShells.Clear(); + foreach (var sh in Quell.Services.Terminal.ShellDetectorService.Instance.GetAvailableShells()) + { + AvailableTerminalShells.Add(sh); + } } catch (Exception ex) { @@ -582,6 +682,8 @@ public async Task OpenAsync() _initialTheme = SelectedTheme; SelectedKeywordColor = ThemeService.Instance.CurrentKeywordColor; _initialKeywordColor = SelectedKeywordColor; + SelectedRenderingMode = HardwareGraphicsService.Instance.RenderingMode; + _initialRenderingMode = SelectedRenderingMode; } IsOpen = true; @@ -659,6 +761,20 @@ public void SelectEditorBackground(string hex) LivePreviewPanelCustomization(); } + [RelayCommand] + public void SelectTerminalBackground(string hex) + { + TerminalBackground = string.IsNullOrWhiteSpace(hex) ? "Default" : hex; + LivePreviewPanelCustomization(); + } + + [RelayCommand] + public void SelectTerminalForeground(string hex) + { + TerminalForeground = string.IsNullOrWhiteSpace(hex) ? "Default" : hex; + LivePreviewPanelCustomization(); + } + [RelayCommand] public void SelectEditorForeground(string hex) { @@ -743,7 +859,7 @@ public void SelectSplitterColor(string hex) LivePreviewPanelCustomization(); } - #region Color Maker ("رنگ‌ساز") Methods & Commands + #region Color Maker Methods & Commands partial void OnColorMakerRChanged(double value) => UpdateHexFromRgb(); partial void OnColorMakerGChanged(double value) => UpdateHexFromRgb(); @@ -866,6 +982,12 @@ public void ApplyColorMakerToTarget() TableGridLinesColor = hex; LivePreviewTableCustomization(); break; + case "Terminal: Background": + TerminalBackground = hex; + break; + case "Terminal: Text": + TerminalForeground = hex; + break; case "Table: Cell Text": TableCellTextColor = hex; LivePreviewTableCustomization(); @@ -1031,6 +1153,8 @@ public void ResetPanelCustomization() ButtonBorderColor = "Default"; BorderColor = "Default"; SplitterColor = "Default"; + TerminalBackground = "Default"; + TerminalForeground = "Default"; EditorFontSize = 13.0; EditorShowLineNumbers = true; LivePreviewPanelCustomization(); @@ -1054,6 +1178,8 @@ private void LivePreviewPanelCustomization() BorderColor = BorderColor, SplitterColor = SplitterColor, TableBackground = TableBackground, + TerminalBackground = TerminalBackground, + TerminalForeground = TerminalForeground, EditorFontSize = EditorFontSize, EditorShowLineNumbers = EditorShowLineNumbers }; @@ -1061,6 +1187,8 @@ private void LivePreviewPanelCustomization() OnPropertyChanged(nameof(PreviewEditorBackgroundBrush)); OnPropertyChanged(nameof(PreviewEditorForegroundBrush)); + OnPropertyChanged(nameof(PreviewTerminalBackgroundBrush)); + OnPropertyChanged(nameof(PreviewTerminalForegroundBrush)); OnPropertyChanged(nameof(PreviewSidebarBackgroundBrush)); OnPropertyChanged(nameof(PreviewSidebarHeaderBrush)); OnPropertyChanged(nameof(PreviewSidebarHoverBrush)); @@ -1396,6 +1524,66 @@ public void ResetAllShortcuts() #endregion + #region Performance & Graphics Properties & Commands + + [ObservableProperty] + [NotifyPropertyChangedFor(nameof(IsAutoRenderingMode))] + [NotifyPropertyChangedFor(nameof(IsGpuRenderingMode))] + [NotifyPropertyChangedFor(nameof(IsCpuRenderingMode))] + [NotifyPropertyChangedFor(nameof(EffectiveModeDescription))] + [NotifyPropertyChangedFor(nameof(RequiresGraphicsRestart))] + private string _selectedRenderingMode = "Auto"; // "Auto", "GPU", "CPU" + + public bool IsAutoRenderingMode => string.Equals(SelectedRenderingMode, "Auto", StringComparison.OrdinalIgnoreCase); + public bool IsGpuRenderingMode => string.Equals(SelectedRenderingMode, "GPU", StringComparison.OrdinalIgnoreCase); + public bool IsCpuRenderingMode => string.Equals(SelectedRenderingMode, "CPU", StringComparison.OrdinalIgnoreCase); + + [ObservableProperty] + private string _gpuAdapterName = "Detecting Graphics Adapter..."; + + [ObservableProperty] + private bool _isGpuDetected; + + [ObservableProperty] + private string _activeRendererSummary = "Skia Hardware (DirectX 11 / ANGLE)"; + + [ObservableProperty] + private int _vramCacheMb = 128; + + private string _initialRenderingMode = "Auto"; + + public bool RequiresGraphicsRestart => !string.Equals(SelectedRenderingMode, _initialRenderingMode, StringComparison.OrdinalIgnoreCase); + + public string EffectiveModeDescription + { + get + { + string eff = HardwareGraphicsService.Instance.ResolveEffectiveMode(SelectedRenderingMode); + if (string.Equals(SelectedRenderingMode, "Auto", StringComparison.OrdinalIgnoreCase)) + { + return IsGpuDetected + ? $"Auto-detect: Dedicated/Integrated GPU detected -> Active Engine: GPU Hardware Acceleration ({GpuAdapterName})" + : "Auto-detect: No discrete GPU detected -> Active Engine: CPU Software Rasterizer (Lightweight)"; + } + if (string.Equals(SelectedRenderingMode, "GPU", StringComparison.OrdinalIgnoreCase)) + { + return $"Forced GPU Mode: Hardware-accelerated DirectX 11 / ANGLE pipeline with {VramCacheMb}MB VRAM cache"; + } + return $"Forced CPU Mode: Minimal RAM footprint with CPU Software Rasterizer ({VramCacheMb}MB buffer)"; + } + } + + [RelayCommand] + public void SelectRenderingMode(string mode) + { + if (string.IsNullOrWhiteSpace(mode)) return; + SelectedRenderingMode = mode; + OnPropertyChanged(nameof(EffectiveModeDescription)); + OnPropertyChanged(nameof(RequiresGraphicsRestart)); + } + + #endregion + #region Appearance Commands [RelayCommand] @@ -1407,6 +1595,8 @@ public void SelectTheme(string theme) OnPropertyChanged(nameof(CurrentKeywordBrush)); OnPropertyChanged(nameof(PreviewEditorBackgroundBrush)); OnPropertyChanged(nameof(PreviewEditorForegroundBrush)); + OnPropertyChanged(nameof(PreviewTerminalBackgroundBrush)); + OnPropertyChanged(nameof(PreviewTerminalForegroundBrush)); OnPropertyChanged(nameof(PreviewSidebarBackgroundBrush)); OnPropertyChanged(nameof(PreviewSidebarHeaderBrush)); OnPropertyChanged(nameof(PreviewSidebarHoverBrush)); @@ -1469,6 +1659,8 @@ public async Task SaveAsync() BorderColor = BorderColor, SplitterColor = SplitterColor, TableBackground = TableBackground, + TerminalBackground = TerminalBackground, + TerminalForeground = TerminalForeground, EditorFontSize = EditorFontSize, EditorShowLineNumbers = EditorShowLineNumbers }; @@ -1501,6 +1693,29 @@ public async Task SaveAsync() // 4. Save Shortcuts to SQLite await ShortcutService.Instance.SaveAllAsync(Shortcuts).ConfigureAwait(false); + + // 5. Save Graphics Rendering Mode to SQLite + await DatabaseStorageService.Instance.SaveGraphicsRenderingModeAsync(SelectedRenderingMode).ConfigureAwait(false); + HardwareGraphicsService.Instance.RenderingMode = SelectedRenderingMode; + HardwareGraphicsService.Instance.EffectiveRenderingMode = HardwareGraphicsService.Instance.ResolveEffectiveMode(SelectedRenderingMode); + HardwareGraphicsService.Instance.NotifyRenderingModeChanged(); + _initialRenderingMode = SelectedRenderingMode; + OnPropertyChanged(nameof(RequiresGraphicsRestart)); + // 5. Save Terminal Settings to SQLite + var termSettingsToSave = new Quell.Models.TerminalSettingsModel + { + DefaultShellId = TerminalDefaultShell, + FontFamily = TerminalFontFamily, + FontSize = TerminalFontSize, + CursorStyle = TerminalCursorStyle, + CursorBlink = TerminalCursorBlink, + MaxScrollback = TerminalMaxScrollback, + PsqlPath = TerminalPsqlPath, + MySqlPath = TerminalMySqlPath, + SqlitePath = TerminalSqlitePath, + SqlCmdPath = TerminalSqlCmdPath + }; + await Quell.Services.Terminal.TerminalSettingsService.Instance.SaveSettingsAsync(termSettingsToSave).ConfigureAwait(false); } catch (Exception ex) { @@ -1533,6 +1748,12 @@ public void Close() // Revert Table Customization ThemeService.Instance.ApplyTableCustomization(_initialTableCustomization); + // Revert Rendering Mode + if (!string.Equals(SelectedRenderingMode, _initialRenderingMode, StringComparison.OrdinalIgnoreCase)) + { + SelectedRenderingMode = _initialRenderingMode; + } + IsOpen = false; } } diff --git a/ViewModels/SidebarTreeNodes.cs b/ViewModels/SidebarTreeNodes.cs index 2a1fdd6..fc2257a 100644 --- a/ViewModels/SidebarTreeNodes.cs +++ b/ViewModels/SidebarTreeNodes.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Collections.ObjectModel; using System.Linq; @@ -6,10 +6,10 @@ using Avalonia.Input.Platform; using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; -using Fdb.Services; -using Fdb.Services.DatabaseProviders; +using Quell.Services; +using Quell.Services.DatabaseProviders; -namespace Fdb.ViewModels; +namespace Quell.ViewModels; public abstract partial class SidebarTreeNodeBase : ViewModelBase { diff --git a/ViewModels/SidebarViewModel.cs b/ViewModels/SidebarViewModel.cs index b816fa8..8a0a711 100644 --- a/ViewModels/SidebarViewModel.cs +++ b/ViewModels/SidebarViewModel.cs @@ -1,13 +1,13 @@ -using System; +using System; using System.Collections.ObjectModel; using System.Linq; using System.Threading.Tasks; using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; -using Fdb.Services; -using Fdb.Services.DatabaseProviders; +using Quell.Services; +using Quell.Services.DatabaseProviders; -namespace Fdb.ViewModels; +namespace Quell.ViewModels; public partial class SidebarViewModel : ViewModelBase { @@ -230,7 +230,7 @@ public async Task RefreshAsync() { // Load saved connections from SQLite var conns = (await DatabaseStorageService.Instance.GetRecentConnectionsAsync()) - .Where(c => !c.Server.Contains("fdb_studio", StringComparison.OrdinalIgnoreCase) && !c.DatabaseName.Contains("fdb_studio", StringComparison.OrdinalIgnoreCase)) + .Where(c => !c.Server.Contains("quell", StringComparison.OrdinalIgnoreCase) && !c.DatabaseName.Contains("quell", StringComparison.OrdinalIgnoreCase) && !c.Server.Contains("fdb_studio", StringComparison.OrdinalIgnoreCase) && !c.DatabaseName.Contains("fdb_studio", StringComparison.OrdinalIgnoreCase)) .ToList(); SavedConnections.Clear(); foreach (var c in conns) diff --git a/ViewModels/SplashScreenViewModel.cs b/ViewModels/SplashScreenViewModel.cs index 81a61d1..5ca3d64 100644 --- a/ViewModels/SplashScreenViewModel.cs +++ b/ViewModels/SplashScreenViewModel.cs @@ -1,15 +1,18 @@ using CommunityToolkit.Mvvm.ComponentModel; -namespace Fdb.ViewModels; +namespace Quell.ViewModels; public partial class SplashScreenViewModel : ViewModelBase { [ObservableProperty] - private string _statusText = "Initializing Fdb Studio..."; + private string _statusText = "Initializing Quell..."; [ObservableProperty] private double _progressValue = 0; + [ObservableProperty] + private string _subStatusText = "Connecting to local storage engine..."; + [ObservableProperty] private string _version = "v1.0.0 Developer Preview"; } diff --git a/ViewModels/TerminalSessionViewModel.cs b/ViewModels/TerminalSessionViewModel.cs new file mode 100644 index 0000000..0942546 --- /dev/null +++ b/ViewModels/TerminalSessionViewModel.cs @@ -0,0 +1,174 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Runtime.InteropServices; +using System.Threading.Tasks; +using Avalonia; +using Avalonia.Controls.ApplicationLifetimes; +using Avalonia.Input.Platform; +using CommunityToolkit.Mvvm.ComponentModel; +using CommunityToolkit.Mvvm.Input; +using Quell.Models; + +namespace Quell.ViewModels; + +public partial class TerminalSessionViewModel : ViewModelBase +{ + public string Id { get; } = Guid.NewGuid().ToString("N"); + + [ObservableProperty] + private string _title = "Terminal"; + + [ObservableProperty] + private string _iconKey = "Terminal"; + + [ObservableProperty] + private string _executablePath = string.Empty; + + [ObservableProperty] + private string _arguments = string.Empty; + + [ObservableProperty] + private string _workingDirectory = string.Empty; + + [ObservableProperty] + private bool _isActive; + + [ObservableProperty] + [NotifyPropertyChangedFor(nameof(StatusText))] + [NotifyPropertyChangedFor(nameof(StatusColor))] + private bool _isRunning; + + [ObservableProperty] + [NotifyPropertyChangedFor(nameof(StatusText))] + private int? _exitCode; + + [ObservableProperty] + private bool _hasCliError; + + [ObservableProperty] + private string _cliErrorTitle = string.Empty; + + [ObservableProperty] + private string _cliErrorMessage = string.Empty; + + [ObservableProperty] + private string _cliDownloadUrl = string.Empty; + + [ObservableProperty] + private bool _isCopied; + + public string StatusText => IsRunning ? "Running" : (ExitCode.HasValue ? $"Exited ({ExitCode.Value})" : "Ready"); + public string StatusColor => IsRunning ? "#10B981" : (ExitCode.HasValue && ExitCode.Value != 0 ? "#EF4444" : "#94A3B8"); + + public string ShellBadge + { + get + { + if (Title.Contains("PowerShell", StringComparison.OrdinalIgnoreCase)) return "PowerShell"; + if (Title.Contains("Command Prompt", StringComparison.OrdinalIgnoreCase) || Title.Contains("cmd", StringComparison.OrdinalIgnoreCase)) return "CMD"; + if (Title.Contains("Git Bash", StringComparison.OrdinalIgnoreCase) || Title.Contains("bash", StringComparison.OrdinalIgnoreCase)) return "Bash"; + if (Title.Contains("WSL", StringComparison.OrdinalIgnoreCase)) return "WSL"; + if (IconKey == "Database") return "DB CLI"; + return "Shell"; + } + } + + public Dictionary EnvironmentVariables { get; } = new(); + + public event Action? SendTextRequested; + public event Action? ClearScreenRequested; + public event Action? KillRequested; + public event Action? FocusRequested; + public event Action? CloseRequested; + public event Action? RestartRequested; + public event Func? CopyToClipboardRequested; + + public void SendText(string text) + { + SendTextRequested?.Invoke(text); + } + + public void ClearScreen() + { + ClearScreenRequested?.Invoke(); + } + + public void Kill() + { + KillRequested?.Invoke(); + } + + public void Focus() + { + FocusRequested?.Invoke(); + } + + [RelayCommand] + public async Task CopyWorkingDirectoryAsync() + { + if (string.IsNullOrWhiteSpace(WorkingDirectory)) return; + + try + { + if (CopyToClipboardRequested != null) + { + await CopyToClipboardRequested.Invoke(WorkingDirectory); + } + else + { + await Quell.Services.ClipboardService.CopyTextAsync(WorkingDirectory); + } + + IsCopied = true; + await Task.Delay(1500); + IsCopied = false; + } + catch (Exception ex) + { + Console.WriteLine($"[TerminalSessionViewModel] Copy to clipboard failed: {ex.Message}"); + } + } + + [RelayCommand] + public void CloseSession() + { + CloseRequested?.Invoke(this); + } + + [RelayCommand] + public void RestartSession() + { + RestartRequested?.Invoke(); + } + + [RelayCommand] + public void OpenDownloadUrl() + { + if (string.IsNullOrWhiteSpace(CliDownloadUrl)) return; + + try + { + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + Process.Start(new ProcessStartInfo + { + FileName = CliDownloadUrl, + UseShellExecute = true + }); + } + else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) + { + Process.Start("xdg-open", CliDownloadUrl); + } + else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) + { + Process.Start("open", CliDownloadUrl); + } + } + catch (Exception ex) + { + Console.WriteLine($"[TerminalSessionViewModel] Failed to open download url: {ex.Message}"); + } + } +} diff --git a/ViewModels/TerminalViewModel.cs b/ViewModels/TerminalViewModel.cs new file mode 100644 index 0000000..d8f5917 --- /dev/null +++ b/ViewModels/TerminalViewModel.cs @@ -0,0 +1,261 @@ +using System; +using System.Collections.ObjectModel; +using System.IO; +using System.Linq; +using System.Threading.Tasks; +using CommunityToolkit.Mvvm.ComponentModel; +using CommunityToolkit.Mvvm.Input; +using Quell.Models; +using Quell.Services.DatabaseProviders; +using Quell.Services.Terminal; + +namespace Quell.ViewModels; + +public partial class TerminalViewModel : ViewModelBase +{ + private readonly ShellDetectorService _shellDetector = ShellDetectorService.Instance; + private readonly DatabaseCliService _cliService = DatabaseCliService.Instance; + private readonly TerminalSettingsService _settingsService = TerminalSettingsService.Instance; + + [ObservableProperty] + private TerminalSessionViewModel? _activeSession; + + [ObservableProperty] + private ShellProfile? _selectedShellProfile; + + [ObservableProperty] + private string _activeConnectionName = string.Empty; + + [ObservableProperty] + private bool _hasActiveConnection; + + [ObservableProperty] + private string _activeConnectionBadge = string.Empty; + + [ObservableProperty] + private TerminalSettingsModel _settings = new(); + + public ObservableCollection Sessions { get; } = new(); + public ObservableCollection AvailableShells { get; } = new(); + + public event Action? RequestFocus; + + public TerminalViewModel() + { + InitializeShells(); + _settingsService.SettingsChanged += (_, s) => Settings = s; + _ = LoadSettingsAsync(); + } + + private async Task LoadSettingsAsync() + { + Settings = await _settingsService.LoadSettingsAsync(); + } + + public void InitializeShells() + { + AvailableShells.Clear(); + foreach (var shell in _shellDetector.GetAvailableShells()) + { + AvailableShells.Add(shell); + } + + var defaultShell = _shellDetector.GetDefaultShell(Settings.DefaultShellId); + SelectedShellProfile = defaultShell; + } + + public void UpdateConnectionContext(DatabaseConnectionConfig? config) + { + if (config != null) + { + HasActiveConnection = true; + ActiveConnectionName = config.DisplayName; + ActiveConnectionBadge = $"{config.DatabaseType}: {config.DisplayName}"; + } + else + { + HasActiveConnection = false; + ActiveConnectionName = string.Empty; + ActiveConnectionBadge = string.Empty; + } + } + + [RelayCommand] + public void NewSession(object? parameter = null) + { + var targetProfile = (parameter as ShellProfile) ?? SelectedShellProfile ?? _shellDetector.GetDefaultShell(Settings.DefaultShellId); + var sessionCount = Sessions.Count(s => s.Title.StartsWith(targetProfile.DisplayName, StringComparison.OrdinalIgnoreCase)) + 1; + + var session = new TerminalSessionViewModel + { + Title = $"{targetProfile.DisplayName} ({sessionCount})", + IconKey = targetProfile.IconKey, + ExecutablePath = targetProfile.ExecutablePath, + Arguments = targetProfile.Arguments, + WorkingDirectory = Directory.GetCurrentDirectory() + }; + + // Inject active connection environment variables if any + if (DatabaseSessionService.Instance.ActiveConfig != null) + { + var env = _cliService.BuildConnectionEnvironment(DatabaseSessionService.Instance.ActiveConfig); + foreach (var kvp in env) + { + session.EnvironmentVariables[kvp.Key] = kvp.Value; + } + } + + AddAndActivateSession(session); + } + + [RelayCommand] + public void LaunchDatabaseCli(DatabaseConnectionConfig? connectionConfig = null) + { + var config = connectionConfig ?? DatabaseSessionService.Instance.ActiveConfig; + if (config == null) + { + // If no active connection, fallback to default shell + NewSession(); + return; + } + + var cliInfo = _cliService.ResolveCli(config, Settings); + var dbTitle = $"{config.DatabaseType} ({config.DisplayName})"; + + var session = new TerminalSessionViewModel + { + Title = dbTitle, + IconKey = "Database", + WorkingDirectory = Directory.GetCurrentDirectory() + }; + + if (cliInfo.IsAvailable) + { + session.ExecutablePath = cliInfo.ExecutablePath; + session.Arguments = string.Join(" ", cliInfo.Arguments); + foreach (var kvp in cliInfo.EnvironmentVariables) + { + session.EnvironmentVariables[kvp.Key] = kvp.Value; + } + } + else + { + session.HasCliError = true; + session.CliErrorTitle = $"{cliInfo.MissingToolName} Not Found"; + session.CliErrorMessage = cliInfo.Instructions; + session.CliDownloadUrl = cliInfo.DownloadUrl; + } + + AddAndActivateSession(session); + } + + [RelayCommand] + public void SelectSession(TerminalSessionViewModel session) + { + if (session == null || !Sessions.Contains(session)) return; + + foreach (var s in Sessions) + { + s.IsActive = (s == session); + } + + ActiveSession = session; + RequestFocus?.Invoke(); + } + + [RelayCommand] + public void CloseSession(TerminalSessionViewModel? session = null) + { + var target = session ?? ActiveSession; + if (target == null || !Sessions.Contains(target)) return; + + target.Kill(); + var index = Sessions.IndexOf(target); + Sessions.Remove(target); + + if (Sessions.Count > 0) + { + var nextIndex = Math.Clamp(index, 0, Sessions.Count - 1); + SelectSession(Sessions[nextIndex]); + } + else + { + ActiveSession = null; + } + } + + [RelayCommand] + public void CloseOtherSessions(TerminalSessionViewModel? session = null) + { + var target = session ?? ActiveSession; + if (target == null || !Sessions.Contains(target)) return; + + var others = Sessions.Where(s => s != target).ToList(); + foreach (var other in others) + { + other.Kill(); + Sessions.Remove(other); + } + + SelectSession(target); + } + + [RelayCommand] + public void ClearActiveBuffer() + { + if (ActiveSession != null) + { + ActiveSession.ClearScreen(); + ActiveSession.SendText("\u000c"); // FormFeed / Ctrl+L + } + } + + public void SendTextToActiveTerminal(string text) + { + if (ActiveSession == null) + { + NewSession(); + } + + if (ActiveSession != null) + { + ActiveSession.SendText(text.EndsWith("\n") ? text : text + Environment.NewLine); + RequestFocus?.Invoke(); + } + } + + private void AddAndActivateSession(TerminalSessionViewModel session) + { + session.CloseRequested += CloseSession; + session.RestartRequested += () => RestartSession(session); + + foreach (var s in Sessions) + { + s.IsActive = false; + } + session.IsActive = true; + ActiveSession = session; + + Sessions.Add(session); + RequestFocus?.Invoke(); + } + + private void RestartSession(TerminalSessionViewModel session) + { + session.HasCliError = false; + if (DatabaseSessionService.Instance.ActiveConfig != null && session.IconKey == "Database") + { + var cliInfo = _cliService.ResolveCli(DatabaseSessionService.Instance.ActiveConfig, Settings); + if (cliInfo.IsAvailable) + { + session.ExecutablePath = cliInfo.ExecutablePath; + session.Arguments = string.Join(" ", cliInfo.Arguments); + session.HasCliError = false; + } + else + { + session.HasCliError = true; + } + } + } +} diff --git a/ViewModels/ViewModelBase.cs b/ViewModels/ViewModelBase.cs index fd2fea3..62e699b 100644 --- a/ViewModels/ViewModelBase.cs +++ b/ViewModels/ViewModelBase.cs @@ -1,6 +1,6 @@ -using CommunityToolkit.Mvvm.ComponentModel; +using CommunityToolkit.Mvvm.ComponentModel; -namespace Fdb.ViewModels; +namespace Quell.ViewModels; public abstract class ViewModelBase : ObservableObject { diff --git a/Views/MainView.axaml b/Views/MainView.axaml index c4eae13..b089e23 100644 --- a/Views/MainView.axaml +++ b/Views/MainView.axaml @@ -1,8 +1,8 @@ @@ -71,5 +71,6 @@ + diff --git a/Views/MainView.axaml.cs b/Views/MainView.axaml.cs index a01d772..613e801 100644 --- a/Views/MainView.axaml.cs +++ b/Views/MainView.axaml.cs @@ -1,11 +1,16 @@ using Avalonia.Controls; +using Quell.Services; -namespace Fdb.Views; +namespace Quell.Views; public partial class MainView : UserControl { public MainView() { InitializeComponent(); + if (HardwareGraphicsService.Instance.IsGpuMotionActive) + { + Classes.Add("gpu-mode"); + } } } diff --git a/Views/MainWindow.axaml b/Views/MainWindow.axaml index 43a70b2..8d9778a 100644 --- a/Views/MainWindow.axaml +++ b/Views/MainWindow.axaml @@ -1,13 +1,16 @@ @@ -76,5 +79,6 @@ + diff --git a/Views/MainWindow.axaml.cs b/Views/MainWindow.axaml.cs index a2d96f6..986d62e 100644 --- a/Views/MainWindow.axaml.cs +++ b/Views/MainWindow.axaml.cs @@ -3,10 +3,10 @@ using Avalonia.Controls; using Avalonia.Input; using Avalonia.Interactivity; -using Fdb.Services; -using Fdb.ViewModels; +using Quell.Services; +using Quell.ViewModels; -namespace Fdb.Views; +namespace Quell.Views; public partial class MainWindow : Window { @@ -32,6 +32,25 @@ public MainWindow() // Global Tunneling Key Handler: intercepts application shortcuts (Ctrl+B, F5, Ctrl+J, etc.) // at the Window root level before any child control (e.g. AvaloniaEdit TextEditor) can swallow them! AddHandler(KeyDownEvent, OnWindowKeyDownTunnel, RoutingStrategies.Tunnel); + + UpdateGpuModeClass(); + HardwareGraphicsService.Instance.RenderingModeChanged += (s, e) => Avalonia.Threading.Dispatcher.UIThread.Post(UpdateGpuModeClass); + } + + public void UpdateGpuModeClass() + { + bool isGpu = HardwareGraphicsService.Instance.IsGpuMotionActive; + if (isGpu) + { + if (!Classes.Contains("gpu-mode")) + { + Classes.Add("gpu-mode"); + } + } + else + { + Classes.Remove("gpu-mode"); + } } private void OnWindowKeyDownTunnel(object? sender, KeyEventArgs e) @@ -199,11 +218,16 @@ private void HandleApplicationShortcut(KeyEventArgs e) if (e.Handled || _viewModel == null) return; // If a modal dialog is open, do not execute workspace shortcuts (except Escape) - if (_viewModel.SettingsModal.IsOpen || _viewModel.ConnectModal.IsOpen || _viewModel.ConfirmModal.IsOpen || _viewModel.ExportModal.IsOpen) + if (_viewModel.SettingsModal.IsOpen || _viewModel.ConnectModal.IsOpen || _viewModel.ConfirmModal.IsOpen || _viewModel.ExportModal.IsOpen || _viewModel.DiagramModal.IsOpen) { if (e.Key == Key.Escape) { - if (_viewModel.ConnectModal.IsOpen) + if (_viewModel.DiagramModal.IsOpen) + { + _viewModel.DiagramModal.IsOpen = false; + e.Handled = true; + } + else if (_viewModel.ConnectModal.IsOpen) { _viewModel.ConnectModal.IsOpen = false; e.Handled = true; @@ -296,6 +320,24 @@ private void HandleApplicationShortcut(KeyEventArgs e) return; } + // Toggle Terminal (Ctrl+` or Ctrl+Tilde) + if (ShortcutService.Instance.IsMatch(ShortcutActions.ToggleTerminal, e) || + (e.KeyModifiers == KeyModifiers.Control && (e.Key == Key.Oem3 || e.Key == Key.OemTilde))) + { + _viewModel.ToggleTerminalCommand.Execute(null); + e.Handled = true; + return; + } + + // Run in Terminal (Ctrl+Shift+Enter) + if (ShortcutService.Instance.IsMatch(ShortcutActions.RunInTerminal, e) || + (e.KeyModifiers == (KeyModifiers.Control | KeyModifiers.Shift) && e.Key == Key.Enter)) + { + _viewModel.RunSelectionInTerminalCommand.Execute(null); + e.Handled = true; + return; + } + // Export Data if (ShortcutService.Instance.IsMatch(ShortcutActions.ExportData, e) || (e.Key == Key.E && e.KeyModifiers == KeyModifiers.Control)) { diff --git a/Views/SplashScreenWindow.axaml b/Views/SplashScreenWindow.axaml index 0c3b42d..624ffae 100644 --- a/Views/SplashScreenWindow.axaml +++ b/Views/SplashScreenWindow.axaml @@ -1,19 +1,20 @@ + Background="#0B0F14" + Focusable="False"> diff --git a/Views/SplashScreenWindow.axaml.cs b/Views/SplashScreenWindow.axaml.cs index 8cdaac9..1a421d5 100644 --- a/Views/SplashScreenWindow.axaml.cs +++ b/Views/SplashScreenWindow.axaml.cs @@ -1,12 +1,12 @@ -using System; +using System; using System.Threading.Tasks; using Avalonia.Controls; using Avalonia.Input; using Avalonia.Threading; -using Fdb.Services; -using Fdb.ViewModels; +using Quell.Services; +using Quell.ViewModels; -namespace Fdb.Views; +namespace Quell.Views; public partial class SplashScreenWindow : Window { @@ -47,38 +47,50 @@ public void Dismiss() _dismissTcs.TrySetResult(true); } - public async Task RunStartupSequenceAsync(int durationMs = 0) + public void UpdateProgress(double progress, string status, string subStatus = "") { var vm = ViewModel; - _isDatabaseReady = false; + vm.ProgressValue = progress; + vm.StatusText = status; + if (!string.IsNullOrWhiteSpace(subStatus)) + { + vm.SubStatusText = subStatus; + } - void UpdateProgress(double progress, string status) + if (!Dispatcher.UIThread.CheckAccess()) { - vm.ProgressValue = progress; - vm.StatusText = status; - if (!Dispatcher.UIThread.CheckAccess()) + Dispatcher.UIThread.Post(() => { - Dispatcher.UIThread.Post(() => + vm.ProgressValue = progress; + vm.StatusText = status; + if (!string.IsNullOrWhiteSpace(subStatus)) { - vm.ProgressValue = progress; - vm.StatusText = status; - }); - } + vm.SubStatusText = subStatus; + } + }); } + } + + public async Task RunStartupSequenceAsync(bool completeTo100 = true) + { + _isDatabaseReady = false; - UpdateProgress(5, "Connecting to SQLite Storage Engine..."); + UpdateProgress(5, "Connecting to SQLite Storage Engine...", "Verifying database file and schemas..."); // Preload all startup data, run SQLite schema migrations, cache settings, theme, keywords and connections - await DatabaseStorageService.Instance.PreloadStartupDataAsync((progress, status) => + await DatabaseStorageService.Instance.PreloadStartupDataAsync(async (progress, status, subStatus) => { - UpdateProgress(progress, status); + UpdateProgress(progress, status, subStatus); + // Brief micro-delay so the UI thread renders this real step's progress and the user can see it + await Task.Delay(40).ConfigureAwait(false); }).ConfigureAwait(false); _isDatabaseReady = true; - UpdateProgress(100, "Studio Environment Ready — SQLite Engine Initialized."); - - // Short smooth transition grace period so 100% progress is cleanly visible - await Task.Delay(180).ConfigureAwait(false); + if (completeTo100) + { + UpdateProgress(100, "Studio Environment Ready — SQLite Engine Initialized.", "All configurations loaded."); + await Task.Delay(180).ConfigureAwait(false); + } } } diff --git a/app.manifest b/app.manifest index 8534d68..5bb8164 100644 --- a/app.manifest +++ b/app.manifest @@ -3,7 +3,7 @@ - + diff --git a/packaging/linux/build-deb.sh b/packaging/linux/build-deb.sh index 2f5ee6c..957c220 100644 --- a/packaging/linux/build-deb.sh +++ b/packaging/linux/build-deb.sh @@ -13,7 +13,7 @@ mkdir -p "$DIST_DIR" DIST_DIR="$(cd "$DIST_DIR" && pwd)" PUBLISH_DIR="$(cd "$PUBLISH_DIR" && pwd)" -echo "Building Debian package for Fdb Studio v$VERSION ($ARCH)..." +echo "Building Debian package for Quell v$VERSION ($ARCH)..." echo "Source publish dir: $PUBLISH_DIR" echo "Target dist dir: $DIST_DIR" @@ -22,45 +22,53 @@ trap 'rm -rf "$PACKAGE_ROOT"' EXIT # Create packaging directory tree mkdir -p "$PACKAGE_ROOT/DEBIAN" -mkdir -p "$PACKAGE_ROOT/usr/lib/fdb-studio" +mkdir -p "$PACKAGE_ROOT/usr/lib/quell" mkdir -p "$PACKAGE_ROOT/usr/bin" mkdir -p "$PACKAGE_ROOT/usr/share/applications" mkdir -p "$PACKAGE_ROOT/usr/share/icons/hicolor/256x256/apps" # Copy published application binaries & assets -cp -r "$PUBLISH_DIR"/* "$PACKAGE_ROOT/usr/lib/fdb-studio/" -if [ -f "$PACKAGE_ROOT/usr/lib/fdb-studio/Fdb" ]; then - chmod +x "$PACKAGE_ROOT/usr/lib/fdb-studio/Fdb" +cp -r "$PUBLISH_DIR"/* "$PACKAGE_ROOT/usr/lib/quell/" +if [ -f "$PACKAGE_ROOT/usr/lib/quell/Quell" ]; then + chmod +x "$PACKAGE_ROOT/usr/lib/quell/Quell" +elif [ -f "$PACKAGE_ROOT/usr/lib/quell/Fdb" ]; then + chmod +x "$PACKAGE_ROOT/usr/lib/quell/Fdb" fi # Create symlink in /usr/bin -ln -sf "/usr/lib/fdb-studio/Fdb" "$PACKAGE_ROOT/usr/bin/fdb-studio" +if [ -f "$PACKAGE_ROOT/usr/lib/quell/Quell" ]; then + ln -sf "/usr/lib/quell/Quell" "$PACKAGE_ROOT/usr/bin/quell" +else + ln -sf "/usr/lib/quell/Fdb" "$PACKAGE_ROOT/usr/bin/quell" +fi # Copy desktop launcher -cp "$SCRIPT_DIR/fdb-studio.desktop" "$PACKAGE_ROOT/usr/share/applications/fdb-studio.desktop" +cp "$SCRIPT_DIR/quell.desktop" "$PACKAGE_ROOT/usr/share/applications/quell.desktop" # Copy icon if available -if [ -f "$ROOT_DIR/Assets/fdb_icon.png" ]; then - cp "$ROOT_DIR/Assets/fdb_icon.png" "$PACKAGE_ROOT/usr/share/icons/hicolor/256x256/apps/fdb-studio.png" -elif [ -f "$ROOT_DIR/Assets/fdb_icon.ico" ]; then - cp "$ROOT_DIR/Assets/fdb_icon.ico" "$PACKAGE_ROOT/usr/share/icons/hicolor/256x256/apps/fdb-studio.ico" +if [ -f "$ROOT_DIR/Assets/quell_icon.png" ]; then + cp "$ROOT_DIR/Assets/quell_icon.png" "$PACKAGE_ROOT/usr/share/icons/hicolor/256x256/apps/quell.png" +elif [ -f "$ROOT_DIR/Assets/fdb_icon.png" ]; then + cp "$ROOT_DIR/Assets/fdb_icon.png" "$PACKAGE_ROOT/usr/share/icons/hicolor/256x256/apps/quell.png" +elif [ -f "$ROOT_DIR/Assets/quell_icon.ico" ]; then + cp "$ROOT_DIR/Assets/quell_icon.ico" "$PACKAGE_ROOT/usr/share/icons/hicolor/256x256/apps/quell.ico" elif [ -f "$ROOT_DIR/Assets/logo.png" ]; then - cp "$ROOT_DIR/Assets/logo.png" "$PACKAGE_ROOT/usr/share/icons/hicolor/256x256/apps/fdb-studio.png" + cp "$ROOT_DIR/Assets/logo.png" "$PACKAGE_ROOT/usr/share/icons/hicolor/256x256/apps/quell.png" elif [ -f "$ROOT_DIR/Assets/logo.ico" ]; then - cp "$ROOT_DIR/Assets/logo.ico" "$PACKAGE_ROOT/usr/share/icons/hicolor/256x256/apps/fdb-studio.ico" + cp "$ROOT_DIR/Assets/logo.ico" "$PACKAGE_ROOT/usr/share/icons/hicolor/256x256/apps/quell.ico" elif [ -f "$ROOT_DIR/Assets/avalonia-logo.ico" ]; then - cp "$ROOT_DIR/Assets/avalonia-logo.ico" "$PACKAGE_ROOT/usr/share/icons/hicolor/256x256/apps/fdb-studio.ico" + cp "$ROOT_DIR/Assets/avalonia-logo.ico" "$PACKAGE_ROOT/usr/share/icons/hicolor/256x256/apps/quell.ico" fi # Write DEBIAN/control cat < "$PACKAGE_ROOT/DEBIAN/control" -Package: fdb-studio +Package: quell Version: $VERSION Section: utils Priority: optional Architecture: $ARCH Maintainer: CipherUnits -Description: Fdb Studio - Modern Cross-Platform Database Client and SQL IDE +Description: Quell - Modern Cross-Platform Database Client and SQL IDE A rich, cross-platform database management tool and SQL IDE built with Avalonia UI. Supports SQL Server, SQLite, IntelliSense autocomplete, and modular theming. EOF @@ -70,7 +78,7 @@ INSTALLED_SIZE=$(du -sk "$PACKAGE_ROOT/usr" | awk '{print $1}') echo "Installed-Size: $INSTALLED_SIZE" >> "$PACKAGE_ROOT/DEBIAN/control" # Build debian package -DEB_FILE="$DIST_DIR/fdb-studio_${VERSION}_amd64.deb" +DEB_FILE="$DIST_DIR/quell_${VERSION}_amd64.deb" dpkg-deb --build --root-owner-group "$PACKAGE_ROOT" "$DEB_FILE" echo "Debian package created: $DEB_FILE" diff --git a/packaging/linux/fdb-studio.desktop b/packaging/linux/quell.desktop similarity index 75% rename from packaging/linux/fdb-studio.desktop rename to packaging/linux/quell.desktop index 5691b28..9a29a50 100644 --- a/packaging/linux/fdb-studio.desktop +++ b/packaging/linux/quell.desktop @@ -1,11 +1,11 @@ [Desktop Entry] Type=Application -Name=Fdb Studio +Name=Quell GenericName=Database Management Tool Comment=Modern Cross-Platform Database Client and SQL IDE -Exec=/usr/bin/fdb-studio %U -Icon=fdb-studio +Exec=/usr/bin/quell %U +Icon=quell Terminal=false Categories=Development;Database;IDE; -StartupWMClass=Fdb +StartupWMClass=Quell Keywords=sql;database;sqlite;mssql;postgres;mysql;client; diff --git a/packaging/macos/Info.plist b/packaging/macos/Info.plist index 7bd39c1..6abff2d 100644 --- a/packaging/macos/Info.plist +++ b/packaging/macos/Info.plist @@ -5,13 +5,13 @@ CFBundleDevelopmentRegion en CFBundleExecutable - Fdb + Quell CFBundleIdentifier - com.cipherunits.fdb + com.cipherunits.quell CFBundleInfoDictionaryVersion 6.0 CFBundleName - Fdb Studio + Quell CFBundlePackageType APPL CFBundleShortVersionString diff --git a/packaging/macos/build-app.sh b/packaging/macos/build-app.sh index 9fdc035..d647cf6 100644 --- a/packaging/macos/build-app.sh +++ b/packaging/macos/build-app.sh @@ -17,7 +17,7 @@ echo "Building macOS application bundle and installer for $RID (v$VERSION)..." echo "Source publish dir: $PUBLISH_DIR" echo "Target dist dir: $DIST_DIR" -APP_NAME="Fdb Studio.app" +APP_NAME="Quell.app" STAGING_DIR=$(mktemp -d) trap 'rm -rf "$STAGING_DIR"' EXIT @@ -31,7 +31,9 @@ mkdir -p "$RESOURCES_PATH" # Copy published application files cp -r "$PUBLISH_DIR"/* "$MACOS_PATH/" -if [ -f "$MACOS_PATH/Fdb" ]; then +if [ -f "$MACOS_PATH/Quell" ]; then + chmod +x "$MACOS_PATH/Quell" +elif [ -f "$MACOS_PATH/Fdb" ]; then chmod +x "$MACOS_PATH/Fdb" fi @@ -44,7 +46,7 @@ if [ -d "$ROOT_DIR/Assets" ]; then fi # Create portable zip archive -ZIP_FILE="$DIST_DIR/Fdb-Studio-${VERSION}-${RID}.zip" +ZIP_FILE="$DIST_DIR/Quell-${VERSION}-${RID}.zip" (cd "$STAGING_DIR" && zip -r -y "$ZIP_FILE" "$APP_NAME") echo "Created zip bundle: $ZIP_FILE" @@ -55,9 +57,9 @@ if command -v hdiutil &>/dev/null; then cp -R "$APP_PATH" "$DMG_DIR/" ln -s /Applications "$DMG_DIR/Applications" - DMG_FILE="$DIST_DIR/Fdb-Studio-${VERSION}-${RID}.dmg" + DMG_FILE="$DIST_DIR/Quell-${VERSION}-${RID}.dmg" rm -f "$DMG_FILE" - hdiutil create -volname "Fdb Studio" -srcfolder "$DMG_DIR" -ov -format UDZO "$DMG_FILE" + hdiutil create -volname "Quell" -srcfolder "$DMG_DIR" -ov -format UDZO "$DMG_FILE" echo "Created DMG installer: $DMG_FILE" fi diff --git a/packaging/windows/installer.iss b/packaging/windows/installer.iss index f262bb2..5605ffb 100644 --- a/packaging/windows/installer.iss +++ b/packaging/windows/installer.iss @@ -1,11 +1,11 @@ -; Inno Setup Script for Fdb Studio -; Generates Windows Installer executable (Fdb-Studio-Setup-win-x64.exe) +; Inno Setup Script for Quell +; Generates Windows Installer executable (Quell-Setup-win-x64.exe) -#define MyAppName "Fdb Studio" +#define MyAppName "Quell" #define MyAppVersion "1.0.0" #define MyAppPublisher "CipherUnits" -#define MyAppURL "https://github.com/cipherunits/Fdb" -#define MyAppExeName "Fdb.exe" +#define MyAppURL "https://github.com/cipherunits/Quell" +#define MyAppExeName "Quell.exe" #ifndef SourceDir #define SourceDir "..\..\publish\win-x64" #endif @@ -23,13 +23,13 @@ AppSupportURL={#MyAppURL} AppUpdatesURL={#MyAppURL} DefaultDirName={autopf}\{#MyAppName} DisableProgramGroupPage=yes -OutputBaseFilename=Fdb-Studio-Setup-win-x64 +OutputBaseFilename=Quell-Setup-win-x64 OutputDir={#OutputDir} Compression=lzma2/ultra64 SolidCompression=yes WizardStyle=modern ArchitecturesInstallIn64BitMode=x64compatible -SetupIconFile=..\..\Assets\fdb_icon.ico +SetupIconFile=..\..\Assets\quell_icon.ico UninstallDisplayIcon={app}\{#MyAppExeName} [Languages] diff --git a/quell_icon.png b/quell_icon.png new file mode 100644 index 0000000..d7a65d1 Binary files /dev/null and b/quell_icon.png differ diff --git a/quell_splash.jpg b/quell_splash.jpg new file mode 100644 index 0000000..9267140 Binary files /dev/null and b/quell_splash.jpg differ