Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 2 additions & 6 deletions .claude/rules/editor.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,9 @@ paths:
- "TablePro/Views/RowInspector/**/*"
---

# Editor and theme
# Editor

- **`ThemeEngine` is the single source of theme colors**; `ThemeEngine.makeEditorTheme()` builds the `TableProEditorTheme` adapter.
- **A theme colors two surfaces: code views and the data grid.** Code views are the SQL editor and every other `SourceEditor` or SQL text view (JSON editor, DDL, trigger, previews, AI code blocks, history). Every other pane, Settings and all chrome use semantic system colors directly and never read the theme. `NSApp.appearance` is the only lever on chrome.
- **A slot exists only with a reader.** `ThemeSlot` is the one registry the decoder, encoder and theme editor enumerate, and `ThemeSlotCoverageTests` fails a slot nothing reads. Every slot is optional: a theme that leaves one out draws `ThemeSlot.nativeColor`, the color AppKit itself uses there. Default Light and Default Dark override syntax only, with Xcode's Default values.
- **Two color tiers.** `palette[slot]` is a dynamic color over the live theme pair; use it wherever a view holds a color. `resolved[slot]` is the active theme's override or the system color object itself, never a flattened copy; turn it into a `CGColor` only inside the drawing view's appearance (`resolved.cgColor(_:in:)`), and check `resolved.isOverridden(_:)` before replacing a native drawing path (row selection, caret, header). Never call `withAlphaComponent` on a system color outside an `NSColor(name:dynamicProvider:)`: it freezes the color.
- **A theme change has to reach the view.** `AppEvents.shared.themeChanged` fires on a theme change, a light/dark flip, an Accent or Highlight change and an Increase Contrast toggle. A SwiftUI view that reads the engine observes it (`@ObservedObject private var themeEngine = ThemeEngine.shared`), because SwiftUI caches `Color(nsColor:)` by object identity; an AppKit view that bakes a color subscribes to `themeChanged` and re-bakes.
- **`ThemeEngine.makeEditorTheme()` builds the `TableProEditorTheme` adapter** the editor draws with. Theme colors and their scope: `.claude/rules/theme.md`.
- **Fonts are not theme.** `TypographySettings` is device-local and stays out of `AppSettingsCategory.synced`: sync replaces a category wholesale, so an older Mac would reset it and each zoom press would push a record.
- **Two font domains.** A view that also wears the editor's colors (the SQL editor, `JSONCodeEditor`, previews) uses `editorFonts`. Every control that shows or edits a stored value uses `ThemeEngine.valueFont` or `valueFontSwiftUI` (the Data Grid Font), so a value reads the same in the grid, the inspector and a popover; a system text style there looks right only while the two settings are equal. `InspectorFieldRow` sets it for the whole inspector, and its `FieldEditorKind` switch is exhaustive on purpose.
- **Multi-cursor lives in `cursorPositions: [CursorPosition]`.**
Expand Down
22 changes: 22 additions & 0 deletions .claude/rules/theme.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
---
paths:
- "TablePro/Theme/**/*"
- "TablePro/Views/**/*"
- "TablePro/Core/Services/Infrastructure/*Window*"
---

# Theme

- **A theme colors content and the window backdrop.** Content is every code view, the data grid, value text and wells, value trees, diffs, record tables, diagrams, charts and the result-area states. `window.background` fills the detail pane and, on macOS 26 and later, `NSWindow.backgroundColor` of the four content windows (main, Data File, Compare & Sync, value viewers); the sidebar, inspector and toolbar glass take their tone from it. Controls, Accent, Highlight, Settings, Welcome, the connection form, sheet and popover chrome, SwiftUI `List`, `Form` and `Table` backgrounds and every bar stay system; the result bar only drops its material while `window.background` themes the detail pane. `NSApp.appearance` is the only lever on chrome.
- **A code field takes the editor theme wherever it appears.** Startup Commands, Pre-Connect Script, a favorite's query, Rewind and Agent SQL wear `editor.*`; the form or sheet around them stays system.
- **`ThemeEngine` is the single source of theme colors, and a slot exists only with a reader.** `ThemeSlot` is the one registry the decoder, encoder and theme editor enumerate, and `ThemeSlotCoverageTests` fails a slot nothing reads. Every slot is optional: one a theme leaves out draws `ThemeSlot.nativeColor`. Default Light and Default Dark override syntax only, with Xcode's Default values.
- **Every site passes what it draws natively.** A themed call site names a slot and, as its fallback, the exact system color or style it drew before, so while the active theme leaves the slot out the pixels are an unthemed app's. A default changes only on purpose, named in the PR and the CHANGELOG.
- **The slot family follows what the surface shows.** Code, values, value trees and wells: `editor.*`, with tree tones from `editor.syntax.*`. Records, schema objects, diagram nodes, chart plots and states that stand in for a grid: `dataGrid.*`. Diff rows: `dataGrid.inserted`, `modified` and `deleted` over the editor background and text. A region behind panes: `window.background`. Identity, severity and status hues stay system.
- **Two color tiers.** `palette[slot]` and `palette.color(_:otherwise:)` are dynamic colors over the live theme pair; use them wherever a view holds a color. `palette.color` returns a new object on every call, so never compare it to skip a write. `resolved[slot]` is the active theme's override or the system color object itself, never a flattened copy; bake it into a `CGColor` only inside the drawing view's appearance (`resolved.cgColor(_:otherwise:in:)`), and check `resolved.isOverridden(_:)` before replacing a native drawing path (row selection, caret, header).
- **The carriers.** SwiftUI: `.themeBackground(_:native:)`, `.themeForeground(_:native:)`, `CodeWell`, `.valueWell(clipsContent:)`, `TextValueEditor(style: .code)`. AppKit: `ThemeTextViewStyle` on an `NSTextView`; `ThemeTableChrome` (`.records` or `.code`) with `ThemeTableRowView` rows on a content `NSTableView` or `NSOutlineView`, never a source list; `ThemeWindowBackdrop` on a content window.
- **The file list is closed.** `ThemeColorReadGuardTests` names every file that may read the theme, use a carrier, or call a helper that reads it (`TreeValueTone.color`, `HighlightedSQLTextView`, `TableProEditorTheme.make()`). A new content surface joins it in the same change; chrome never does.
- **Hold every AppKit carrier strongly**, as a stored property on its owner. Its subscription and its row views keep weak references, so a carrier held only in a local stops at the first theme change.
- **A theme change has to reach the view.** `AppEvents.shared.themeChanged` fires on a theme change, a light/dark flip, an Accent or Highlight change and an Increase Contrast toggle. A SwiftUI view that reads the engine observes it (`@ObservedObject private var themeEngine = ThemeEngine.shared`), because SwiftUI caches `Color(nsColor:)` by object identity; an AppKit view that bakes a color re-bakes on it. Never call `withAlphaComponent` on a system color outside an `NSColor(name:dynamicProvider:)`: it freezes the color.
- **No fixed color on selectable-row text.** A theme color does not turn white on the emphasized selection the way `.primary` does, so `.themeForeground` never goes on a container of selectable rows. Row text keeps a hierarchical style unless the theme also sets that table's selection.
- **The window backdrop yields to Increase Contrast** and draws native there, since the format has no increased-contrast variant. Content slots keep the theme.
- **Never fill chrome.** No fill on the sidebar, inspector frame, toolbar or titlebar, and no `.tint` with a theme color. AppKit keeps its own selection unless the theme sets the selection slot. `PaneBackgroundGuardTests` bans `.scrollContentBackground(.hidden)`, `.listRowBackground(` and a window `backgroundColor` write outside `ThemeWindowBackdrop`.
5 changes: 3 additions & 2 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ swiftlint lint --strict <files>

**Change tracking.** A cell edit goes to `DataChangeManager`; Save turns it into statements through `SQLStatementGenerator`; `AnyChangeManager` abstracts the concrete managers. Undo comes from `StructureChangeManager`'s private `UndoManager` and `ConnectionWorkspace.undoManager`.

**Editor and theme.** `ThemeCatalog` owns the theme list, `ThemeResolver` picks the light and dark pair, and `ThemeEngine` holds the palette and the font caches; the fonts themselves are `TypographySettings`. `CompletionEngine` is framework-agnostic and `QueryCompletionAdapter` bridges it to the editor. Editor tabs are drawn by `EditorTabStrip`, not native window tabs. Details: `.claude/rules/editor.md`.
**Editor and theme.** `ThemeCatalog` owns the theme list, `ThemeResolver` picks the light and dark pair, and `ThemeEngine` holds the palette and the font caches; the fonts themselves are `TypographySettings`. A theme colors content and the window backdrop, never chrome or controls; see `.claude/rules/theme.md`. `CompletionEngine` is framework-agnostic and `QueryCompletionAdapter` bridges it to the editor. Editor tabs are drawn by `EditorTabStrip`, not native window tabs. Details: `.claude/rules/editor.md`.

**Storage.** Passwords in the Keychain, preferences in UserDefaults, query history in SQLite FTS5, tab state as JSON. The full table of which store owns what is in `.claude/rules/data-sync-storage.md`.

Expand Down Expand Up @@ -137,13 +137,14 @@ For everything: docs, commits, CHANGELOG, UI strings, errors, PR descriptions. S
| `connection-window.md` | connection windows, workspaces, sessions, `DatabaseManager` |
| `data-grid.md` | the data grid under `TablePro/Views/Results` |
| `data-sync-storage.md` | storage, CloudKit sync, the database core |
| `editor.md` | `Packages/TableProEditor`, `TablePro/Theme`, editor views |
| `editor.md` | `Packages/TableProEditor`, editor views, fonts |
| `libs.md` | `Libs/` and the libs scripts |
| `mongodb-driver.md`, `mysql-driver.md`, `redis-driver.md` | those drivers |
| `native-bridges.md` | `Native/` and the Dameng and HANA plugins |
| `plugin-system.md` | `Plugins/`, TableProPluginKit, `project.yml`, plugin CI |
| `split-views.md` | split view controllers and workspace panes |
| `tests.md` | `TableProTests`, `TableProUITests` |
| `theme.md` | `TablePro/Theme`, every view a theme reaches, content windows |
| `ui-lifecycle.md` | views, view models, UI tests |
| `welcome-library.md` | the welcome window's connection list |

Expand Down
9 changes: 8 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,15 +12,20 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Header, grid line, selection and focus colors in the theme editor.
- Light theme and Dark theme pickers, and New Theme Based On, Rename and Show in Finder for custom themes in Settings > Appearance.
- Reason shown in Settings > Appearance for a theme file that could not be loaded.
- Window background color in themes. On macOS 26 and later the sidebar, inspector and toolbar take it on.
- Setting in Settings > General to hide the version and Check for Updates on the Welcome window.
- Install Parquet… in the Export dialog's format menu when the Parquet plugin is not installed.
- Cloud Firestore and Firebase Realtime Database as a registry plugin over their REST APIs. (#2504)

### Changed

- Default Light and Default Dark draw the editor and data grid in the system colors and use Xcode's default syntax colors.
- Themes color only the SQL editor, other code views and the data grid. Every other pane keeps the system colors.
- Themes color every content view, including trees, diffs, diagrams and charts. Controls and Settings keep the system look.
- JSON and PHP tree values use the editor's syntax colors, matching the same value shown as text.
- Theme file format 3, where every color is optional. Themes written for earlier versions are not read and need to be recreated.
- Triggers and ClickHouse Parts in table structure shown as data grids, with multiple selection and Copy.
- Filter and maintenance SQL previews highlighted as SQL in the editor font.
- Result bar items separated by space instead of a dot.
- Settings > Editor grouped into Display, Gutter and Editing.
- Editor Font and Data Grid Font moved to Settings > Editor and Settings > Data, and kept per Mac.
- Editor font size range of 10 to 24 points everywhere, including zoom.
Expand Down Expand Up @@ -69,6 +74,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Zoom In and Zoom Out staying enabled at the largest and smallest font size.
- JSON and PHP tree values colored differently from the same values in the row inspector.
- Theme color wells and the theme preview unreadable to VoiceOver.
- Query plan image export coming out light in Dark mode.
- Favorite query field turning straight quotes into curly quotes.
- Reopen Last Session forgetting its connections after an MCP client launched TablePro in the background.
- GitHub Copilot starting at launch when not in use, and running on after AI was turned off or Copilot was removed.
- Settings > AI reading sign-in tokens and running command line tools for providers that were never added.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,8 @@ internal final class TabWindowController: NSWindowController, NSWindowDelegate {

private var activity: NSUserActivity?

private let windowBackdrop: ThemeWindowBackdrop

/// `adopting` carries a connection that is moving here from another window, whole. Everything
/// the user has in it lives on that object, so the window takes it rather than building a
/// second one around the same connection.
Expand Down Expand Up @@ -123,6 +125,7 @@ internal final class TabWindowController: NSWindowController, NSWindowDelegate {
window.subtitle = splitVC.windowSubtitle
window.representedURL = splitVC.windowRepresentedURL
splitVC.installTabStripAccessory(on: window)
windowBackdrop = ThemeWindowBackdrop(window: window)

super.init(window: window)

Expand Down
7 changes: 2 additions & 5 deletions TablePro/Models/Query/ExecutionSlot.swift
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,9 @@ internal struct ExecutionSlot: Equatable {
}

internal let report: Report?
internal let leadsWithSeparator: Bool

internal init(isOffered: Bool, followsReadout: Bool, isRevealed: Bool, lastTiming: PluginQueryTiming?) {
let report = isOffered ? Self.report(isRevealed: isRevealed, lastTiming: lastTiming) : nil
self.report = report
leadsWithSeparator = followsReadout && report != nil
internal init(isOffered: Bool, isRevealed: Bool, lastTiming: PluginQueryTiming?) {
report = isOffered ? Self.report(isRevealed: isRevealed, lastTiming: lastTiming) : nil
}

private static func report(isRevealed: Bool, lastTiming: PluginQueryTiming?) -> Report? {
Expand Down
16 changes: 0 additions & 16 deletions TablePro/Models/UI/FilterableTreeNode.swift
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
// TablePro
//

import AppKit
import Foundation

internal protocol FilterableTreeNode: Identifiable {
Expand Down Expand Up @@ -169,9 +168,6 @@ internal struct TreeSiblingCounter {

// MARK: - Row Content

/// Shared by the JSON and PHP tree viewers, the results tree and the JSON row inspector. These are
/// system colors on purpose: a theme colors only the SQL editor and the data grid, so a value reads
/// the same here under every theme.
internal enum TreeValueTone: Equatable, Sendable {
case key
case container
Expand All @@ -181,18 +177,6 @@ internal enum TreeValueTone: Equatable, Sendable {
case serialized
case reference
case muted

var color: NSColor {
switch self {
case .key, .container: return .systemBlue
case .string: return .systemRed
case .number: return .systemPurple
case .literal: return .systemOrange
case .serialized: return .systemTeal
case .reference: return .systemGray
case .muted: return .secondaryLabelColor
}
}
}

internal struct TreeRowContent: Equatable, Sendable {
Expand Down
12 changes: 1 addition & 11 deletions TablePro/Resources/Localizable.xcstrings
Original file line number Diff line number Diff line change
Expand Up @@ -171521,16 +171521,6 @@
},
"Sum %@" : {

},
"Sum %@ · Average %@ · Count %lld" : {
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "new",
"value" : "Sum %1$@ · Average %2$@ · Count %3$lld"
}
}
}
},
"Sum %@, Average %@, Count %lld" : {
"localizations" : {
Expand Down Expand Up @@ -189087,7 +189077,7 @@
}
}
},
"Themes color the SQL editor and the data grid. Everything else follows the system appearance." : {
"Themes color the editor, the data grid, other content views and the window background. Controls, sidebars and Settings follow the system appearance." : {

},
"There are no connections to publish." : {
Expand Down
5 changes: 4 additions & 1 deletion TablePro/Resources/Themes/tablepro.dracula.json
Original file line number Diff line number Diff line change
Expand Up @@ -43,5 +43,8 @@
},
"id": "tablepro.dracula",
"name": "Dracula",
"schema": 3
"schema": 3,
"window": {
"background": "#282A36"
}
}
5 changes: 4 additions & 1 deletion TablePro/Resources/Themes/tablepro.nord.json
Original file line number Diff line number Diff line change
Expand Up @@ -43,5 +43,8 @@
},
"id": "tablepro.nord",
"name": "Nord",
"schema": 3
"schema": 3,
"window": {
"background": "#2E3440"
}
}
2 changes: 2 additions & 0 deletions TablePro/Theme/ThemeColor.swift
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,8 @@ internal extension ThemeSlot {
/// `withAlphaComponent` on a system color outside one freezes it at the current appearance.
var nativeColor: NSColor {
switch self {
case .windowBackdrop: return .windowBackgroundColor

case .editorBackground: return .textBackgroundColor
case .editorText, .syntaxOperator: return .textColor
case .editorCursor: return NativeThemeColors.insertionPoint
Expand Down
Loading
Loading