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
1 change: 1 addition & 0 deletions .claude/rules/data-sync-storage.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ This path holds user data. A silent wrong answer here looks exactly like an empt
| --- | --- | --- |
| Connection passwords | Keychain | `ConnectionStorage` |
| Preferences | UserDefaults | `AppSettingsStorage`, `AppSettingsManager` |
| Editor and grid fonts | UserDefaults, device-local, never synced | `TypographySettings` through `AppSettingsManager` |
| Query history | SQLite FTS5 | `QueryHistoryStorage` |
| Tab state | JSON | `TabPersistenceCoordinator`, `TabDiskActor` |
| Filter defaults and presets | UserDefaults | `FilterSettingsStorage`, `FilterPresetStorage` |
Expand Down
6 changes: 5 additions & 1 deletion .claude/rules/editor.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,11 @@ paths:

# Editor and theme

- **`ThemeEngine` is the single source of editor colors and fonts**; `ThemeEngine.makeEditorTheme()` builds the `TableProEditorTheme` adapter.
- **`ThemeEngine` is the single source of theme colors**; `ThemeEngine.makeEditorTheme()` builds the `TableProEditorTheme` adapter.
- **A slot exists only with a reader.** `ThemeSlot` is the one registry the gate, the theme editor and the decoder enumerate, and `ThemeSlotCoverageTests` fails a slot nothing reads. A theme file must declare every slot of its schema, as `#hex` or `system:<name>`.
- **Two color tiers.** `palette[slot]` is a dynamic color that answers for the theme in effect; use it wherever a view holds a color. `resolved[slot]` is a snapshot for code that bakes one (`CALayer`, the editor theme, the grid's cell drawing), paired with an `AppEvents.shared.themeChanged` subscription.
- **A theme change has to reach the view.** 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 subscribes to `themeChanged` and redisplays.
- **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]`.**
- **`TableProEditor` is still in Swift 5 language mode** (`.swiftLanguageMode(.v5)`); moving it to Swift 6 is its own change with its own tests.
2 changes: 1 addition & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,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.** `ThemeEngine` owns the active theme, editor colors and fonts. `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`. `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
33 changes: 33 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,39 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Added

- Header, grid line, selection and focus colors in the theme editor.
- Panel and status colors in the theme editor, covering the results, inspector, structure, compare and query plan panes.
- Themes that name a system color for a slot, so the built-in themes keep the system's own contrast settings.
- Reason shown in Settings > Appearance for a theme file that could not be loaded.

### Changed

- Theme file format 2. Themes written for earlier versions are not read and need to be recreated.
- 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.

### Removed

- Interface, Sidebar and Toolbar color groups from the theme editor and the theme file.
- Fonts from the theme file.

### Fixed

- Interface, Sidebar and Toolbar colors having no effect anywhere in the app.
- Line Number color having no effect on the editor gutter.
- Default Light used for the dark theme when the chosen dark theme could not be loaded.
- Editor zoom lost on the next appearance or theme change.
- DDL, trigger and SQL import previews keeping the previous theme's colors.
- Query history preview ignoring the theme and the editor font.
- Theme editor changing the active theme instead of the theme selected for the slot being edited.
- Malformed theme files loading as Default Light under their own name.
- Color with a typo in it rendering as a different color instead of being reported.
- Content panes outside the editor and the data grid ignoring the theme.
- JSON and PHP tree values colored differently from the same values in the row inspector.
- Autocomplete icon colors ignoring the theme.

## [0.80.0] - 2026-10-11

Google Cloud IAM sign-in for MySQL and PostgreSQL on Cloud SQL, with no Cloud SQL Auth Proxy.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -162,10 +162,11 @@ public extension SourceEditorConfiguration {

(controller.textView as? SourceEditorTextView)?.statementHighlightColor = theme.statementHighlight

controller.gutterView.textColor = theme.text.color.withAlphaComponent(0.35)
controller.gutterView.statementRunRibbon.glyphColor = theme.text.color.withAlphaComponent(0.35)
let gutterColor = theme.lineNumber ?? theme.text.color.withAlphaComponent(0.35)
controller.gutterView.textColor = gutterColor
controller.gutterView.statementRunRibbon.glyphColor = gutterColor
controller.gutterView.statementRunRibbon.hoveredGlyphColor = theme.text.color
controller.gutterView.foldingRibbon.chevronColor = theme.text.color.withAlphaComponent(0.35)
controller.gutterView.foldingRibbon.chevronColor = gutterColor
controller.gutterView.foldingRibbon.collapsedChevronColor = theme.text.color.withAlphaComponent(0.65)
controller.gutterView.foldingRibbon.hoveredChevronColor = theme.text.color
controller.gutterView.foldingRibbon.foldExtentColor = theme.text.color.withAlphaComponent(0.2)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,10 @@ public struct EditorTheme: Equatable {
public var statementHighlight: NSColor

public var selection: NSColor

/// The gutter's line numbers. `nil` keeps the derived look: the text colour at 35% alpha.
public var lineNumber: NSColor?

public var keywords: Attribute
public var commands: Attribute
public var types: Attribute
Expand All @@ -56,6 +60,7 @@ public struct EditorTheme: Equatable {
lineHighlight: NSColor,
statementHighlight: NSColor = .clear,
selection: NSColor,
lineNumber: NSColor? = nil,
keywords: Attribute,
commands: Attribute,
types: Attribute,
Expand All @@ -76,6 +81,7 @@ public struct EditorTheme: Equatable {
self.lineHighlight = lineHighlight
self.statementHighlight = statementHighlight
self.selection = selection
self.lineNumber = lineNumber
self.keywords = keywords
self.commands = commands
self.types = types
Expand Down
2 changes: 1 addition & 1 deletion TablePro/AppDelegate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ class AppDelegate: NSObject, NSApplicationDelegate {
/// Only a screenshot run overrides the mode, so only a screenshot run resolves it twice.
if let screenshotMode = ScreenshotEnvironment.appearanceMode {
let appearanceSettings = AppSettingsManager.shared.appearance
ThemeEngine.shared.updateAppearanceAndTheme(
ThemeEngine.shared.apply(
mode: screenshotMode,
lightThemeId: appearanceSettings.preferredLightThemeId,
darkThemeId: appearanceSettings.preferredDarkThemeId
Expand Down
17 changes: 9 additions & 8 deletions TablePro/Core/Autocomplete/SQLCompletionItem.swift
Original file line number Diff line number Diff line change
Expand Up @@ -36,16 +36,17 @@ enum SQLCompletionKind: String, CaseIterable {
}

/// Color for the icon
@MainActor
var iconColor: NSColor {
switch self {
case .keyword: return .systemBlue
case .table: return .systemTeal
case .view: return .systemPurple
case .column: return .systemOrange
case .function: return .systemPink
case .schema: return .systemGreen
case .alias: return .systemGray
case .operator: return .systemIndigo
case .keyword: return ThemeEngine.shared.palette[.syntaxKeyword]
case .table: return ThemeEngine.shared.palette[.syntaxType]
case .view: return ThemeEngine.shared.palette[.syntaxType]
case .column: return ThemeEngine.shared.palette[.syntaxNull]
case .function: return ThemeEngine.shared.palette[.syntaxFunction]
case .schema: return ThemeEngine.shared.palette[.syntaxType]
case .alias: return ThemeEngine.shared.palette[.syntaxNull]
case .operator: return ThemeEngine.shared.palette[.syntaxOperator]
case .favorite: return .systemYellow
}
}
Expand Down
6 changes: 5 additions & 1 deletion TablePro/Core/Events/AppEvents.swift
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,11 @@ final class AppEvents {

// MARK: - Theme & Accessibility

let themeChanged = PassthroughSubject<Void, Never>()
/// Carries the palette revision and the effective appearance. A consumer that snapshots
/// colours or fonts needs both: a forced Light/Dark switch never changes the theme, and a
/// theme switch never changes the appearance. Subscribing reaches a view that is alive but
/// detached from every window, which a walk of the window tree would miss.
let themeChanged = PassthroughSubject<ThemeChange, Never>()

let accessibilityTextSizeChanged = PassthroughSubject<Void, Never>()

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -144,10 +144,18 @@ extension MainSplitViewController {
/// these first, so this is what Zoom In means everywhere else.
extension MainSplitViewController: ZoomCommandResponding {
@objc func zoomIn(_ sender: Any?) {
ThemeEngine.shared.adjustEditorFontSize(by: 1)
adjustEditorTextSize(by: 1)
}

@objc func zoomOut(_ sender: Any?) {
ThemeEngine.shared.adjustEditorFontSize(by: -1)
adjustEditorTextSize(by: -1)
}

private func adjustEditorTextSize(by delta: Int) {
var typography = AppSettingsManager.shared.typography
let size = TypographySettings.clamp(typography.editorFontSize + delta)
guard size != typography.editorFontSize else { return }
typography.editorFontSize = size
AppSettingsManager.shared.typography = typography
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -830,8 +830,8 @@ internal final class MainSplitViewController: NSSplitViewController {
/// intermediate value instead of letting one run-loop turn settle on the final one.
private func refreshPanes(of workspace: ConnectionWorkspace) {
workspace.panes.sidebar.rootView = AnyView(buildSidebarView(for: workspace))
workspace.panes.detail.rootView = AnyView(buildDetailView(for: workspace))
workspace.panes.inspector.rootView = AnyView(buildInspectorView(for: workspace))
workspace.panes.detail.rootView = AnyView(buildDetailView(for: workspace).themedContent())
workspace.panes.inspector.rootView = AnyView(buildInspectorView(for: workspace).themedContent())
workspace.panes.assistant.rootView = AnyView(buildAssistantView(for: workspace))
refreshAgentPanes(of: workspace)
refreshTabStripPane(of: workspace)
Expand Down Expand Up @@ -1055,7 +1055,7 @@ internal final class MainSplitViewController: NSSplitViewController {
/// publishes those actions.
func rebuildTrailingPanes() {
guard let selected = workspaces.selected else { return }
selected.panes.inspector.rootView = AnyView(buildInspectorView(for: selected))
selected.panes.inspector.rootView = AnyView(buildInspectorView(for: selected).themedContent())
selected.panes.assistant.rootView = AnyView(buildAssistantView(for: selected))
}

Expand Down
33 changes: 15 additions & 18 deletions TablePro/Core/Storage/AppSettingsManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ final class AppSettingsManager: ObservableObject {
@Published var appearance: AppearanceSettings {
didSet {
storage.saveAppearance(appearance)
themeEngine.updateAppearanceAndTheme(
themeEngine.apply(
mode: appearance.appearanceMode,
lightThemeId: appearance.preferredLightThemeId,
darkThemeId: appearance.preferredDarkThemeId
Expand All @@ -37,19 +37,21 @@ final class AppSettingsManager: ObservableObject {
@Published var editor: EditorSettings {
didSet {
storage.saveEditor(editor)
themeEngine.updateEditorSettings(
highlightCurrentLine: editor.highlightCurrentLine,
highlightCurrentStatement: editor.highlightCurrentStatement,
showLineNumbers: editor.showLineNumbers,
tabWidth: editor.clampedTabWidth,

wordWrap: editor.wordWrap
)
appEvents.editorSettingsChanged.send(())
syncTracker.markDirty(.settings, id: AppSettingsCategory.editor)
}
}

/// Device-local: it is deliberately absent from `AppSettingsCategory.synced`, so a zoom press
/// never pushes an iCloud record and a Mac on an older build cannot reset it.
@Published var typography: TypographySettings {
didSet {
guard typography != oldValue else { return }
storage.saveTypography(typography)
themeEngine.apply(typography: typography)
}
}

@Published var notifications: NotificationSettings {
didSet {
guard !isValidating else { return }
Expand Down Expand Up @@ -245,23 +247,17 @@ final class AppSettingsManager: ObservableObject {
self.sync = storage.loadSync()
self.mcp = storage.loadMCP()
self.notifications = storage.loadNotifications()
self.typography = storage.loadTypography()

general.language.apply()

themeEngine.updateAppearanceAndTheme(
themeEngine.apply(typography: typography)
themeEngine.apply(
mode: appearance.appearanceMode,
lightThemeId: appearance.preferredLightThemeId,
darkThemeId: appearance.preferredDarkThemeId
)

themeEngine.updateEditorSettings(
highlightCurrentLine: editor.highlightCurrentLine,
highlightCurrentStatement: editor.highlightCurrentStatement,
showLineNumbers: editor.showLineNumbers,
tabWidth: editor.clampedTabWidth,
wordWrap: editor.wordWrap
)

dateFormattingService.updateFormat(dataGrid.dateFormat)

if ai.enabled, ai.providers.contains(where: { $0.type == .copilot }) {
Expand Down Expand Up @@ -302,6 +298,7 @@ final class AppSettingsManager: ObservableObject {
ai = .default
sync = .default
mcp = .default
typography = .default
storage.resetToDefaults()
connectionListPreferences.resetShowsRecent()
SoftwareUpdater.shared.resetUpdatePreferences()
Expand Down
25 changes: 25 additions & 0 deletions TablePro/Core/Storage/AppSettingsStorage.swift
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ final class AppSettingsStorage: Sendable {
static let sync = "com.TablePro.settings.sync"
static let mcp = "com.TablePro.settings.mcp"
static let notifications = "com.TablePro.settings.notifications"
static let typography = "com.TablePro.settings.typography"
static let hasSeenWelcomeSheet = "com.TablePro.settings.hasCompletedOnboarding"
static let startupReopenMigration = "com.TablePro.settings.didMigrateStartupToReopenLast"
static let jsonFieldHeightMigration = "com.TablePro.settings.didMigrateJsonFieldHeightKey"
Expand Down Expand Up @@ -85,6 +86,29 @@ final class AppSettingsStorage: Sendable {
load(key: Keys.appearance, default: .default)
}

/// Fonts moved out of the theme file, and the theme file format they lived in is no longer
/// read. Without this every user who had picked a font or zoomed the editor would silently be
/// returned to System Mono 13 on upgrade, so their sizes are lifted once from whichever theme
/// their slot pointed at, and the theme file itself is left untouched.
func loadTypography() -> TypographySettings {
if let stored = defaults.data(forKey: Keys.typography) {
do {
return try decoder.decode(TypographySettings.self, from: stored)
} catch {
Self.logger.error("Failed to decode typography settings: \(error)")
return .default
}
}

let carried = LegacyThemeFonts.read(preferring: loadAppearance())
saveTypography(carried)
return carried
}

func saveTypography(_ settings: TypographySettings) {
save(settings, key: Keys.typography)
}

func saveAppearance(_ settings: AppearanceSettings) {
save(settings, key: Keys.appearance)
}
Expand Down Expand Up @@ -230,6 +254,7 @@ final class AppSettingsStorage: Sendable {
saveAI(.default)
saveSync(.default)
saveMCP(.default)
saveTypography(.default)
defaults.removeObject(forKey: PreferenceKeys.selectedSettingsPane.name)
defaults.removeObject(forKey: PreferenceKeys.rowInspectorJsonFieldHeight.name)
defaults.removeObject(forKey: PreferenceKeys.rowInspectorTextFieldHeight.name)
Expand Down
2 changes: 1 addition & 1 deletion TablePro/Core/Vim/VimCursorManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -154,7 +154,7 @@ final class VimCursorManager {
} else {
let layer = CALayer()
layer.contentsScale = textView.window?.backingScaleFactor ?? 2.0
layer.backgroundColor = ThemeEngine.shared.colors.editor.cursor.withAlphaComponent(0.4).cgColor
layer.backgroundColor = ThemeEngine.shared.resolved[.editorCursor].withAlphaComponent(0.4).cgColor
layer.frame = frame

if !isPaused {
Expand Down
2 changes: 1 addition & 1 deletion TablePro/Core/Vim/VimTextBufferAdapter.swift
Original file line number Diff line number Diff line change
Expand Up @@ -414,7 +414,7 @@ final class VimTextBufferAdapter: VimTextBuffer {
}

func indentWidth() -> Int {
ThemeEngine.shared.tabWidth
AppSettingsManager.shared.editor.clampedTabWidth
}

// MARK: - Helpers
Expand Down
Loading
Loading