Skip to content
Open
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 CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- A plus button in the bottom bar of the Tables sidebar opens a menu to create a new table or view, without right-clicking. It's disabled while safe mode blocks writes.
- The sidebar can show every database on the server as an expandable tree. Switch a connection between the flat list and the tree from the View menu (Sidebar Layout); right-click a database or schema to set it active. Set the default layout for new connections in Settings, General. Applies to MySQL, MariaDB, PostgreSQL, MSSQL, ClickHouse, Redshift; SQLite, Redis, MongoDB, BigQuery keep their existing sidebar. (#139)
- A connection can read its password from a file, environment variable, or command at connect time instead of the Keychain, so scripts can provision a connection without entering the password by hand. (#1254)
- Function-key shortcuts: F5 to refresh, F9 to run a query, F1 to open documentation. F5 and F9 work alongside Cmd+R and Cmd+Return, and all three are assignable in Settings > Keyboard.

### Changed

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@ public enum SSLHandshakeError: Error, LocalizedError, Sendable {
case clientCertRequired(serverMessage: String)
case cipherMismatch(serverMessage: String)
case unknown(serverMessage: String)
case clientKeyPassphraseRequired(serverMessage: String)
case clientKeyPassphraseIncorrect(serverMessage: String)
case clientKeyInvalid(serverMessage: String)

public var serverMessage: String {
switch self {
Expand All @@ -16,6 +19,9 @@ public enum SSLHandshakeError: Error, LocalizedError, Sendable {
.untrustedCertificate(let msg),
.hostnameMismatch(let msg),
.clientCertRequired(let msg),
.clientKeyPassphraseRequired(let msg),
.clientKeyPassphraseIncorrect(let msg),
.clientKeyInvalid(let msg),
.cipherMismatch(let msg),
.unknown(let msg):
return msg
Expand All @@ -34,6 +40,12 @@ public enum SSLHandshakeError: Error, LocalizedError, Sendable {
return String(localized: "The server's TLS certificate does not match the hostname being connected to.")
case .clientCertRequired:
return String(localized: "The server requires a client certificate for TLS mutual authentication.")
case .clientKeyPassphraseRequired:
return String(localized: "The client private key is encrypted and needs a passphrase.")
case .clientKeyPassphraseIncorrect:
return String(localized: "The passphrase for the client private key is incorrect.")
case .clientKeyInvalid:
return String(localized: "The client private key could not be read. It may be malformed or in an unsupported format.")
case .cipherMismatch:
return String(localized: "The server and TablePro could not agree on a TLS cipher or protocol version.")
case .unknown:
Expand Down Expand Up @@ -86,6 +98,12 @@ public enum SSLHandshakeError: Error, LocalizedError, Sendable {
return String(localized: "Switch SSL Mode to Verify CA (validates the CA chain but skips hostname check), or update the host field to match the certificate.")
case .clientCertRequired:
return String(localized: "Provide the client certificate and key paths in the SSL tab.")
case .clientKeyPassphraseRequired:
return String(localized: "Open the connection editor, switch to the SSL tab, and enter the Key Passphrase.")
case .clientKeyPassphraseIncorrect:
return String(localized: "Open the connection editor, switch to the SSL tab, and correct the Key Passphrase.")
case .clientKeyInvalid:
return String(localized: "Check that the Client Key path points to a valid PEM private key.")
case .cipherMismatch:
return String(localized: "Update the server's TLS configuration or use a newer database server version that supports modern ciphers.")
case .unknown:
Expand Down
7 changes: 1 addition & 6 deletions TablePro/AppDelegate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,6 @@ class AppDelegate: NSObject, NSApplicationDelegate {

private var hasRunPostLaunchActivation = false

private static var isUITesting: Bool {
ProcessInfo.processInfo.environment["TABLEPRO_UI_TESTING"] == "1"
}

// MARK: - URL & File Open

func applicationWillFinishLaunching(_ notification: Notification) {
Expand Down Expand Up @@ -71,6 +67,7 @@ class AppDelegate: NSObject, NSApplicationDelegate {
UNUserNotificationCenter.current().delegate = self
PluginNotificationService.shared.setUp()
ChatToolBootstrap.register()
FunctionKeyShortcutMonitor.shared.start()

NSWorkspace.shared.notificationCenter.addObserver(
self, selector: #selector(handleSystemDidWake),
Expand All @@ -97,14 +94,12 @@ class AppDelegate: NSObject, NSApplicationDelegate {

func applicationDidBecomeActive(_ notification: Notification) {
runPostLaunchActivationIfNeeded()
guard !Self.isUITesting else { return }
SyncCoordinator.shared.syncIfNeeded()
}

private func runPostLaunchActivationIfNeeded() {
guard !hasRunPostLaunchActivation else { return }
hasRunPostLaunchActivation = true
guard !Self.isUITesting else { return }

ConnectionStorage.shared.migratePluginSecureFieldsIfNeeded()
AnalyticsService.shared.startPeriodicHeartbeat()
Expand Down
77 changes: 77 additions & 0 deletions TablePro/Core/KeyboardHandling/FunctionKeyShortcutMonitor.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
//
// FunctionKeyShortcutMonitor.swift
// TablePro
//
// Dispatches function-key shortcuts (F1–F12) that can't ride on SwiftUI menu
// key equivalents: secondary bindings whose primary already owns a menu
// shortcut (e.g. F5 alongside ⌘R), and Help actions with no menu shortcut.
//

import AppKit
import Combine
import Foundation

@MainActor
final class FunctionKeyShortcutMonitor {
static let shared = FunctionKeyShortcutMonitor()

private var eventMonitor: Any?

private init() {}

func start() {
guard eventMonitor == nil else { return }
eventMonitor = NSEvent.addLocalMonitorForEvents(matching: .keyDown) { [weak self] event in
guard let self else { return event }
return self.handle(event)
}
}

func stop() {
if let eventMonitor {
NSEvent.removeMonitor(eventMonitor)
}
eventMonitor = nil
}

private func handle(_ event: NSEvent) -> NSEvent? {
guard let action = matchedAction(for: event) else { return event }
if NSApp.keyWindow?.firstResponder is ShortcutRecorderNSView {
return event
}
return perform(action) ? nil : event
}

private func matchedAction(for event: NSEvent) -> ShortcutAction? {
let keyboard = AppSettingsManager.shared.keyboard
for action in ShortcutAction.allCases where action.supportsFunctionKeyPrimary {
if let combo = keyboard.shortcut(for: action), combo.isFunctionKey, combo.matches(event) {
return action
}
}
for action in ShortcutAction.allCases where action.supportsFunctionKeyAlternate {
if let combo = keyboard.alternateShortcut(for: action), combo.isFunctionKey, combo.matches(event) {
return action
}
}
return nil
}

private func perform(_ action: ShortcutAction) -> Bool {
switch action {
case .refresh:
AppCommands.shared.refreshData.send(nil)
return true
case .executeQuery:
guard let actions = CommandActionsRegistry.shared.current else { return false }
actions.runQuery()
return true
case .openDocumentation:
guard let url = URL(string: "https://docs.tablepro.app") else { return false }
NSWorkspace.shared.open(url)
return true
default:
return false
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ struct RefreshToolbarButton: View {
} label: {
Label("Refresh", systemImage: "arrow.clockwise")
}
.help(String(localized: "Refresh (⌘R)"))
.help(String(localized: "Refresh (⌘R / F5)"))
.disabled(state.connectionState != .connected)
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -180,6 +180,7 @@ internal final class MainWindowToolbar: NSObject, NSToolbarDelegate {
let item = NSToolbarItem(itemIdentifier: Self.inspector)
item.label = String(localized: "Inspector")
item.paletteLabel = String(localized: "Inspector")
item.toolTip = String(localized: "Toggle Inspector (⌘⌥I)")
return item
case Self.dashboard:
return hostingItem(
Expand Down
Loading
Loading