diff --git a/.idea/vcs.xml b/.idea/vcs.xml
new file mode 100644
index 0000000..94a25f7
--- /dev/null
+++ b/.idea/vcs.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/WorkbenchReplacement/App.swift b/WorkbenchReplacement/App.swift
index cf5480f..3b08ab8 100644
--- a/WorkbenchReplacement/App.swift
+++ b/WorkbenchReplacement/App.swift
@@ -13,5 +13,287 @@ struct WorkbenchReplacementApp: App {
.commands {
CommandGroup(replacing: .newItem) {}
}
+
+
+ Settings {
+ WorkbenchSettingsView()
+ }
+}
+}
+
+
+// MARK: - Application Preferences
+
+@MainActor
+final class WorkbenchPreferences: ObservableObject {
+ static let shared = WorkbenchPreferences()
+
+ @AppStorage("preferences.editorFontSize")
+ var editorFontSize = 13.0
+
+ @AppStorage("preferences.uppercaseKeywords")
+ var uppercaseKeywords = true
+
+ @AppStorage("preferences.showLineNumbers")
+ var showLineNumbers = true
+
+ @AppStorage("preferences.tabWidth")
+ var tabWidth = 4
+
+ @AppStorage("preferences.resultRowHeight")
+ var resultRowHeight = 24.0
+
+ @AppStorage("preferences.resultColumnWidth")
+ var resultColumnWidth = 170.0
+
+ @AppStorage("preferences.alternatingRows")
+ var alternatingRows = true
+
+ @AppStorage("preferences.appearance")
+ var appearance = "light"
+
+ @AppStorage("preferences.autoExpandTables")
+ var autoExpandTables = true
+}
+
+
+// MARK: - Workbench Settings
+
+struct WorkbenchSettingsView: View {
+
+ @AppStorage("preferences.editorFontSize")
+ private var editorFontSize = 13.0
+
+ @AppStorage("preferences.uppercaseKeywords")
+ private var uppercaseKeywords = true
+
+ @AppStorage("preferences.showLineNumbers")
+ private var showLineNumbers = true
+
+ @AppStorage("preferences.tabWidth")
+ private var tabWidth = 4
+
+ @AppStorage("preferences.defaultRowLimit")
+ private var defaultRowLimit = 500
+
+ @AppStorage("preferences.resultRowHeight")
+ private var resultRowHeight = 24.0
+
+ @AppStorage("preferences.resultColumnWidth")
+ private var resultColumnWidth = 170.0
+
+ @AppStorage("preferences.alternatingRows")
+ private var alternatingRows = true
+
+ @AppStorage("preferences.autoExpandTables")
+ private var autoExpandTables = true
+
+ var body: some View {
+ TabView {
+
+ editorSettings
+ .tabItem {
+ Label(
+ "SQL Editor",
+ systemImage: "chevron.left.forwardslash.chevron.right"
+ )
+ }
+
+ executionSettings
+ .tabItem {
+ Label(
+ "Query Execution",
+ systemImage: "bolt"
+ )
+ }
+
+ resultSettings
+ .tabItem {
+ Label(
+ "Result Grid",
+ systemImage: "tablecells"
+ )
+ }
+
+ generalSettings
+ .tabItem {
+ Label(
+ "General",
+ systemImage: "gearshape"
+ )
+ }
+ }
+ .frame(width: 620, height: 390)
+ .preferredColorScheme(.light)
+ }
+
+ private var editorSettings: some View {
+ settingsPage("SQL Editor") {
+
+ settingsRow("Font Size") {
+ Stepper(
+ "\(Int(editorFontSize)) pt",
+ value: $editorFontSize,
+ in: 10...24,
+ step: 1
+ )
+ .frame(width: 130)
+ }
+
+ settingsRow("Tab Width") {
+ Picker("", selection: $tabWidth) {
+ Text("2 spaces").tag(2)
+ Text("4 spaces").tag(4)
+ Text("8 spaces").tag(8)
+ }
+ .labelsHidden()
+ .frame(width: 150)
+ }
+
+ settingsRow("SQL Formatting") {
+ Toggle(
+ "Automatically uppercase SQL keywords",
+ isOn: $uppercaseKeywords
+ )
+ }
+
+ settingsRow("Editor Display") {
+ Toggle(
+ "Show line numbers",
+ isOn: $showLineNumbers
+ )
+ }
+ }
+ }
+
+ private var executionSettings: some View {
+ settingsPage("Query Execution") {
+
+ settingsRow("Default Row Limit") {
+ HStack(spacing: 12) {
+ TextField(
+ "",
+ value: $defaultRowLimit,
+ format: .number
+ )
+ .textFieldStyle(.roundedBorder)
+ .frame(width: 110)
+
+ Stepper(
+ "",
+ value: $defaultRowLimit,
+ in: 50...100000,
+ step: 50
+ )
+ .labelsHidden()
+ }
+ }
+
+ Text(
+ "Controls the number of rows retrieved when opening a table from the schema navigator."
+ )
+ .font(.system(size: 12))
+ .foregroundStyle(.secondary)
+ .frame(maxWidth: .infinity, alignment: .leading)
+ .padding(.leading, 190)
+
+ Divider()
+ .padding(.vertical, 12)
+
+ Text("Examples")
+ .font(.headline)
+
+ Text(
+ "500 rows: Faster browsing and lower memory usage.\n" +
+ "5,000 rows: Useful for larger datasets.\n" +
+ "50,000 rows: May take longer to retrieve."
+ )
+ .font(.system(size: 12))
+ .foregroundStyle(.secondary)
+ .frame(maxWidth: .infinity, alignment: .leading)
+ }
+ }
+
+ private var resultSettings: some View {
+ settingsPage("Result Grid") {
+
+ settingsRow("Row Height") {
+ Stepper(
+ "\(Int(resultRowHeight)) px",
+ value: $resultRowHeight,
+ in: 18...40,
+ step: 1
+ )
+ .frame(width: 130)
+ }
+
+ settingsRow("Column Width") {
+ Stepper(
+ "\(Int(resultColumnWidth)) px",
+ value: $resultColumnWidth,
+ in: 80...400,
+ step: 10
+ )
+ .frame(width: 140)
+ }
+
+ settingsRow("Appearance") {
+ Toggle(
+ "Alternating row colours",
+ isOn: $alternatingRows
+ )
+ }
+ }
+ }
+
+ private var generalSettings: some View {
+ settingsPage("General") {
+
+ settingsRow("Navigator") {
+ Toggle(
+ "Automatically expand table folders",
+ isOn: $autoExpandTables
+ )
+ }
+ }
+ }
+
+ private func settingsPage(
+ _ title: String,
+ @ViewBuilder content: () -> Content
+ ) -> some View {
+
+ VStack(alignment: .leading, spacing: 18) {
+
+ Text(title)
+ .font(.system(size: 20, weight: .semibold))
+
+ Divider()
+
+ content()
+
+ Spacer(minLength: 0)
+ }
+ .padding(26)
+ .frame(maxWidth: .infinity, maxHeight: .infinity)
+ .background(Color(nsColor: .windowBackgroundColor))
+ }
+
+ private func settingsRow(
+ _ title: String,
+ @ViewBuilder content: () -> Content
+ ) -> some View {
+
+ HStack(alignment: .center, spacing: 16) {
+
+ Text(title)
+ .font(.system(size: 13))
+ .frame(width: 170, alignment: .trailing)
+
+ content()
+
+ Spacer(minLength: 0)
+ }
+ .frame(height: 30)
}
}
diff --git a/WorkbenchReplacement/App.swift.backup-20261016-172158 b/WorkbenchReplacement/App.swift.backup-20261016-172158
new file mode 100644
index 0000000..cf5480f
--- /dev/null
+++ b/WorkbenchReplacement/App.swift.backup-20261016-172158
@@ -0,0 +1,17 @@
+import SwiftUI
+
+@main
+struct WorkbenchReplacementApp: App {
+ @StateObject private var model = DatabaseModel()
+
+ var body: some Scene {
+ WindowGroup("Workbench Replacement") {
+ MainView()
+ .environmentObject(model)
+ .frame(minWidth: 1000, minHeight: 650)
+ }
+ .commands {
+ CommandGroup(replacing: .newItem) {}
+ }
+ }
+}
diff --git a/WorkbenchReplacement/App.swift.backup-20261016-172337 b/WorkbenchReplacement/App.swift.backup-20261016-172337
new file mode 100644
index 0000000..bf1f010
--- /dev/null
+++ b/WorkbenchReplacement/App.swift.backup-20261016-172337
@@ -0,0 +1,177 @@
+import SwiftUI
+
+@main
+struct WorkbenchReplacementApp: App {
+ @StateObject private var model = DatabaseModel()
+
+ var body: some Scene {
+ WindowGroup("Workbench Replacement") {
+ MainView()
+ .environmentObject(model)
+ .frame(minWidth: 1000, minHeight: 650)
+ }
+ .commands {
+ CommandGroup(replacing: .newItem) {}
+ }
+
+
+ Settings {
+ WorkbenchSettingsView()
+ }
+}
+}
+
+
+// MARK: - Application Preferences
+
+@MainActor
+final class WorkbenchPreferences: ObservableObject {
+ static let shared = WorkbenchPreferences()
+
+ @AppStorage("preferences.editorFontSize")
+ var editorFontSize = 13.0
+
+ @AppStorage("preferences.uppercaseKeywords")
+ var uppercaseKeywords = true
+
+ @AppStorage("preferences.showLineNumbers")
+ var showLineNumbers = true
+
+ @AppStorage("preferences.tabWidth")
+ var tabWidth = 4
+
+ @AppStorage("preferences.resultRowHeight")
+ var resultRowHeight = 24.0
+
+ @AppStorage("preferences.resultColumnWidth")
+ var resultColumnWidth = 170.0
+
+ @AppStorage("preferences.alternatingRows")
+ var alternatingRows = true
+
+ @AppStorage("preferences.appearance")
+ var appearance = "light"
+
+ @AppStorage("preferences.autoExpandTables")
+ var autoExpandTables = true
+}
+
+struct WorkbenchSettingsView: View {
+ @AppStorage("preferences.editorFontSize")
+ private var editorFontSize = 13.0
+
+ @AppStorage("preferences.uppercaseKeywords")
+ private var uppercaseKeywords = true
+
+ @AppStorage("preferences.showLineNumbers")
+ private var showLineNumbers = true
+
+ @AppStorage("preferences.tabWidth")
+ private var tabWidth = 4
+
+ @AppStorage("preferences.resultRowHeight")
+ private var resultRowHeight = 24.0
+
+ @AppStorage("preferences.resultColumnWidth")
+ private var resultColumnWidth = 170.0
+
+ @AppStorage("preferences.alternatingRows")
+ private var alternatingRows = true
+
+ @AppStorage("preferences.appearance")
+ private var appearance = "light"
+
+ @AppStorage("preferences.autoExpandTables")
+ private var autoExpandTables = true
+
+ var body: some View {
+ TabView {
+ Form {
+ HStack {
+ Text("Editor Font Size")
+ Spacer()
+ Stepper(
+ "\(Int(editorFontSize)) pt",
+ value: $editorFontSize,
+ in: 10...24,
+ step: 1
+ )
+ .frame(width: 130)
+ }
+
+ Toggle(
+ "Automatically uppercase SQL keywords",
+ isOn: $uppercaseKeywords
+ )
+
+ Toggle(
+ "Show line numbers",
+ isOn: $showLineNumbers
+ )
+
+ Picker("Tab Width", selection: $tabWidth) {
+ Text("2 spaces").tag(2)
+ Text("4 spaces").tag(4)
+ Text("8 spaces").tag(8)
+ }
+ }
+ .padding(20)
+ .tabItem {
+ Label("SQL Editor", systemImage: "chevron.left.forwardslash.chevron.right")
+ }
+
+ Form {
+ HStack {
+ Text("Result Row Height")
+ Spacer()
+ Stepper(
+ "\(Int(resultRowHeight)) px",
+ value: $resultRowHeight,
+ in: 18...40,
+ step: 1
+ )
+ .frame(width: 130)
+ }
+
+ HStack {
+ Text("Default Column Width")
+ Spacer()
+ Stepper(
+ "\(Int(resultColumnWidth)) px",
+ value: $resultColumnWidth,
+ in: 80...400,
+ step: 10
+ )
+ .frame(width: 140)
+ }
+
+ Toggle(
+ "Alternating row colours",
+ isOn: $alternatingRows
+ )
+ }
+ .padding(20)
+ .tabItem {
+ Label("Result Grid", systemImage: "tablecells")
+ }
+
+ Form {
+ Picker("Appearance", selection: $appearance) {
+ Text("Light").tag("light")
+ Text("Dark").tag("dark")
+ Text("System").tag("system")
+ }
+
+ Toggle(
+ "Automatically expand table folders",
+ isOn: $autoExpandTables
+ )
+ }
+ .padding(20)
+ .tabItem {
+ Label("General", systemImage: "gearshape")
+ }
+ }
+ .frame(width: 560, height: 330)
+ }
+}
diff --git a/WorkbenchReplacement/DatabaseModel.swift b/WorkbenchReplacement/DatabaseModel.swift
index d48fa03..b12439c 100644
--- a/WorkbenchReplacement/DatabaseModel.swift
+++ b/WorkbenchReplacement/DatabaseModel.swift
@@ -47,6 +47,7 @@ final class DatabaseModel: ObservableObject {
@Published var result = QueryResult()
@Published var databases: [String] = []
@Published var schemaTables: [String: [String]] = [:]
+ @Published var schemaViews: [String: [String]] = [:]
@Published var selectedDatabase = ""
@Published var isRunning = false
@Published var status = "Not connected"
@@ -74,40 +75,92 @@ final class DatabaseModel: ObservableObject {
sql = SQLFormatter.uppercaseKeywords(sql)
}
+
func loadTables(database: String) {
- guard let profile = selected else { return }
+ guard let profile = selected else {
+ status = "Select a MySQL connection"
+ return
+ }
let password = PasswordStore.read(profile.id)
- let escaped = database.replacingOccurrences(of: "`", with: "``")
+ let escaped = database.replacingOccurrences(
+ of: "`",
+ with: "``"
+ )
+
+ status = "Loading tables from \(database)…"
Task {
do {
let parsed = try await NativeMySQLRunner.run(
profile: profile,
- database: "",
+ database: database,
password: password,
- sql: "SHOW TABLES FROM `\\(escaped)`"
+ sql: "SHOW FULL TABLES FROM `\(escaped)`"
)
- schemaTables[database] = parsed.rows.compactMap { $0.first }
+ var tables: [String] = []
+ var views: [String] = []
+
+ for row in parsed.rows {
+ guard let name = row.first else {
+ continue
+ }
+
+ if row.count > 1 && row[1] == "VIEW" {
+ views.append(name)
+ } else {
+ tables.append(name)
+ }
+ }
+
+ schemaTables[database] = tables
+ schemaViews[database] = views
+
+ status = "Loaded \(tables.count) tables and \(views.count) views from \(database)"
} catch {
- status = "Unable to load tables: \\(error.localizedDescription)"
+ status = "Unable to load tables: \(error.localizedDescription)"
+ result = QueryResult(message: error.localizedDescription)
}
}
}
- func selectTable(database: String, table: String) {
- let db = database.replacingOccurrences(of: "`", with: "``")
- let tbl = table.replacingOccurrences(of: "`", with: "``")
- sql = "SELECT * FROM `\\(db)`.`\\(tbl)` LIMIT 100;"
+
+ func selectTable(database: String, table: String) {
+ let db = database.replacingOccurrences(
+ of: "`",
+ with: "``"
+ )
+
+ let tbl = table.replacingOccurrences(
+ of: "`",
+ with: "``"
+ )
+
+ selectedDatabase = database
+
+ let configuredLimit = UserDefaults.standard.integer(
+ forKey: "preferences.defaultRowLimit"
+ )
+
+ let rowLimit = configuredLimit > 0
+ ? min(configuredLimit, 100000)
+ : 500
+
+ sql = "SELECT * FROM `\(db)`.`\(tbl)` LIMIT \(rowLimit);"
+
runSQL()
}
func loadDatabases() { execute("SHOW DATABASES;", listingDatabases: true) }
func runSQL() {
- formatSQL()
+ if UserDefaults.standard.object(
+ forKey: "preferences.uppercaseKeywords"
+ ) as? Bool ?? true {
+ formatSQL()
+ }
execute(sql, listingDatabases: false)
}
private func execute(_ statement: String, listingDatabases: Bool) {
diff --git a/WorkbenchReplacement/DatabaseModel.swift.backup-20261016-170516 b/WorkbenchReplacement/DatabaseModel.swift.backup-20261016-170516
new file mode 100644
index 0000000..d48fa03
--- /dev/null
+++ b/WorkbenchReplacement/DatabaseModel.swift.backup-20261016-170516
@@ -0,0 +1,294 @@
+import Foundation
+import Security
+
+struct ConnectionProfile: Identifiable, Codable, Hashable {
+ var id = UUID()
+ var name = "Local MySQL"
+ var host = "127.0.0.1"
+ var port = 3306
+ var username = "root"
+ var database = ""
+}
+struct QueryResult {
+ var columns: [String] = []
+ var rows: [[String]] = []
+ var message = ""
+}
+
+enum PasswordStore {
+ static func read(_ id: UUID) -> String {
+ let query: [String: Any] = [kSecClass as String: kSecClassGenericPassword,
+ kSecAttrService as String: "WorkbenchReplacement.MySQL",
+ kSecAttrAccount as String: id.uuidString,
+ kSecReturnData as String: true,
+ kSecMatchLimit as String: kSecMatchLimitOne]
+ var value: CFTypeRef?
+ guard SecItemCopyMatching(query as CFDictionary, &value) == errSecSuccess,
+ let data = value as? Data else { return "" }
+ return String(data: data, encoding: .utf8) ?? ""
+ }
+ static func save(_ password: String, id: UUID) {
+ let query: [String: Any] = [kSecClass as String: kSecClassGenericPassword,
+ kSecAttrService as String: "WorkbenchReplacement.MySQL",
+ kSecAttrAccount as String: id.uuidString]
+ SecItemDelete(query as CFDictionary)
+ guard !password.isEmpty else { return }
+ var attributes = query
+ attributes[kSecValueData as String] = Data(password.utf8)
+ SecItemAdd(attributes as CFDictionary, nil)
+ }
+}
+
+@MainActor
+final class DatabaseModel: ObservableObject {
+ @Published var profiles: [ConnectionProfile] = []
+ @Published var selectedID: UUID?
+ @Published var sql = "SELECT VERSION() AS server_version;"
+ @Published var result = QueryResult()
+ @Published var databases: [String] = []
+ @Published var schemaTables: [String: [String]] = [:]
+ @Published var selectedDatabase = ""
+ @Published var isRunning = false
+ @Published var status = "Not connected"
+ private let storageKey = "WorkbenchReplacement.connectionProfiles.native.v1"
+
+ init() {
+ if let data = UserDefaults.standard.data(forKey: storageKey),
+ let decoded = try? JSONDecoder().decode([ConnectionProfile].self, from: data) { profiles = decoded }
+ if profiles.isEmpty { profiles = [ConnectionProfile()] }
+ selectedID = profiles.first?.id
+ }
+ var selected: ConnectionProfile? { profiles.first { $0.id == selectedID } }
+ func save(_ profile: ConnectionProfile, password: String) {
+ if let i = profiles.firstIndex(where: { $0.id == profile.id }) { profiles[i] = profile }
+ else { profiles.append(profile) }
+ PasswordStore.save(password, id: profile.id)
+ selectedID = profile.id
+ persist()
+ }
+ private func persist() {
+ if let data = try? JSONEncoder().encode(profiles) { UserDefaults.standard.set(data, forKey: storageKey) }
+ }
+
+ func formatSQL() {
+ sql = SQLFormatter.uppercaseKeywords(sql)
+ }
+
+ func loadTables(database: String) {
+ guard let profile = selected else { return }
+
+ let password = PasswordStore.read(profile.id)
+ let escaped = database.replacingOccurrences(of: "`", with: "``")
+
+ Task {
+ do {
+ let parsed = try await NativeMySQLRunner.run(
+ profile: profile,
+ database: "",
+ password: password,
+ sql: "SHOW TABLES FROM `\\(escaped)`"
+ )
+
+ schemaTables[database] = parsed.rows.compactMap { $0.first }
+ } catch {
+ status = "Unable to load tables: \\(error.localizedDescription)"
+ }
+ }
+ }
+
+ func selectTable(database: String, table: String) {
+ let db = database.replacingOccurrences(of: "`", with: "``")
+ let tbl = table.replacingOccurrences(of: "`", with: "``")
+
+ sql = "SELECT * FROM `\\(db)`.`\\(tbl)` LIMIT 100;"
+ runSQL()
+ }
+
+ func loadDatabases() { execute("SHOW DATABASES;", listingDatabases: true) }
+
+ func runSQL() {
+ formatSQL()
+ execute(sql, listingDatabases: false)
+ }
+ private func execute(_ statement: String, listingDatabases: Bool) {
+ guard let profile = selected else { status = "Select a connection"; return }
+ guard !statement.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty else { return }
+ isRunning = true
+ status = "Connecting…"
+ let activeDB = listingDatabases ? "" : (selectedDatabase.isEmpty ? profile.database : selectedDatabase)
+ let password = PasswordStore.read(profile.id)
+ Task {
+ do {
+ let parsed = try await NativeMySQLRunner.run(profile: profile, database: activeDB, password: password, sql: statement)
+ if listingDatabases {
+ databases = parsed.rows.compactMap { $0.first }
+ status = "Connected to \(profile.name)"
+ } else {
+ result = parsed
+ status = "Query completed (\(parsed.rows.count) rows)"
+ }
+ } catch {
+ status = "Error"
+ result = QueryResult(message: error.localizedDescription)
+ }
+ isRunning = false
+ }
+ }
+}
+
+
+// MARK: - SQL Keyword Formatter
+
+enum SQLFormatter {
+
+ private static let keywords: Set = [
+ "SELECT", "FROM", "WHERE", "INSERT", "INTO", "VALUES",
+ "UPDATE", "DELETE", "CREATE", "ALTER", "DROP", "TABLE",
+ "DATABASE", "SCHEMA", "USE", "SHOW", "DESCRIBE", "EXPLAIN",
+ "JOIN", "INNER", "LEFT", "RIGHT", "FULL", "OUTER",
+ "ON", "AS", "AND", "OR", "NOT", "NULL", "IS", "IN",
+ "BETWEEN", "LIKE", "ORDER", "BY", "GROUP", "HAVING",
+ "LIMIT", "OFFSET", "ASC", "DESC", "DISTINCT", "ALL",
+ "PRIMARY", "KEY", "FOREIGN", "REFERENCES", "INDEX",
+ "UNIQUE", "DEFAULT", "AUTO_INCREMENT", "IF", "EXISTS",
+ "CASCADE", "RESTRICT", "SET", "BEGIN", "COMMIT",
+ "ROLLBACK", "TRANSACTION", "UNION", "CASE", "WHEN",
+ "THEN", "ELSE", "END", "TRUE", "FALSE", "COUNT",
+ "SUM", "AVG", "MIN", "MAX", "HAVING", "WITH",
+ "RECURSIVE", "TRUNCATE", "REPLACE", "VIEW",
+ "PROCEDURE", "FUNCTION", "TRIGGER", "GRANT",
+ "REVOKE", "DISTINCT", "CURRENT_TIMESTAMP"
+ ]
+
+ static func uppercaseKeywords(_ sql: String) -> String {
+ let chars = Array(sql)
+ var output = ""
+ var index = 0
+
+ enum State {
+ case normal
+ case singleQuote
+ case doubleQuote
+ case backtick
+ case lineComment
+ case blockComment
+ }
+
+ var state = State.normal
+
+ while index < chars.count {
+ let ch = chars[index]
+ let next: Character? = index + 1 < chars.count
+ ? chars[index + 1] : nil
+
+ switch state {
+ case .normal:
+ if ch == "'" {
+ state = .singleQuote
+ output.append(ch)
+ index += 1
+ } else if ch == "\"" {
+ state = .doubleQuote
+ output.append(ch)
+ index += 1
+ } else if ch == "`" {
+ state = .backtick
+ output.append(ch)
+ index += 1
+ } else if ch == "-" && next == "-" &&
+ (index + 2 == chars.count ||
+ chars[index + 2].isWhitespace) {
+ state = .lineComment
+ output += "--"
+ index += 2
+ } else if ch == "#" {
+ state = .lineComment
+ output.append(ch)
+ index += 1
+ } else if ch == "/" && next == "*" {
+ state = .blockComment
+ output += "/*"
+ index += 2
+ } else if ch.isLetter || ch == "_" {
+ let start = index
+ while index < chars.count &&
+ (chars[index].isLetter ||
+ chars[index].isNumber ||
+ chars[index] == "_" ||
+ chars[index] == "$") {
+ index += 1
+ }
+
+ let word = String(chars[start.. String {
+ let query: [String: Any] = [kSecClass as String: kSecClassGenericPassword,
+ kSecAttrService as String: "WorkbenchReplacement.MySQL",
+ kSecAttrAccount as String: id.uuidString,
+ kSecReturnData as String: true,
+ kSecMatchLimit as String: kSecMatchLimitOne]
+ var value: CFTypeRef?
+ guard SecItemCopyMatching(query as CFDictionary, &value) == errSecSuccess,
+ let data = value as? Data else { return "" }
+ return String(data: data, encoding: .utf8) ?? ""
+ }
+ static func save(_ password: String, id: UUID) {
+ let query: [String: Any] = [kSecClass as String: kSecClassGenericPassword,
+ kSecAttrService as String: "WorkbenchReplacement.MySQL",
+ kSecAttrAccount as String: id.uuidString]
+ SecItemDelete(query as CFDictionary)
+ guard !password.isEmpty else { return }
+ var attributes = query
+ attributes[kSecValueData as String] = Data(password.utf8)
+ SecItemAdd(attributes as CFDictionary, nil)
+ }
+}
+
+@MainActor
+final class DatabaseModel: ObservableObject {
+ @Published var profiles: [ConnectionProfile] = []
+ @Published var selectedID: UUID?
+ @Published var sql = "SELECT VERSION() AS server_version;"
+ @Published var result = QueryResult()
+ @Published var databases: [String] = []
+ @Published var schemaTables: [String: [String]] = [:]
+ @Published var selectedDatabase = ""
+ @Published var isRunning = false
+ @Published var status = "Not connected"
+ private let storageKey = "WorkbenchReplacement.connectionProfiles.native.v1"
+
+ init() {
+ if let data = UserDefaults.standard.data(forKey: storageKey),
+ let decoded = try? JSONDecoder().decode([ConnectionProfile].self, from: data) { profiles = decoded }
+ if profiles.isEmpty { profiles = [ConnectionProfile()] }
+ selectedID = profiles.first?.id
+ }
+ var selected: ConnectionProfile? { profiles.first { $0.id == selectedID } }
+ func save(_ profile: ConnectionProfile, password: String) {
+ if let i = profiles.firstIndex(where: { $0.id == profile.id }) { profiles[i] = profile }
+ else { profiles.append(profile) }
+ PasswordStore.save(password, id: profile.id)
+ selectedID = profile.id
+ persist()
+ }
+ private func persist() {
+ if let data = try? JSONEncoder().encode(profiles) { UserDefaults.standard.set(data, forKey: storageKey) }
+ }
+
+ func formatSQL() {
+ sql = SQLFormatter.uppercaseKeywords(sql)
+ }
+
+ func loadTables(database: String) {
+ guard let profile = selected else { return }
+
+ let password = PasswordStore.read(profile.id)
+ let escaped = database.replacingOccurrences(of: "`", with: "``")
+
+ Task {
+ do {
+ let parsed = try await NativeMySQLRunner.run(
+ profile: profile,
+ database: "",
+ password: password,
+ sql: "SHOW TABLES FROM `\\(escaped)`"
+ )
+
+ schemaTables[database] = parsed.rows.compactMap { $0.first }
+ } catch {
+ status = "Unable to load tables: \\(error.localizedDescription)"
+ }
+ }
+ }
+
+ func selectTable(database: String, table: String) {
+ let db = database.replacingOccurrences(of: "`", with: "``")
+ let tbl = table.replacingOccurrences(of: "`", with: "``")
+
+ sql = "SELECT * FROM `\\(db)`.`\\(tbl)` LIMIT 100;"
+ runSQL()
+ }
+
+ func loadDatabases() { execute("SHOW DATABASES;", listingDatabases: true) }
+
+ func runSQL() {
+ formatSQL()
+ execute(sql, listingDatabases: false)
+ }
+ private func execute(_ statement: String, listingDatabases: Bool) {
+ guard let profile = selected else { status = "Select a connection"; return }
+ guard !statement.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty else { return }
+ isRunning = true
+ status = "Connecting…"
+ let activeDB = listingDatabases ? "" : (selectedDatabase.isEmpty ? profile.database : selectedDatabase)
+ let password = PasswordStore.read(profile.id)
+ Task {
+ do {
+ let parsed = try await NativeMySQLRunner.run(profile: profile, database: activeDB, password: password, sql: statement)
+ if listingDatabases {
+ databases = parsed.rows.compactMap { $0.first }
+ status = "Connected to \(profile.name)"
+ } else {
+ result = parsed
+ status = "Query completed (\(parsed.rows.count) rows)"
+ }
+ } catch {
+ status = "Error"
+ result = QueryResult(message: error.localizedDescription)
+ }
+ isRunning = false
+ }
+ }
+}
+
+
+// MARK: - SQL Keyword Formatter
+
+enum SQLFormatter {
+
+ private static let keywords: Set = [
+ "SELECT", "FROM", "WHERE", "INSERT", "INTO", "VALUES",
+ "UPDATE", "DELETE", "CREATE", "ALTER", "DROP", "TABLE",
+ "DATABASE", "SCHEMA", "USE", "SHOW", "DESCRIBE", "EXPLAIN",
+ "JOIN", "INNER", "LEFT", "RIGHT", "FULL", "OUTER",
+ "ON", "AS", "AND", "OR", "NOT", "NULL", "IS", "IN",
+ "BETWEEN", "LIKE", "ORDER", "BY", "GROUP", "HAVING",
+ "LIMIT", "OFFSET", "ASC", "DESC", "DISTINCT", "ALL",
+ "PRIMARY", "KEY", "FOREIGN", "REFERENCES", "INDEX",
+ "UNIQUE", "DEFAULT", "AUTO_INCREMENT", "IF", "EXISTS",
+ "CASCADE", "RESTRICT", "SET", "BEGIN", "COMMIT",
+ "ROLLBACK", "TRANSACTION", "UNION", "CASE", "WHEN",
+ "THEN", "ELSE", "END", "TRUE", "FALSE", "COUNT",
+ "SUM", "AVG", "MIN", "MAX", "HAVING", "WITH",
+ "RECURSIVE", "TRUNCATE", "REPLACE", "VIEW",
+ "PROCEDURE", "FUNCTION", "TRIGGER", "GRANT",
+ "REVOKE", "DISTINCT", "CURRENT_TIMESTAMP"
+ ]
+
+ static func uppercaseKeywords(_ sql: String) -> String {
+ let chars = Array(sql)
+ var output = ""
+ var index = 0
+
+ enum State {
+ case normal
+ case singleQuote
+ case doubleQuote
+ case backtick
+ case lineComment
+ case blockComment
+ }
+
+ var state = State.normal
+
+ while index < chars.count {
+ let ch = chars[index]
+ let next: Character? = index + 1 < chars.count
+ ? chars[index + 1] : nil
+
+ switch state {
+ case .normal:
+ if ch == "'" {
+ state = .singleQuote
+ output.append(ch)
+ index += 1
+ } else if ch == "\"" {
+ state = .doubleQuote
+ output.append(ch)
+ index += 1
+ } else if ch == "`" {
+ state = .backtick
+ output.append(ch)
+ index += 1
+ } else if ch == "-" && next == "-" &&
+ (index + 2 == chars.count ||
+ chars[index + 2].isWhitespace) {
+ state = .lineComment
+ output += "--"
+ index += 2
+ } else if ch == "#" {
+ state = .lineComment
+ output.append(ch)
+ index += 1
+ } else if ch == "/" && next == "*" {
+ state = .blockComment
+ output += "/*"
+ index += 2
+ } else if ch.isLetter || ch == "_" {
+ let start = index
+ while index < chars.count &&
+ (chars[index].isLetter ||
+ chars[index].isNumber ||
+ chars[index] == "_" ||
+ chars[index] == "$") {
+ index += 1
+ }
+
+ let word = String(chars[start.. String {
+ let query: [String: Any] = [kSecClass as String: kSecClassGenericPassword,
+ kSecAttrService as String: "WorkbenchReplacement.MySQL",
+ kSecAttrAccount as String: id.uuidString,
+ kSecReturnData as String: true,
+ kSecMatchLimit as String: kSecMatchLimitOne]
+ var value: CFTypeRef?
+ guard SecItemCopyMatching(query as CFDictionary, &value) == errSecSuccess,
+ let data = value as? Data else { return "" }
+ return String(data: data, encoding: .utf8) ?? ""
+ }
+ static func save(_ password: String, id: UUID) {
+ let query: [String: Any] = [kSecClass as String: kSecClassGenericPassword,
+ kSecAttrService as String: "WorkbenchReplacement.MySQL",
+ kSecAttrAccount as String: id.uuidString]
+ SecItemDelete(query as CFDictionary)
+ guard !password.isEmpty else { return }
+ var attributes = query
+ attributes[kSecValueData as String] = Data(password.utf8)
+ SecItemAdd(attributes as CFDictionary, nil)
+ }
+}
+
+@MainActor
+final class DatabaseModel: ObservableObject {
+ @Published var profiles: [ConnectionProfile] = []
+ @Published var selectedID: UUID?
+ @Published var sql = "SELECT VERSION() AS server_version;"
+ @Published var result = QueryResult()
+ @Published var databases: [String] = []
+ @Published var schemaTables: [String: [String]] = [:]
+ @Published var schemaViews: [String: [String]] = [:]
+ @Published var selectedDatabase = ""
+ @Published var isRunning = false
+ @Published var status = "Not connected"
+ private let storageKey = "WorkbenchReplacement.connectionProfiles.native.v1"
+
+ init() {
+ if let data = UserDefaults.standard.data(forKey: storageKey),
+ let decoded = try? JSONDecoder().decode([ConnectionProfile].self, from: data) { profiles = decoded }
+ if profiles.isEmpty { profiles = [ConnectionProfile()] }
+ selectedID = profiles.first?.id
+ }
+ var selected: ConnectionProfile? { profiles.first { $0.id == selectedID } }
+ func save(_ profile: ConnectionProfile, password: String) {
+ if let i = profiles.firstIndex(where: { $0.id == profile.id }) { profiles[i] = profile }
+ else { profiles.append(profile) }
+ PasswordStore.save(password, id: profile.id)
+ selectedID = profile.id
+ persist()
+ }
+ private func persist() {
+ if let data = try? JSONEncoder().encode(profiles) { UserDefaults.standard.set(data, forKey: storageKey) }
+ }
+
+ func formatSQL() {
+ sql = SQLFormatter.uppercaseKeywords(sql)
+ }
+
+
+ func loadTables(database: String) {
+ guard let profile = selected else {
+ status = "Select a MySQL connection"
+ return
+ }
+
+ let password = PasswordStore.read(profile.id)
+ let escaped = database.replacingOccurrences(
+ of: "`",
+ with: "``"
+ )
+
+ status = "Loading tables from \(database)…"
+
+ Task {
+ do {
+ let parsed = try await NativeMySQLRunner.run(
+ profile: profile,
+ database: database,
+ password: password,
+ sql: "SHOW FULL TABLES FROM `\(escaped)`"
+ )
+
+ var tables: [String] = []
+ var views: [String] = []
+
+ for row in parsed.rows {
+ guard let name = row.first else {
+ continue
+ }
+
+ if row.count > 1 && row[1] == "VIEW" {
+ views.append(name)
+ } else {
+ tables.append(name)
+ }
+ }
+
+ schemaTables[database] = tables
+ schemaViews[database] = views
+
+ status = "Loaded \(tables.count) tables and \(views.count) views from \(database)"
+ } catch {
+ status = "Unable to load tables: \(error.localizedDescription)"
+ result = QueryResult(message: error.localizedDescription)
+ }
+ }
+ }
+
+ func selectTable(database: String, table: String) {
+ let db = database.replacingOccurrences(of: "`", with: "``")
+ let tbl = table.replacingOccurrences(of: "`", with: "``")
+
+ sql = "SELECT * FROM `\\(db)`.`\\(tbl)` LIMIT 100;"
+ runSQL()
+ }
+
+ func loadDatabases() { execute("SHOW DATABASES;", listingDatabases: true) }
+
+ func runSQL() {
+ formatSQL()
+ execute(sql, listingDatabases: false)
+ }
+ private func execute(_ statement: String, listingDatabases: Bool) {
+ guard let profile = selected else { status = "Select a connection"; return }
+ guard !statement.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty else { return }
+ isRunning = true
+ status = "Connecting…"
+ let activeDB = listingDatabases ? "" : (selectedDatabase.isEmpty ? profile.database : selectedDatabase)
+ let password = PasswordStore.read(profile.id)
+ Task {
+ do {
+ let parsed = try await NativeMySQLRunner.run(profile: profile, database: activeDB, password: password, sql: statement)
+ if listingDatabases {
+ databases = parsed.rows.compactMap { $0.first }
+ status = "Connected to \(profile.name)"
+ } else {
+ result = parsed
+ status = "Query completed (\(parsed.rows.count) rows)"
+ }
+ } catch {
+ status = "Error"
+ result = QueryResult(message: error.localizedDescription)
+ }
+ isRunning = false
+ }
+ }
+}
+
+
+// MARK: - SQL Keyword Formatter
+
+enum SQLFormatter {
+
+ private static let keywords: Set = [
+ "SELECT", "FROM", "WHERE", "INSERT", "INTO", "VALUES",
+ "UPDATE", "DELETE", "CREATE", "ALTER", "DROP", "TABLE",
+ "DATABASE", "SCHEMA", "USE", "SHOW", "DESCRIBE", "EXPLAIN",
+ "JOIN", "INNER", "LEFT", "RIGHT", "FULL", "OUTER",
+ "ON", "AS", "AND", "OR", "NOT", "NULL", "IS", "IN",
+ "BETWEEN", "LIKE", "ORDER", "BY", "GROUP", "HAVING",
+ "LIMIT", "OFFSET", "ASC", "DESC", "DISTINCT", "ALL",
+ "PRIMARY", "KEY", "FOREIGN", "REFERENCES", "INDEX",
+ "UNIQUE", "DEFAULT", "AUTO_INCREMENT", "IF", "EXISTS",
+ "CASCADE", "RESTRICT", "SET", "BEGIN", "COMMIT",
+ "ROLLBACK", "TRANSACTION", "UNION", "CASE", "WHEN",
+ "THEN", "ELSE", "END", "TRUE", "FALSE", "COUNT",
+ "SUM", "AVG", "MIN", "MAX", "HAVING", "WITH",
+ "RECURSIVE", "TRUNCATE", "REPLACE", "VIEW",
+ "PROCEDURE", "FUNCTION", "TRIGGER", "GRANT",
+ "REVOKE", "DISTINCT", "CURRENT_TIMESTAMP"
+ ]
+
+ static func uppercaseKeywords(_ sql: String) -> String {
+ let chars = Array(sql)
+ var output = ""
+ var index = 0
+
+ enum State {
+ case normal
+ case singleQuote
+ case doubleQuote
+ case backtick
+ case lineComment
+ case blockComment
+ }
+
+ var state = State.normal
+
+ while index < chars.count {
+ let ch = chars[index]
+ let next: Character? = index + 1 < chars.count
+ ? chars[index + 1] : nil
+
+ switch state {
+ case .normal:
+ if ch == "'" {
+ state = .singleQuote
+ output.append(ch)
+ index += 1
+ } else if ch == "\"" {
+ state = .doubleQuote
+ output.append(ch)
+ index += 1
+ } else if ch == "`" {
+ state = .backtick
+ output.append(ch)
+ index += 1
+ } else if ch == "-" && next == "-" &&
+ (index + 2 == chars.count ||
+ chars[index + 2].isWhitespace) {
+ state = .lineComment
+ output += "--"
+ index += 2
+ } else if ch == "#" {
+ state = .lineComment
+ output.append(ch)
+ index += 1
+ } else if ch == "/" && next == "*" {
+ state = .blockComment
+ output += "/*"
+ index += 2
+ } else if ch.isLetter || ch == "_" {
+ let start = index
+ while index < chars.count &&
+ (chars[index].isLetter ||
+ chars[index].isNumber ||
+ chars[index] == "_" ||
+ chars[index] == "$") {
+ index += 1
+ }
+
+ let word = String(chars[start.. String {
+ let query: [String: Any] = [kSecClass as String: kSecClassGenericPassword,
+ kSecAttrService as String: "WorkbenchReplacement.MySQL",
+ kSecAttrAccount as String: id.uuidString,
+ kSecReturnData as String: true,
+ kSecMatchLimit as String: kSecMatchLimitOne]
+ var value: CFTypeRef?
+ guard SecItemCopyMatching(query as CFDictionary, &value) == errSecSuccess,
+ let data = value as? Data else { return "" }
+ return String(data: data, encoding: .utf8) ?? ""
+ }
+ static func save(_ password: String, id: UUID) {
+ let query: [String: Any] = [kSecClass as String: kSecClassGenericPassword,
+ kSecAttrService as String: "WorkbenchReplacement.MySQL",
+ kSecAttrAccount as String: id.uuidString]
+ SecItemDelete(query as CFDictionary)
+ guard !password.isEmpty else { return }
+ var attributes = query
+ attributes[kSecValueData as String] = Data(password.utf8)
+ SecItemAdd(attributes as CFDictionary, nil)
+ }
+}
+
+@MainActor
+final class DatabaseModel: ObservableObject {
+ @Published var profiles: [ConnectionProfile] = []
+ @Published var selectedID: UUID?
+ @Published var sql = "SELECT VERSION() AS server_version;"
+ @Published var result = QueryResult()
+ @Published var databases: [String] = []
+ @Published var schemaTables: [String: [String]] = [:]
+ @Published var schemaViews: [String: [String]] = [:]
+ @Published var selectedDatabase = ""
+ @Published var isRunning = false
+ @Published var status = "Not connected"
+ private let storageKey = "WorkbenchReplacement.connectionProfiles.native.v1"
+
+ init() {
+ if let data = UserDefaults.standard.data(forKey: storageKey),
+ let decoded = try? JSONDecoder().decode([ConnectionProfile].self, from: data) { profiles = decoded }
+ if profiles.isEmpty { profiles = [ConnectionProfile()] }
+ selectedID = profiles.first?.id
+ }
+ var selected: ConnectionProfile? { profiles.first { $0.id == selectedID } }
+ func save(_ profile: ConnectionProfile, password: String) {
+ if let i = profiles.firstIndex(where: { $0.id == profile.id }) { profiles[i] = profile }
+ else { profiles.append(profile) }
+ PasswordStore.save(password, id: profile.id)
+ selectedID = profile.id
+ persist()
+ }
+ private func persist() {
+ if let data = try? JSONEncoder().encode(profiles) { UserDefaults.standard.set(data, forKey: storageKey) }
+ }
+
+ func formatSQL() {
+ sql = SQLFormatter.uppercaseKeywords(sql)
+ }
+
+
+ func loadTables(database: String) {
+ guard let profile = selected else {
+ status = "Select a MySQL connection"
+ return
+ }
+
+ let password = PasswordStore.read(profile.id)
+ let escaped = database.replacingOccurrences(
+ of: "`",
+ with: "``"
+ )
+
+ status = "Loading tables from \(database)…"
+
+ Task {
+ do {
+ let parsed = try await NativeMySQLRunner.run(
+ profile: profile,
+ database: database,
+ password: password,
+ sql: "SHOW FULL TABLES FROM `\(escaped)`"
+ )
+
+ var tables: [String] = []
+ var views: [String] = []
+
+ for row in parsed.rows {
+ guard let name = row.first else {
+ continue
+ }
+
+ if row.count > 1 && row[1] == "VIEW" {
+ views.append(name)
+ } else {
+ tables.append(name)
+ }
+ }
+
+ schemaTables[database] = tables
+ schemaViews[database] = views
+
+ status = "Loaded \(tables.count) tables and \(views.count) views from \(database)"
+ } catch {
+ status = "Unable to load tables: \(error.localizedDescription)"
+ result = QueryResult(message: error.localizedDescription)
+ }
+ }
+ }
+
+
+ func selectTable(database: String, table: String) {
+ let db = database.replacingOccurrences(
+ of: "`",
+ with: "``"
+ )
+
+ let tbl = table.replacingOccurrences(
+ of: "`",
+ with: "``"
+ )
+
+ selectedDatabase = database
+
+ sql = "SELECT * FROM `\(db)`.`\(tbl)` LIMIT 100;"
+
+ runSQL()
+ }
+
+ func loadDatabases() { execute("SHOW DATABASES;", listingDatabases: true) }
+
+ func runSQL() {
+ formatSQL()
+ execute(sql, listingDatabases: false)
+ }
+ private func execute(_ statement: String, listingDatabases: Bool) {
+ guard let profile = selected else { status = "Select a connection"; return }
+ guard !statement.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty else { return }
+ isRunning = true
+ status = "Connecting…"
+ let activeDB = listingDatabases ? "" : (selectedDatabase.isEmpty ? profile.database : selectedDatabase)
+ let password = PasswordStore.read(profile.id)
+ Task {
+ do {
+ let parsed = try await NativeMySQLRunner.run(profile: profile, database: activeDB, password: password, sql: statement)
+ if listingDatabases {
+ databases = parsed.rows.compactMap { $0.first }
+ status = "Connected to \(profile.name)"
+ } else {
+ result = parsed
+ status = "Query completed (\(parsed.rows.count) rows)"
+ }
+ } catch {
+ status = "Error"
+ result = QueryResult(message: error.localizedDescription)
+ }
+ isRunning = false
+ }
+ }
+}
+
+
+// MARK: - SQL Keyword Formatter
+
+enum SQLFormatter {
+
+ private static let keywords: Set = [
+ "SELECT", "FROM", "WHERE", "INSERT", "INTO", "VALUES",
+ "UPDATE", "DELETE", "CREATE", "ALTER", "DROP", "TABLE",
+ "DATABASE", "SCHEMA", "USE", "SHOW", "DESCRIBE", "EXPLAIN",
+ "JOIN", "INNER", "LEFT", "RIGHT", "FULL", "OUTER",
+ "ON", "AS", "AND", "OR", "NOT", "NULL", "IS", "IN",
+ "BETWEEN", "LIKE", "ORDER", "BY", "GROUP", "HAVING",
+ "LIMIT", "OFFSET", "ASC", "DESC", "DISTINCT", "ALL",
+ "PRIMARY", "KEY", "FOREIGN", "REFERENCES", "INDEX",
+ "UNIQUE", "DEFAULT", "AUTO_INCREMENT", "IF", "EXISTS",
+ "CASCADE", "RESTRICT", "SET", "BEGIN", "COMMIT",
+ "ROLLBACK", "TRANSACTION", "UNION", "CASE", "WHEN",
+ "THEN", "ELSE", "END", "TRUE", "FALSE", "COUNT",
+ "SUM", "AVG", "MIN", "MAX", "HAVING", "WITH",
+ "RECURSIVE", "TRUNCATE", "REPLACE", "VIEW",
+ "PROCEDURE", "FUNCTION", "TRIGGER", "GRANT",
+ "REVOKE", "DISTINCT", "CURRENT_TIMESTAMP"
+ ]
+
+ static func uppercaseKeywords(_ sql: String) -> String {
+ let chars = Array(sql)
+ var output = ""
+ var index = 0
+
+ enum State {
+ case normal
+ case singleQuote
+ case doubleQuote
+ case backtick
+ case lineComment
+ case blockComment
+ }
+
+ var state = State.normal
+
+ while index < chars.count {
+ let ch = chars[index]
+ let next: Character? = index + 1 < chars.count
+ ? chars[index + 1] : nil
+
+ switch state {
+ case .normal:
+ if ch == "'" {
+ state = .singleQuote
+ output.append(ch)
+ index += 1
+ } else if ch == "\"" {
+ state = .doubleQuote
+ output.append(ch)
+ index += 1
+ } else if ch == "`" {
+ state = .backtick
+ output.append(ch)
+ index += 1
+ } else if ch == "-" && next == "-" &&
+ (index + 2 == chars.count ||
+ chars[index + 2].isWhitespace) {
+ state = .lineComment
+ output += "--"
+ index += 2
+ } else if ch == "#" {
+ state = .lineComment
+ output.append(ch)
+ index += 1
+ } else if ch == "/" && next == "*" {
+ state = .blockComment
+ output += "/*"
+ index += 2
+ } else if ch.isLetter || ch == "_" {
+ let start = index
+ while index < chars.count &&
+ (chars[index].isLetter ||
+ chars[index].isNumber ||
+ chars[index] == "_" ||
+ chars[index] == "$") {
+ index += 1
+ }
+
+ let word = String(chars[start.. String {
+ let query: [String: Any] = [kSecClass as String: kSecClassGenericPassword,
+ kSecAttrService as String: "WorkbenchReplacement.MySQL",
+ kSecAttrAccount as String: id.uuidString,
+ kSecReturnData as String: true,
+ kSecMatchLimit as String: kSecMatchLimitOne]
+ var value: CFTypeRef?
+ guard SecItemCopyMatching(query as CFDictionary, &value) == errSecSuccess,
+ let data = value as? Data else { return "" }
+ return String(data: data, encoding: .utf8) ?? ""
+ }
+ static func save(_ password: String, id: UUID) {
+ let query: [String: Any] = [kSecClass as String: kSecClassGenericPassword,
+ kSecAttrService as String: "WorkbenchReplacement.MySQL",
+ kSecAttrAccount as String: id.uuidString]
+ SecItemDelete(query as CFDictionary)
+ guard !password.isEmpty else { return }
+ var attributes = query
+ attributes[kSecValueData as String] = Data(password.utf8)
+ SecItemAdd(attributes as CFDictionary, nil)
+ }
+}
+
+@MainActor
+final class DatabaseModel: ObservableObject {
+ @Published var profiles: [ConnectionProfile] = []
+ @Published var selectedID: UUID?
+ @Published var sql = "SELECT VERSION() AS server_version;"
+ @Published var result = QueryResult()
+ @Published var databases: [String] = []
+ @Published var schemaTables: [String: [String]] = [:]
+ @Published var schemaViews: [String: [String]] = [:]
+ @Published var selectedDatabase = ""
+ @Published var isRunning = false
+ @Published var status = "Not connected"
+ private let storageKey = "WorkbenchReplacement.connectionProfiles.native.v1"
+
+ init() {
+ if let data = UserDefaults.standard.data(forKey: storageKey),
+ let decoded = try? JSONDecoder().decode([ConnectionProfile].self, from: data) { profiles = decoded }
+ if profiles.isEmpty { profiles = [ConnectionProfile()] }
+ selectedID = profiles.first?.id
+ }
+ var selected: ConnectionProfile? { profiles.first { $0.id == selectedID } }
+ func save(_ profile: ConnectionProfile, password: String) {
+ if let i = profiles.firstIndex(where: { $0.id == profile.id }) { profiles[i] = profile }
+ else { profiles.append(profile) }
+ PasswordStore.save(password, id: profile.id)
+ selectedID = profile.id
+ persist()
+ }
+ private func persist() {
+ if let data = try? JSONEncoder().encode(profiles) { UserDefaults.standard.set(data, forKey: storageKey) }
+ }
+
+ func formatSQL() {
+ sql = SQLFormatter.uppercaseKeywords(sql)
+ }
+
+
+ func loadTables(database: String) {
+ guard let profile = selected else {
+ status = "Select a MySQL connection"
+ return
+ }
+
+ let password = PasswordStore.read(profile.id)
+ let escaped = database.replacingOccurrences(
+ of: "`",
+ with: "``"
+ )
+
+ status = "Loading tables from \(database)…"
+
+ Task {
+ do {
+ let parsed = try await NativeMySQLRunner.run(
+ profile: profile,
+ database: database,
+ password: password,
+ sql: "SHOW FULL TABLES FROM `\(escaped)`"
+ )
+
+ var tables: [String] = []
+ var views: [String] = []
+
+ for row in parsed.rows {
+ guard let name = row.first else {
+ continue
+ }
+
+ if row.count > 1 && row[1] == "VIEW" {
+ views.append(name)
+ } else {
+ tables.append(name)
+ }
+ }
+
+ schemaTables[database] = tables
+ schemaViews[database] = views
+
+ status = "Loaded \(tables.count) tables and \(views.count) views from \(database)"
+ } catch {
+ status = "Unable to load tables: \(error.localizedDescription)"
+ result = QueryResult(message: error.localizedDescription)
+ }
+ }
+ }
+
+
+
+ func selectTable(database: String, table: String) {
+ let db = database.replacingOccurrences(
+ of: "`",
+ with: "``"
+ )
+
+ let tbl = table.replacingOccurrences(
+ of: "`",
+ with: "``"
+ )
+
+ selectedDatabase = database
+
+ sql = "SELECT * FROM `\(db)`.`\(tbl)` LIMIT 100;"
+
+ runSQL()
+ }
+
+ func loadDatabases() { execute("SHOW DATABASES;", listingDatabases: true) }
+
+ func runSQL() {
+ formatSQL()
+ execute(sql, listingDatabases: false)
+ }
+ private func execute(_ statement: String, listingDatabases: Bool) {
+ guard let profile = selected else { status = "Select a connection"; return }
+ guard !statement.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty else { return }
+ isRunning = true
+ status = "Connecting…"
+ let activeDB = listingDatabases ? "" : (selectedDatabase.isEmpty ? profile.database : selectedDatabase)
+ let password = PasswordStore.read(profile.id)
+ Task {
+ do {
+ let parsed = try await NativeMySQLRunner.run(profile: profile, database: activeDB, password: password, sql: statement)
+ if listingDatabases {
+ databases = parsed.rows.compactMap { $0.first }
+ status = "Connected to \(profile.name)"
+ } else {
+ result = parsed
+ status = "Query completed (\(parsed.rows.count) rows)"
+ }
+ } catch {
+ status = "Error"
+ result = QueryResult(message: error.localizedDescription)
+ }
+ isRunning = false
+ }
+ }
+}
+
+
+// MARK: - SQL Keyword Formatter
+
+enum SQLFormatter {
+
+ private static let keywords: Set = [
+ "SELECT", "FROM", "WHERE", "INSERT", "INTO", "VALUES",
+ "UPDATE", "DELETE", "CREATE", "ALTER", "DROP", "TABLE",
+ "DATABASE", "SCHEMA", "USE", "SHOW", "DESCRIBE", "EXPLAIN",
+ "JOIN", "INNER", "LEFT", "RIGHT", "FULL", "OUTER",
+ "ON", "AS", "AND", "OR", "NOT", "NULL", "IS", "IN",
+ "BETWEEN", "LIKE", "ORDER", "BY", "GROUP", "HAVING",
+ "LIMIT", "OFFSET", "ASC", "DESC", "DISTINCT", "ALL",
+ "PRIMARY", "KEY", "FOREIGN", "REFERENCES", "INDEX",
+ "UNIQUE", "DEFAULT", "AUTO_INCREMENT", "IF", "EXISTS",
+ "CASCADE", "RESTRICT", "SET", "BEGIN", "COMMIT",
+ "ROLLBACK", "TRANSACTION", "UNION", "CASE", "WHEN",
+ "THEN", "ELSE", "END", "TRUE", "FALSE", "COUNT",
+ "SUM", "AVG", "MIN", "MAX", "HAVING", "WITH",
+ "RECURSIVE", "TRUNCATE", "REPLACE", "VIEW",
+ "PROCEDURE", "FUNCTION", "TRIGGER", "GRANT",
+ "REVOKE", "DISTINCT", "CURRENT_TIMESTAMP"
+ ]
+
+ static func uppercaseKeywords(_ sql: String) -> String {
+ let chars = Array(sql)
+ var output = ""
+ var index = 0
+
+ enum State {
+ case normal
+ case singleQuote
+ case doubleQuote
+ case backtick
+ case lineComment
+ case blockComment
+ }
+
+ var state = State.normal
+
+ while index < chars.count {
+ let ch = chars[index]
+ let next: Character? = index + 1 < chars.count
+ ? chars[index + 1] : nil
+
+ switch state {
+ case .normal:
+ if ch == "'" {
+ state = .singleQuote
+ output.append(ch)
+ index += 1
+ } else if ch == "\"" {
+ state = .doubleQuote
+ output.append(ch)
+ index += 1
+ } else if ch == "`" {
+ state = .backtick
+ output.append(ch)
+ index += 1
+ } else if ch == "-" && next == "-" &&
+ (index + 2 == chars.count ||
+ chars[index + 2].isWhitespace) {
+ state = .lineComment
+ output += "--"
+ index += 2
+ } else if ch == "#" {
+ state = .lineComment
+ output.append(ch)
+ index += 1
+ } else if ch == "/" && next == "*" {
+ state = .blockComment
+ output += "/*"
+ index += 2
+ } else if ch.isLetter || ch == "_" {
+ let start = index
+ while index < chars.count &&
+ (chars[index].isLetter ||
+ chars[index].isNumber ||
+ chars[index] == "_" ||
+ chars[index] == "$") {
+ index += 1
+ }
+
+ let word = String(chars[start.. String {
+ let query: [String: Any] = [kSecClass as String: kSecClassGenericPassword,
+ kSecAttrService as String: "WorkbenchReplacement.MySQL",
+ kSecAttrAccount as String: id.uuidString,
+ kSecReturnData as String: true,
+ kSecMatchLimit as String: kSecMatchLimitOne]
+ var value: CFTypeRef?
+ guard SecItemCopyMatching(query as CFDictionary, &value) == errSecSuccess,
+ let data = value as? Data else { return "" }
+ return String(data: data, encoding: .utf8) ?? ""
+ }
+ static func save(_ password: String, id: UUID) {
+ let query: [String: Any] = [kSecClass as String: kSecClassGenericPassword,
+ kSecAttrService as String: "WorkbenchReplacement.MySQL",
+ kSecAttrAccount as String: id.uuidString]
+ SecItemDelete(query as CFDictionary)
+ guard !password.isEmpty else { return }
+ var attributes = query
+ attributes[kSecValueData as String] = Data(password.utf8)
+ SecItemAdd(attributes as CFDictionary, nil)
+ }
+}
+
+@MainActor
+final class DatabaseModel: ObservableObject {
+ @Published var profiles: [ConnectionProfile] = []
+ @Published var selectedID: UUID?
+ @Published var sql = "SELECT VERSION() AS server_version;"
+ @Published var result = QueryResult()
+ @Published var databases: [String] = []
+ @Published var schemaTables: [String: [String]] = [:]
+ @Published var schemaViews: [String: [String]] = [:]
+ @Published var selectedDatabase = ""
+ @Published var isRunning = false
+ @Published var status = "Not connected"
+ private let storageKey = "WorkbenchReplacement.connectionProfiles.native.v1"
+
+ init() {
+ if let data = UserDefaults.standard.data(forKey: storageKey),
+ let decoded = try? JSONDecoder().decode([ConnectionProfile].self, from: data) { profiles = decoded }
+ if profiles.isEmpty { profiles = [ConnectionProfile()] }
+ selectedID = profiles.first?.id
+ }
+ var selected: ConnectionProfile? { profiles.first { $0.id == selectedID } }
+ func save(_ profile: ConnectionProfile, password: String) {
+ if let i = profiles.firstIndex(where: { $0.id == profile.id }) { profiles[i] = profile }
+ else { profiles.append(profile) }
+ PasswordStore.save(password, id: profile.id)
+ selectedID = profile.id
+ persist()
+ }
+ private func persist() {
+ if let data = try? JSONEncoder().encode(profiles) { UserDefaults.standard.set(data, forKey: storageKey) }
+ }
+
+ func formatSQL() {
+ sql = SQLFormatter.uppercaseKeywords(sql)
+ }
+
+
+ func loadTables(database: String) {
+ guard let profile = selected else {
+ status = "Select a MySQL connection"
+ return
+ }
+
+ let password = PasswordStore.read(profile.id)
+ let escaped = database.replacingOccurrences(
+ of: "`",
+ with: "``"
+ )
+
+ status = "Loading tables from \(database)…"
+
+ Task {
+ do {
+ let parsed = try await NativeMySQLRunner.run(
+ profile: profile,
+ database: database,
+ password: password,
+ sql: "SHOW FULL TABLES FROM `\(escaped)`"
+ )
+
+ var tables: [String] = []
+ var views: [String] = []
+
+ for row in parsed.rows {
+ guard let name = row.first else {
+ continue
+ }
+
+ if row.count > 1 && row[1] == "VIEW" {
+ views.append(name)
+ } else {
+ tables.append(name)
+ }
+ }
+
+ schemaTables[database] = tables
+ schemaViews[database] = views
+
+ status = "Loaded \(tables.count) tables and \(views.count) views from \(database)"
+ } catch {
+ status = "Unable to load tables: \(error.localizedDescription)"
+ result = QueryResult(message: error.localizedDescription)
+ }
+ }
+ }
+
+
+
+ func selectTable(database: String, table: String) {
+ let db = database.replacingOccurrences(
+ of: "`",
+ with: "``"
+ )
+
+ let tbl = table.replacingOccurrences(
+ of: "`",
+ with: "``"
+ )
+
+ selectedDatabase = database
+
+ sql = "SELECT * FROM `\(db)`.`\(tbl)` LIMIT 100;"
+
+ runSQL()
+ }
+
+ func loadDatabases() { execute("SHOW DATABASES;", listingDatabases: true) }
+
+ func runSQL() {
+ if UserDefaults.standard.object(
+ forKey: "preferences.uppercaseKeywords"
+ ) as? Bool ?? true {
+ formatSQL()
+ }
+ execute(sql, listingDatabases: false)
+ }
+ private func execute(_ statement: String, listingDatabases: Bool) {
+ guard let profile = selected else { status = "Select a connection"; return }
+ guard !statement.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty else { return }
+ isRunning = true
+ status = "Connecting…"
+ let activeDB = listingDatabases ? "" : (selectedDatabase.isEmpty ? profile.database : selectedDatabase)
+ let password = PasswordStore.read(profile.id)
+ Task {
+ do {
+ let parsed = try await NativeMySQLRunner.run(profile: profile, database: activeDB, password: password, sql: statement)
+ if listingDatabases {
+ databases = parsed.rows.compactMap { $0.first }
+ status = "Connected to \(profile.name)"
+ } else {
+ result = parsed
+ status = "Query completed (\(parsed.rows.count) rows)"
+ }
+ } catch {
+ status = "Error"
+ result = QueryResult(message: error.localizedDescription)
+ }
+ isRunning = false
+ }
+ }
+}
+
+
+// MARK: - SQL Keyword Formatter
+
+enum SQLFormatter {
+
+ private static let keywords: Set = [
+ "SELECT", "FROM", "WHERE", "INSERT", "INTO", "VALUES",
+ "UPDATE", "DELETE", "CREATE", "ALTER", "DROP", "TABLE",
+ "DATABASE", "SCHEMA", "USE", "SHOW", "DESCRIBE", "EXPLAIN",
+ "JOIN", "INNER", "LEFT", "RIGHT", "FULL", "OUTER",
+ "ON", "AS", "AND", "OR", "NOT", "NULL", "IS", "IN",
+ "BETWEEN", "LIKE", "ORDER", "BY", "GROUP", "HAVING",
+ "LIMIT", "OFFSET", "ASC", "DESC", "DISTINCT", "ALL",
+ "PRIMARY", "KEY", "FOREIGN", "REFERENCES", "INDEX",
+ "UNIQUE", "DEFAULT", "AUTO_INCREMENT", "IF", "EXISTS",
+ "CASCADE", "RESTRICT", "SET", "BEGIN", "COMMIT",
+ "ROLLBACK", "TRANSACTION", "UNION", "CASE", "WHEN",
+ "THEN", "ELSE", "END", "TRUE", "FALSE", "COUNT",
+ "SUM", "AVG", "MIN", "MAX", "HAVING", "WITH",
+ "RECURSIVE", "TRUNCATE", "REPLACE", "VIEW",
+ "PROCEDURE", "FUNCTION", "TRIGGER", "GRANT",
+ "REVOKE", "DISTINCT", "CURRENT_TIMESTAMP"
+ ]
+
+ static func uppercaseKeywords(_ sql: String) -> String {
+ let chars = Array(sql)
+ var output = ""
+ var index = 0
+
+ enum State {
+ case normal
+ case singleQuote
+ case doubleQuote
+ case backtick
+ case lineComment
+ case blockComment
+ }
+
+ var state = State.normal
+
+ while index < chars.count {
+ let ch = chars[index]
+ let next: Character? = index + 1 < chars.count
+ ? chars[index + 1] : nil
+
+ switch state {
+ case .normal:
+ if ch == "'" {
+ state = .singleQuote
+ output.append(ch)
+ index += 1
+ } else if ch == "\"" {
+ state = .doubleQuote
+ output.append(ch)
+ index += 1
+ } else if ch == "`" {
+ state = .backtick
+ output.append(ch)
+ index += 1
+ } else if ch == "-" && next == "-" &&
+ (index + 2 == chars.count ||
+ chars[index + 2].isWhitespace) {
+ state = .lineComment
+ output += "--"
+ index += 2
+ } else if ch == "#" {
+ state = .lineComment
+ output.append(ch)
+ index += 1
+ } else if ch == "/" && next == "*" {
+ state = .blockComment
+ output += "/*"
+ index += 2
+ } else if ch.isLetter || ch == "_" {
+ let start = index
+ while index < chars.count &&
+ (chars[index].isLetter ||
+ chars[index].isNumber ||
+ chars[index] == "_" ||
+ chars[index] == "$") {
+ index += 1
+ }
+
+ let word = String(chars[start.. = []
+ @State private var expandedTableFolders: Set = []
+ @State private var selectedTableName: String? = nil
+ @State private var outputExpanded = true
+
var body: some View {
- NavigationSplitView {
- navigator
- .navigationSplitViewColumnWidth(
- min: 220,
- ideal: 270,
- max: 400
- )
- } detail: {
- editorPanel
+ VStack(spacing: 0) {
+ mainToolbar
+
+ Rectangle()
+ .fill(WB.border)
+ .frame(height: 1)
+
+ HSplitView {
+ navigator
+ .frame(
+ minWidth: 210,
+ idealWidth: 265,
+ maxWidth: 420
+ )
+
+ workspace
+ .frame(minWidth: 600)
+ }
+
+ statusBar
}
- .navigationSplitViewStyle(.balanced)
- .frame(minWidth: 1000, minHeight: 650)
+ .frame(minWidth: 1100, minHeight: 720)
+ .preferredColorScheme(
+ UserDefaults.standard.string(
+ forKey: "preferences.appearance"
+ ) == "dark" ? .dark : .light
+ )
+ .background(Color(nsColor: .windowBackgroundColor))
.sheet(isPresented: $showingEditor) {
ConnectionEditor(
profile: $editing,
@@ -33,14 +73,101 @@ struct MainView: View {
}
}
+ // MARK: - Main Toolbar
+
+ private var mainToolbar: some View {
+ HStack(spacing: 16) {
+ Image(systemName: "externaldrive.connected.to.line.below")
+ .font(.system(size: 23))
+ .foregroundStyle(.blue)
+
+ Text("MySQL Workbench")
+ .font(.system(size: 15, weight: .semibold))
+ .foregroundStyle(.black)
+
+ Rectangle()
+ .fill(WB.border)
+ .frame(width: 1, height: 30)
+
+ toolbarButton(
+ "New Connection",
+ icon: "plus.rectangle.on.rectangle"
+ ) {
+ editing = ConnectionProfile(name: "New Connection")
+ editingPassword = ""
+ showingEditor = true
+ }
+
+ toolbarButton(
+ "Refresh",
+ icon: "arrow.clockwise"
+ ) {
+ model.loadDatabases()
+ }
+
+ toolbarButton(
+ "Execute",
+ icon: "bolt.fill"
+ ) {
+ model.runSQL()
+ }
+ .keyboardShortcut(.return, modifiers: [.command])
+
+ toolbarButton(
+ "Format SQL",
+ icon: "text.alignleft"
+ ) {
+ model.formatSQL()
+ }
+
+ Spacer()
+
+ VStack(alignment: .trailing, spacing: 2) {
+ Text(model.selected?.name ?? "No Connection")
+ .font(.system(size: 12, weight: .semibold))
+
+ Text(model.selectedDatabase.isEmpty
+ ? "No default schema"
+ : model.selectedDatabase)
+ .font(.system(size: 11))
+ .foregroundStyle(.secondary)
+ }
+ }
+ .padding(.horizontal, 10)
+ .frame(height: 46)
+ .background(WB.toolbar)
+ }
+
+ private func toolbarButton(
+ _ title: String,
+ icon: String,
+ action: @escaping () -> Void
+ ) -> some View {
+ Button(action: action) {
+ VStack(spacing: 3) {
+ Image(systemName: icon)
+ .font(.system(size: 19))
+ .frame(height: 23)
+
+ Text(title)
+ .font(.system(size: 10))
+ }
+ .foregroundStyle(.black)
+ .frame(minWidth: 65)
+ }
+ .buttonStyle(.plain)
+ .help(title)
+ }
+
// MARK: - Navigator
private var navigator: some View {
VStack(spacing: 0) {
+ sectionHeader("NAVIGATOR")
+
HStack {
- Text("NAVIGATOR")
+ Text("SCHEMAS")
.font(.system(size: 11, weight: .bold))
- .foregroundStyle(.secondary)
Spacer()
@@ -50,21 +177,48 @@ struct MainView: View {
Image(systemName: "arrow.clockwise")
}
.buttonStyle(.borderless)
- .help("Refresh schemas")
}
- .padding(12)
+ .padding(.horizontal, 12)
+ .frame(height: 34)
Divider()
- List(selection: $model.selectedID) {
+ ScrollView {
+ LazyVStack(alignment: .leading, spacing: 2) {
+ ForEach(model.databases, id: \.self) { db in
+ schemaNode(db)
+ }
+ }
+ .padding(.vertical, 8)
+ }
- Section("MYSQL CONNECTIONS") {
+ Divider()
+
+ sectionHeader("MYSQL CONNECTIONS")
+
+ ScrollView {
+ VStack(alignment: .leading, spacing: 4) {
ForEach(model.profiles) { profile in
- Label(
- profile.name,
- systemImage: "server.rack"
- )
- .tag(profile.id)
+ Button {
+ model.selectedID = profile.id
+ model.loadDatabases()
+ } label: {
+ Label(
+ profile.name,
+ systemImage: "server.rack"
+ )
+ .frame(
+ maxWidth: .infinity,
+ alignment: .leading
+ )
+ .padding(6)
+ .background(
+ model.selectedID == profile.id
+ ? Color.blue.opacity(0.14)
+ : Color.clear
+ )
+ }
+ .buttonStyle(.plain)
.contextMenu {
Button("Edit Connection") {
edit(profile)
@@ -72,324 +226,458 @@ struct MainView: View {
}
}
}
+ .padding(8)
+ }
+ .frame(height: 125)
+ }
+ .background(WB.sidebar)
+ }
- Section("SCHEMAS") {
- ForEach(model.databases, id: \.self) { db in
- DisclosureGroup {
- schemaContents(db)
- } label: {
- Label(
- db,
- systemImage: "cylinder.split.1x2"
- )
- .fontWeight(
- model.selectedDatabase == db
- ? .semibold : .regular
- )
- .contextMenu {
- Button("Set as Default Schema") {
- model.selectedDatabase = db
- }
- Button("Refresh Tables") {
- model.loadTables(database: db)
- }
- }
- .onTapGesture {
- model.selectedDatabase = db
- if model.schemaTables[db] == nil {
- model.loadTables(database: db)
- }
- }
- }
+ // MARK: - Compact Schema Tree
+
+ private func schemaNode(_ db: String) -> some View {
+ VStack(alignment: .leading, spacing: 0) {
+
+ HStack(spacing: 5) {
+ Image(systemName:
+ expandedSchemas.contains(db)
+ ? "chevron.down"
+ : "chevron.right"
+ )
+ .font(.system(size: 10))
+ .foregroundStyle(.secondary)
+ .frame(width: 13)
+
+ Image(systemName: "cylinder.split.1x2")
+ .foregroundStyle(.blue)
+ .frame(width: 17)
+
+ Text(db)
+ .font(.system(size: 12, weight:
+ model.selectedDatabase == db
+ ? .semibold : .regular))
+ .lineLimit(1)
+
+ Spacer(minLength: 0)
+ }
+ .frame(height: 25)
+ .padding(.leading, 10)
+ .contentShape(Rectangle())
+ .onTapGesture {
+ toggleSchema(db)
+ }
+ .onTapGesture(count: 2) {
+ model.selectedDatabase = db
+ expandedSchemas.insert(db)
+ expandedTableFolders.insert(db)
+
+ if model.schemaTables[db] == nil {
+ model.loadTables(database: db)
+ }
+ }
+ .contextMenu {
+ Button("Set as Default Schema") {
+ model.selectedDatabase = db
+ }
+
+ Button("Refresh Tables") {
+ model.loadTables(database: db)
+ }
+ }
+
+ if expandedSchemas.contains(db) {
+ schemaChildren(db)
+ }
+ }
+ .frame(maxWidth: .infinity, alignment: .leading)
+ }
+
+ private func toggleSchema(_ db: String) {
+ if expandedSchemas.contains(db) {
+ expandedSchemas.remove(db)
+ } else {
+ expandedSchemas.insert(db)
+ expandedTableFolders.insert(db)
+
+ if model.schemaTables[db] == nil {
+ model.loadTables(database: db)
+ }
+ }
+ }
+
+ private func schemaChildren(_ db: String) -> some View {
+ VStack(alignment: .leading, spacing: 0) {
+
+ // Tables folder
+ HStack(spacing: 5) {
+ Image(systemName:
+ expandedTableFolders.contains(db)
+ ? "chevron.down"
+ : "chevron.right"
+ )
+ .font(.system(size: 10))
+ .foregroundStyle(.secondary)
+ .frame(width: 13)
+
+ Image(systemName: "folder")
+ .foregroundStyle(.secondary)
+ .frame(width: 17)
+
+ Text("Tables")
+ .font(.system(size: 12))
+
+ Spacer(minLength: 0)
+ }
+ .frame(height: 24)
+ .padding(.leading, 28)
+ .contentShape(Rectangle())
+ .onTapGesture {
+ if expandedTableFolders.contains(db) {
+ expandedTableFolders.remove(db)
+ } else {
+ expandedTableFolders.insert(db)
+
+ if model.schemaTables[db] == nil {
+ model.loadTables(database: db)
}
}
}
- .listStyle(.sidebar)
+
+ // Tables
+ if expandedTableFolders.contains(db) {
+ if let tables = model.schemaTables[db] {
+ ForEach(tables, id: \.self) { table in
+ tableNode(database: db, table: table)
+ }
+ } else {
+ Button("Load Tables") {
+ model.loadTables(database: db)
+ }
+ .font(.system(size: 11))
+ .buttonStyle(.borderless)
+ .padding(.leading, 66)
+ .frame(height: 24)
+ }
+ }
+
+ folderNode("Views", indent: 28)
+ folderNode("Stored Procedures", indent: 28)
+ folderNode("Functions", indent: 28)
+ }
+ .frame(maxWidth: .infinity, alignment: .leading)
+ }
+
+ private func tableNode(database: String, table: String) -> some View {
+ let identifier = database + "." + table
+ let isSelected = selectedTableName == identifier
+
+ return HStack(spacing: 5) {
+ Image(systemName: "tablecells")
+ .font(.system(size: 11))
+ .foregroundStyle(
+ isSelected ? Color.blue : Color.gray
+ )
+ .frame(width: 17)
+
+ Text(table)
+ .font(.system(size: 12))
+ .lineLimit(1)
+ .truncationMode(.middle)
+
+ Spacer(minLength: 0)
+ }
+ .frame(height: 23)
+ .padding(.leading, 49)
+ .frame(maxWidth: .infinity, alignment: .leading)
+ .background(
+ isSelected
+ ? Color.blue.opacity(0.14)
+ : Color.clear
+ )
+ .contentShape(Rectangle())
+ .onTapGesture {
+ selectedTableName = identifier
+ }
+ .onTapGesture(count: 2) {
+ selectedTableName = identifier
+ model.selectTable(
+ database: database,
+ table: table
+ )
+ }
+ .contextMenu {
+ Button("Select First 100 Rows") {
+ selectedTableName = identifier
+ model.selectTable(
+ database: database,
+ table: table
+ )
+ }
+ }
+ }
+
+ private func folderNode(
+ _ title: String,
+ indent: CGFloat
+ ) -> some View {
+ HStack(spacing: 5) {
+ Image(systemName: "chevron.right")
+ .font(.system(size: 10))
+ .foregroundStyle(.secondary)
+ .frame(width: 13)
+
+ Image(systemName: "folder")
+ .foregroundStyle(.secondary)
+ .frame(width: 17)
+
+ Text(title)
+ .font(.system(size: 12))
+
+ Spacer(minLength: 0)
+ }
+ .frame(height: 24)
+ .padding(.leading, indent)
+ .frame(maxWidth: .infinity, alignment: .leading)
+ }
+
+ // MARK: - Workspace
+
+ private var workspace: some View {
+ VStack(spacing: 0) {
+ queryTabBar
Divider()
- HStack {
- Button {
- editing = ConnectionProfile(
- name: "New Connection"
- )
- editingPassword = ""
- showingEditor = true
- } label: {
- Label(
- "New Connection",
- systemImage: "plus"
- )
- }
- .buttonStyle(.borderless)
-
- Spacer()
- }
- .padding(10)
- }
- }
-
- @ViewBuilder
- private func schemaContents(_ database: String) -> some View {
- if let tables = model.schemaTables[database] {
- if tables.isEmpty {
- Text("No tables")
- .foregroundStyle(.secondary)
- } else {
- ForEach(tables, id: \.self) { table in
- Label(table, systemImage: "tablecells")
- .font(.system(size: 12))
- .onTapGesture(count: 2) {
- model.selectTable(
- database: database,
- table: table
- )
- }
- .contextMenu {
- Button("Select First 100 Rows") {
- model.selectTable(
- database: database,
- table: table
- )
- }
- }
- }
- }
- } else {
- Button("Load Tables") {
- model.loadTables(database: database)
- }
- .buttonStyle(.borderless)
- }
- }
-
- // MARK: - SQL Editor
-
- private var editorPanel: some View {
- VStack(spacing: 0) {
- editorToolbar
+ queryToolbar
Divider()
VSplitView {
sqlEditor
- .frame(minHeight: 180)
+ .frame(minHeight: 230)
- resultsPanel
- .frame(minHeight: 180)
+ bottomWorkspace
+ .frame(minHeight: 230)
}
-
- Divider()
-
- statusBar
}
- .navigationTitle("WorkbenchReplacement")
}
- private var editorToolbar: some View {
+ private var queryTabBar: some View {
+ HStack(spacing: 0) {
+ HStack(spacing: 8) {
+ Image(systemName: "doc.text")
+ .foregroundStyle(.blue)
+
+ Text("Query 1")
+ .font(.system(size: 12, weight: .semibold))
+
+ Image(systemName: "xmark")
+ .font(.system(size: 9))
+ .foregroundStyle(.secondary)
+ }
+ .padding(.horizontal, 10)
+ .frame(height: 29)
+ .background(Color(nsColor: .textBackgroundColor))
+
+ Spacer()
+ }
+ .background(WB.header)
+ }
+
+ private var queryToolbar: some View {
HStack(spacing: 14) {
-
- Label(
- "SQL Editor",
- systemImage:
- "chevron.left.forwardslash.chevron.right"
- )
- .font(.headline)
-
- Divider()
- .frame(height: 20)
-
Button {
model.runSQL()
} label: {
- Label(
- "Execute",
- systemImage: "bolt.fill"
- )
+ Image(systemName: "bolt.fill")
+ .foregroundStyle(.orange)
}
- .keyboardShortcut(
- .return,
- modifiers: [.command]
- )
- .disabled(model.isRunning)
+ .help("Execute SQL")
Button {
model.formatSQL()
} label: {
- Label(
- "Format SQL",
- systemImage: "text.alignleft"
- )
+ Image(systemName: "text.alignleft")
}
+ .help("Format SQL")
+
+ Divider()
+ .frame(height: 17)
+
+ Text("Default Schema:")
+ .font(.system(size: 11))
+
+ Text(model.selectedDatabase.isEmpty
+ ? "None"
+ : model.selectedDatabase)
+ .font(.system(size: 11, weight: .semibold))
Spacer()
- if !model.selectedDatabase.isEmpty {
- Label(
- model.selectedDatabase,
- systemImage: "cylinder"
- )
+ Text("SQL Editor")
+ .font(.system(size: 11))
.foregroundStyle(.secondary)
- }
-
- Button {
- model.loadDatabases()
- } label: {
- Image(systemName: "arrow.clockwise")
- }
- .help("Refresh schemas")
}
.buttonStyle(.borderless)
- .padding(12)
+ .padding(.horizontal, 12)
+ .frame(height: 30)
+ .background(WB.header)
}
private var sqlEditor: some View {
- VStack(alignment: .leading, spacing: 0) {
- HStack {
- Image(systemName: "doc.text")
- Text("Query 1")
- .fontWeight(.medium)
+ HStack(spacing: 0) {
+ if UserDefaults.standard.object(
+ forKey: "preferences.showLineNumbers"
+ ) as? Bool ?? true {
+ lineNumbers
- Spacer()
-
- Text("MySQL")
- .foregroundStyle(.secondary)
+ Rectangle()
+ .fill(WB.border)
+ .frame(width: 1)
}
- .font(.system(size: 12))
- .padding(.horizontal, 12)
- .padding(.vertical, 8)
- Divider()
+ Rectangle()
+ .fill(WB.border)
+ .frame(width: 1)
- TextEditor(text: $model.sql)
- .font(
- .system(
- size: 14,
- design: .monospaced
- )
- )
- .scrollContentBackground(.hidden)
- .padding(12)
- .background(
- Color(nsColor: .textBackgroundColor)
- )
+ SQLTextView(text: $model.sql)
+ }
+ .background(Color(nsColor: .textBackgroundColor))
+ }
+
+ private var lineNumbers: some View {
+ ScrollView {
+ VStack(alignment: .trailing, spacing: 0) {
+ ForEach(
+ 1...max(1, model.sql.components(
+ separatedBy: "\n"
+ ).count),
+ id: \.self
+ ) { number in
+ Text("\(number)")
+ .frame(height: 19)
+ }
+ }
+ .padding(.horizontal, 9)
+ .padding(.top, 8)
+ }
+ .font(.system(size: 12, design: .monospaced))
+ .foregroundStyle(.secondary)
+ .frame(width: 48)
+ .background(WB.header)
+ .allowsHitTesting(false)
+ }
+
+ // MARK: - Results and Action Output
+
+ private var bottomWorkspace: some View {
+ VSplitView {
+ resultsPanel
+ .frame(minHeight: 230)
+
+ if outputExpanded {
+ actionOutput
+ .frame(minHeight: 90)
+ }
}
}
- // MARK: - Results
-
private var resultsPanel: some View {
VStack(spacing: 0) {
-
HStack {
- Label(
- "Result Grid",
- systemImage: "tablecells"
- )
- .font(.headline)
+ Image(systemName: "tablecells")
+ Text("Result Grid")
+ .fontWeight(.semibold)
Spacer()
Text(model.result.message)
- .font(.caption)
.foregroundStyle(.secondary)
}
- .padding(12)
+ .font(.system(size: 12))
+ .padding(.horizontal, 12)
+ .frame(height: 28)
+ .background(WB.header)
Divider()
if model.result.columns.isEmpty {
- ContentUnavailableView(
- "No Results",
- systemImage: "tablecells",
- description: Text(
- "Execute a SQL query to display results."
- )
- )
+ VStack {
+ Spacer()
+ Text("No query results")
+ .foregroundStyle(.secondary)
+ Spacer()
+ }
} else {
- resultsGrid
+ resultGrid
}
}
}
- private var resultsGrid: some View {
- ScrollView([.horizontal, .vertical]) {
- Grid(
- alignment: .leading,
- horizontalSpacing: 0,
- verticalSpacing: 0
- ) {
- GridRow {
- ForEach(
- model.result.columns.indices,
- id: \.self
- ) { index in
- Text(model.result.columns[index])
- .fontWeight(.semibold)
- .frame(
- minWidth: 150,
- alignment: .leading
- )
- .padding(8)
- .background(
- Color(nsColor:
- .controlBackgroundColor)
- )
- }
+
+ private var resultGrid: some View {
+ NativeResultGrid(
+ columns: model.result.columns,
+ rows: model.result.rows
+ )
+ }
+
+ private var actionOutput: some View {
+ VStack(spacing: 0) {
+ HStack {
+ Image(systemName: "list.bullet.rectangle")
+ Text("Action Output")
+ .fontWeight(.semibold)
+
+ Spacer()
+
+ Button {
+ outputExpanded = false
+ } label: {
+ Image(systemName: "chevron.down")
+ }
+ .buttonStyle(.borderless)
+ }
+ .font(.system(size: 12))
+ .padding(.horizontal, 12)
+ .frame(height: 26)
+ .background(WB.header)
+
+ Divider()
+
+ HStack(alignment: .top, spacing: 12) {
+ Image(systemName:
+ model.status == "Error"
+ ? "xmark.circle.fill"
+ : "checkmark.circle.fill"
+ )
+ .foregroundStyle(
+ model.status == "Error"
+ ? .red : .green
+ )
+
+ VStack(alignment: .leading, spacing: 5) {
+ Text(model.status)
+ .fontWeight(.semibold)
+
+ Text(model.result.message)
+ .foregroundStyle(.secondary)
}
- ForEach(
- model.result.rows.indices,
- id: \.self
- ) { rowIndex in
- resultRow(
- model.result.rows[rowIndex],
- index: rowIndex
- )
- }
+ Spacer()
}
+ .font(.system(size: 12))
+ .padding(12)
+
+ Spacer()
}
}
- private func resultRow(
- _ row: [String],
- index: Int
- ) -> some View {
- GridRow {
- ForEach(
- model.result.columns.indices,
- id: \.self
- ) { columnIndex in
- Text(
- columnIndex < row.count
- ? row[columnIndex] : ""
- )
- .font(
- .system(
- size: 12,
- design: .monospaced
- )
- )
- .frame(
- minWidth: 150,
- alignment: .leading
- )
- .padding(8)
- .background(
- index.isMultiple(of: 2)
- ? Color.clear
- : Color(nsColor:
- .alternatingContentBackgroundColors[0])
- )
- }
- }
- }
-
- // MARK: - Status
+ // MARK: - Status Bar
private var statusBar: some View {
- HStack {
+ HStack(spacing: 10) {
Circle()
.fill(
model.status == "Error"
@@ -402,10 +690,37 @@ struct MainView: View {
Spacer()
- Text(model.selected?.name ?? "No connection")
+ Text(model.selected?.name ?? "Disconnected")
+
+ Divider()
+ .frame(height: 13)
+
+ Text(model.selectedDatabase.isEmpty
+ ? "No Schema"
+ : model.selectedDatabase)
+
+ Divider()
+ .frame(height: 13)
+
+ Text("UTF-8")
}
- .font(.caption)
- .padding(9)
+ .font(.system(size: 11))
+ .padding(.horizontal, 12)
+ .frame(height: 22)
+ .background(WB.header)
+ }
+
+ private func sectionHeader(_ title: String) -> some View {
+ HStack {
+ Text(title)
+ .font(.system(size: 11, weight: .bold))
+ .foregroundStyle(.secondary)
+
+ Spacer()
+ }
+ .padding(.horizontal, 12)
+ .frame(height: 30)
+ .background(WB.header)
}
private func edit(_ profile: ConnectionProfile) {
@@ -415,10 +730,144 @@ struct MainView: View {
}
}
+// MARK: - Native SQL Text Editor
+
+struct SQLTextView: NSViewRepresentable {
+ @Binding var text: String
+
+ func makeCoordinator() -> Coordinator {
+ Coordinator(self)
+ }
+
+ func makeNSView(context: Context) -> NSScrollView {
+ let scroll = NSTextView.scrollableTextView()
+ let editor = scroll.documentView as! NSTextView
+
+ scroll.appearance = NSAppearance(named: .aqua)
+ editor.appearance = NSAppearance(named: .aqua)
+ editor.backgroundColor = .white
+ editor.textColor = .black
+ scroll.backgroundColor = .white
+
+ editor.isEditable = true
+ editor.isRichText = false
+ editor.allowsUndo = true
+ editor.isAutomaticQuoteSubstitutionEnabled = false
+ editor.isAutomaticDashSubstitutionEnabled = false
+ editor.font = NSFont.monospacedSystemFont(
+ ofSize: CGFloat(
+ UserDefaults.standard.object(
+ forKey: "preferences.editorFontSize"
+ ) as? Double ?? 13
+ ),
+ weight: .regular
+ )
+ editor.textContainerInset = NSSize(
+ width: 10,
+ height: 8
+ )
+ editor.delegate = context.coordinator
+ editor.string = text
+
+ context.coordinator.highlight(editor)
+
+ return scroll
+ }
+
+ func updateNSView(
+ _ scroll: NSScrollView,
+ context: Context
+ ) {
+ guard let editor = scroll.documentView as? NSTextView else {
+ return
+ }
+
+ if editor.string != text {
+ let selection = editor.selectedRange()
+ editor.string = text
+
+ let safeLocation = min(
+ selection.location,
+ (text as NSString).length
+ )
+
+ editor.setSelectedRange(
+ NSRange(location: safeLocation, length: 0)
+ )
+
+ context.coordinator.highlight(editor)
+ }
+ }
+
+ final class Coordinator: NSObject, NSTextViewDelegate {
+ var parent: SQLTextView
+
+ init(_ parent: SQLTextView) {
+ self.parent = parent
+ }
+
+ func textDidChange(_ notification: Notification) {
+ guard let editor = notification.object as? NSTextView else {
+ return
+ }
+
+ parent.text = editor.string
+ highlight(editor)
+ }
+
+ func highlight(_ editor: NSTextView) {
+ guard let storage = editor.textStorage else {
+ return
+ }
+
+ let text = editor.string as NSString
+ let range = NSRange(
+ location: 0,
+ length: text.length
+ )
+
+ storage.beginEditing()
+
+ storage.setAttributes([
+ .foregroundColor: NSColor.textColor,
+ .font: NSFont.monospacedSystemFont(
+ ofSize: CGFloat(
+ UserDefaults.standard.object(
+ forKey: "preferences.editorFontSize"
+ ) as? Double ?? 13
+ ),
+ weight: .regular
+ )
+ ], range: range)
+
+ let pattern = """
+ \\b(SELECT|FROM|WHERE|INSERT|UPDATE|DELETE|CREATE|TABLE|DATABASE|SHOW|USE|JOIN|LEFT|RIGHT|INNER|OUTER|ON|AS|AND|OR|ORDER|GROUP|BY|LIMIT|VALUES|SET|NULL|NOT|IN|LIKE|DISTINCT|ALTER|DROP|DESCRIBE|EXPLAIN)\\b
+ """
+
+ if let regex = try? NSRegularExpression(
+ pattern: pattern,
+ options: [.caseInsensitive]
+ ) {
+ for match in regex.matches(
+ in: editor.string,
+ range: range
+ ) {
+ storage.addAttribute(
+ .foregroundColor,
+ value: NSColor.systemBlue,
+ range: match.range
+ )
+ }
+ }
+
+ storage.endEditing()
+ }
+ }
+}
+
// MARK: - Connection Editor
struct ConnectionEditor: View {
-
@Binding var profile: ConnectionProfile
@Binding var password: String
@@ -454,7 +903,7 @@ struct ConnectionEditor: View {
)
SecureField(
- "Password (stored in macOS Keychain)",
+ "Password (macOS Keychain)",
text: $password
)
@@ -464,12 +913,6 @@ struct ConnectionEditor: View {
)
}
- Text(
- "Native MySQL connection. Local TCP connections and SSH tunnels are supported."
- )
- .font(.caption)
- .foregroundStyle(.secondary)
-
HStack {
Spacer()
@@ -487,3 +930,184 @@ struct ConnectionEditor: View {
.frame(width: 520)
}
}
+
+
+
+// MARK: - High Performance Native Result Grid
+
+struct NativeResultGrid: NSViewRepresentable {
+
+ let columns: [String]
+ let rows: [[String]]
+
+ func makeCoordinator() -> Coordinator {
+ Coordinator()
+ }
+
+ func makeNSView(context: Context) -> NSScrollView {
+ let scroll = NSScrollView()
+ scroll.hasVerticalScroller = true
+ scroll.hasHorizontalScroller = true
+ scroll.autohidesScrollers = true
+ scroll.drawsBackground = true
+ scroll.backgroundColor = .white
+
+ let table = NSTableView()
+ table.headerView = NSTableHeaderView()
+ table.usesAlternatingRowBackgroundColors =
+ UserDefaults.standard.object(
+ forKey: "preferences.alternatingRows"
+ ) as? Bool ?? true
+ table.rowHeight = CGFloat(
+ UserDefaults.standard.object(
+ forKey: "preferences.resultRowHeight"
+ ) as? Double ?? 24
+ )
+ table.intercellSpacing = NSSize(width: 1, height: 1)
+ table.allowsMultipleSelection = true
+ table.allowsColumnResizing = true
+ table.allowsColumnReordering = true
+ table.columnAutoresizingStyle = .noColumnAutoresizing
+ table.backgroundColor = .white
+
+ context.coordinator.columns = columns
+ context.coordinator.rows = rows
+
+ table.delegate = context.coordinator
+ table.dataSource = context.coordinator
+
+ configureColumns(table)
+
+ scroll.documentView = table
+ table.reloadData()
+
+ return scroll
+ }
+
+ func updateNSView(
+ _ scroll: NSScrollView,
+ context: Context
+ ) {
+ guard let table = scroll.documentView as? NSTableView else {
+ return
+ }
+
+ let oldColumns = context.coordinator.columns
+
+ context.coordinator.columns = columns
+ context.coordinator.rows = rows
+
+ if oldColumns != columns {
+ configureColumns(table)
+ }
+
+ table.reloadData()
+ }
+
+ private func configureColumns(_ table: NSTableView) {
+ for column in Array(table.tableColumns) {
+ table.removeTableColumn(column)
+ }
+
+ for (index, name) in columns.enumerated() {
+ let identifier = NSUserInterfaceItemIdentifier(
+ "column_\(index)"
+ )
+
+ let column = NSTableColumn(identifier: identifier)
+ column.title = name
+ column.width = CGFloat(
+ UserDefaults.standard.object(
+ forKey: "preferences.resultColumnWidth"
+ ) as? Double ?? 170
+ )
+ column.minWidth = 70
+ column.maxWidth = 700
+ column.resizingMask = .userResizingMask
+
+ table.addTableColumn(column)
+ }
+ }
+
+ final class Coordinator: NSObject,
+ NSTableViewDataSource,
+ NSTableViewDelegate {
+
+ var columns: [String] = []
+ var rows: [[String]] = []
+
+ func numberOfRows(in tableView: NSTableView) -> Int {
+ rows.count
+ }
+
+ func tableView(
+ _ tableView: NSTableView,
+ viewFor tableColumn: NSTableColumn?,
+ row: Int
+ ) -> NSView? {
+
+ guard let tableColumn,
+ row >= 0,
+ row < rows.count else {
+ return nil
+ }
+
+ let identifier = tableColumn.identifier.rawValue
+
+ guard identifier.hasPrefix("column_"),
+ let columnIndex = Int(identifier.dropFirst(7)),
+ columnIndex < columns.count else {
+ return nil
+ }
+
+ let cellIdentifier =
+ NSUserInterfaceItemIdentifier("ResultCell")
+
+ let cell: NSTableCellView
+
+ if let reused = tableView.makeView(
+ withIdentifier: cellIdentifier,
+ owner: nil
+ ) as? NSTableCellView {
+ cell = reused
+ } else {
+ cell = NSTableCellView()
+ cell.identifier = cellIdentifier
+
+ let field = NSTextField(labelWithString: "")
+ field.font = NSFont.monospacedSystemFont(
+ ofSize: 12,
+ weight: .regular
+ )
+ field.lineBreakMode = .byTruncatingTail
+ field.translatesAutoresizingMaskIntoConstraints = false
+
+ cell.addSubview(field)
+ cell.textField = field
+
+ NSLayoutConstraint.activate([
+ field.leadingAnchor.constraint(
+ equalTo: cell.leadingAnchor,
+ constant: 7
+ ),
+ field.trailingAnchor.constraint(
+ equalTo: cell.trailingAnchor,
+ constant: -7
+ ),
+ field.centerYAnchor.constraint(
+ equalTo: cell.centerYAnchor
+ )
+ ])
+ }
+
+ let values = rows[row]
+
+ cell.textField?.stringValue =
+ columnIndex < values.count
+ ? values[columnIndex]
+ : ""
+
+ return cell
+ }
+ }
+}
diff --git a/WorkbenchReplacement/MainView.swift.backup-20261016-170516 b/WorkbenchReplacement/MainView.swift.backup-20261016-170516
new file mode 100644
index 0000000..55e9d80
--- /dev/null
+++ b/WorkbenchReplacement/MainView.swift.backup-20261016-170516
@@ -0,0 +1,489 @@
+
+import SwiftUI
+
+struct MainView: View {
+
+ @EnvironmentObject private var model: DatabaseModel
+
+ @State private var showingEditor = false
+ @State private var editing = ConnectionProfile()
+ @State private var editingPassword = ""
+
+ var body: some View {
+ NavigationSplitView {
+ navigator
+ .navigationSplitViewColumnWidth(
+ min: 220,
+ ideal: 270,
+ max: 400
+ )
+ } detail: {
+ editorPanel
+ }
+ .navigationSplitViewStyle(.balanced)
+ .frame(minWidth: 1000, minHeight: 650)
+ .sheet(isPresented: $showingEditor) {
+ ConnectionEditor(
+ profile: $editing,
+ password: $editingPassword
+ ) {
+ model.save(editing, password: editingPassword)
+ showingEditor = false
+ }
+ }
+ }
+
+ // MARK: - Navigator
+
+ private var navigator: some View {
+ VStack(spacing: 0) {
+ HStack {
+ Text("NAVIGATOR")
+ .font(.system(size: 11, weight: .bold))
+ .foregroundStyle(.secondary)
+
+ Spacer()
+
+ Button {
+ model.loadDatabases()
+ } label: {
+ Image(systemName: "arrow.clockwise")
+ }
+ .buttonStyle(.borderless)
+ .help("Refresh schemas")
+ }
+ .padding(12)
+
+ Divider()
+
+ List(selection: $model.selectedID) {
+
+ Section("MYSQL CONNECTIONS") {
+ ForEach(model.profiles) { profile in
+ Label(
+ profile.name,
+ systemImage: "server.rack"
+ )
+ .tag(profile.id)
+ .contextMenu {
+ Button("Edit Connection") {
+ edit(profile)
+ }
+ }
+ }
+ }
+
+ Section("SCHEMAS") {
+ ForEach(model.databases, id: \.self) { db in
+ DisclosureGroup {
+ schemaContents(db)
+ } label: {
+ Label(
+ db,
+ systemImage: "cylinder.split.1x2"
+ )
+ .fontWeight(
+ model.selectedDatabase == db
+ ? .semibold : .regular
+ )
+ .contextMenu {
+ Button("Set as Default Schema") {
+ model.selectedDatabase = db
+ }
+
+ Button("Refresh Tables") {
+ model.loadTables(database: db)
+ }
+ }
+ .onTapGesture {
+ model.selectedDatabase = db
+ if model.schemaTables[db] == nil {
+ model.loadTables(database: db)
+ }
+ }
+ }
+ }
+ }
+ }
+ .listStyle(.sidebar)
+
+ Divider()
+
+ HStack {
+ Button {
+ editing = ConnectionProfile(
+ name: "New Connection"
+ )
+ editingPassword = ""
+ showingEditor = true
+ } label: {
+ Label(
+ "New Connection",
+ systemImage: "plus"
+ )
+ }
+ .buttonStyle(.borderless)
+
+ Spacer()
+ }
+ .padding(10)
+ }
+ }
+
+ @ViewBuilder
+ private func schemaContents(_ database: String) -> some View {
+ if let tables = model.schemaTables[database] {
+ if tables.isEmpty {
+ Text("No tables")
+ .foregroundStyle(.secondary)
+ } else {
+ ForEach(tables, id: \.self) { table in
+ Label(table, systemImage: "tablecells")
+ .font(.system(size: 12))
+ .onTapGesture(count: 2) {
+ model.selectTable(
+ database: database,
+ table: table
+ )
+ }
+ .contextMenu {
+ Button("Select First 100 Rows") {
+ model.selectTable(
+ database: database,
+ table: table
+ )
+ }
+ }
+ }
+ }
+ } else {
+ Button("Load Tables") {
+ model.loadTables(database: database)
+ }
+ .buttonStyle(.borderless)
+ }
+ }
+
+ // MARK: - SQL Editor
+
+ private var editorPanel: some View {
+ VStack(spacing: 0) {
+ editorToolbar
+
+ Divider()
+
+ VSplitView {
+ sqlEditor
+ .frame(minHeight: 180)
+
+ resultsPanel
+ .frame(minHeight: 180)
+ }
+
+ Divider()
+
+ statusBar
+ }
+ .navigationTitle("WorkbenchReplacement")
+ }
+
+ private var editorToolbar: some View {
+ HStack(spacing: 14) {
+
+ Label(
+ "SQL Editor",
+ systemImage:
+ "chevron.left.forwardslash.chevron.right"
+ )
+ .font(.headline)
+
+ Divider()
+ .frame(height: 20)
+
+ Button {
+ model.runSQL()
+ } label: {
+ Label(
+ "Execute",
+ systemImage: "bolt.fill"
+ )
+ }
+ .keyboardShortcut(
+ .return,
+ modifiers: [.command]
+ )
+ .disabled(model.isRunning)
+
+ Button {
+ model.formatSQL()
+ } label: {
+ Label(
+ "Format SQL",
+ systemImage: "text.alignleft"
+ )
+ }
+
+ Spacer()
+
+ if !model.selectedDatabase.isEmpty {
+ Label(
+ model.selectedDatabase,
+ systemImage: "cylinder"
+ )
+ .foregroundStyle(.secondary)
+ }
+
+ Button {
+ model.loadDatabases()
+ } label: {
+ Image(systemName: "arrow.clockwise")
+ }
+ .help("Refresh schemas")
+ }
+ .buttonStyle(.borderless)
+ .padding(12)
+ }
+
+ private var sqlEditor: some View {
+ VStack(alignment: .leading, spacing: 0) {
+ HStack {
+ Image(systemName: "doc.text")
+ Text("Query 1")
+ .fontWeight(.medium)
+
+ Spacer()
+
+ Text("MySQL")
+ .foregroundStyle(.secondary)
+ }
+ .font(.system(size: 12))
+ .padding(.horizontal, 12)
+ .padding(.vertical, 8)
+
+ Divider()
+
+ TextEditor(text: $model.sql)
+ .font(
+ .system(
+ size: 14,
+ design: .monospaced
+ )
+ )
+ .scrollContentBackground(.hidden)
+ .padding(12)
+ .background(
+ Color(nsColor: .textBackgroundColor)
+ )
+ }
+ }
+
+ // MARK: - Results
+
+ private var resultsPanel: some View {
+ VStack(spacing: 0) {
+
+ HStack {
+ Label(
+ "Result Grid",
+ systemImage: "tablecells"
+ )
+ .font(.headline)
+
+ Spacer()
+
+ Text(model.result.message)
+ .font(.caption)
+ .foregroundStyle(.secondary)
+ }
+ .padding(12)
+
+ Divider()
+
+ if model.result.columns.isEmpty {
+ ContentUnavailableView(
+ "No Results",
+ systemImage: "tablecells",
+ description: Text(
+ "Execute a SQL query to display results."
+ )
+ )
+ } else {
+ resultsGrid
+ }
+ }
+ }
+
+ private var resultsGrid: some View {
+ ScrollView([.horizontal, .vertical]) {
+ Grid(
+ alignment: .leading,
+ horizontalSpacing: 0,
+ verticalSpacing: 0
+ ) {
+ GridRow {
+ ForEach(
+ model.result.columns.indices,
+ id: \.self
+ ) { index in
+ Text(model.result.columns[index])
+ .fontWeight(.semibold)
+ .frame(
+ minWidth: 150,
+ alignment: .leading
+ )
+ .padding(8)
+ .background(
+ Color(nsColor:
+ .controlBackgroundColor)
+ )
+ }
+ }
+
+ ForEach(
+ model.result.rows.indices,
+ id: \.self
+ ) { rowIndex in
+ resultRow(
+ model.result.rows[rowIndex],
+ index: rowIndex
+ )
+ }
+ }
+ }
+ }
+
+ private func resultRow(
+ _ row: [String],
+ index: Int
+ ) -> some View {
+ GridRow {
+ ForEach(
+ model.result.columns.indices,
+ id: \.self
+ ) { columnIndex in
+ Text(
+ columnIndex < row.count
+ ? row[columnIndex] : ""
+ )
+ .font(
+ .system(
+ size: 12,
+ design: .monospaced
+ )
+ )
+ .frame(
+ minWidth: 150,
+ alignment: .leading
+ )
+ .padding(8)
+ .background(
+ index.isMultiple(of: 2)
+ ? Color.clear
+ : Color(nsColor:
+ .alternatingContentBackgroundColors[0])
+ )
+ }
+ }
+ }
+
+ // MARK: - Status
+
+ private var statusBar: some View {
+ HStack {
+ Circle()
+ .fill(
+ model.status == "Error"
+ ? Color.red : Color.green
+ )
+ .frame(width: 8, height: 8)
+
+ Text(model.status)
+ .lineLimit(1)
+
+ Spacer()
+
+ Text(model.selected?.name ?? "No connection")
+ }
+ .font(.caption)
+ .padding(9)
+ }
+
+ private func edit(_ profile: ConnectionProfile) {
+ editing = profile
+ editingPassword = PasswordStore.read(profile.id)
+ showingEditor = true
+ }
+}
+
+// MARK: - Connection Editor
+
+struct ConnectionEditor: View {
+
+ @Binding var profile: ConnectionProfile
+ @Binding var password: String
+
+ let save: () -> Void
+
+ @Environment(\.dismiss) private var dismiss
+
+ var body: some View {
+ VStack(alignment: .leading, spacing: 14) {
+ Text("Connection Settings")
+ .font(.title2.bold())
+
+ Form {
+ TextField(
+ "Connection Name",
+ text: $profile.name
+ )
+
+ TextField(
+ "Hostname",
+ text: $profile.host
+ )
+
+ TextField(
+ "Port",
+ value: $profile.port,
+ format: .number
+ )
+
+ TextField(
+ "Username",
+ text: $profile.username
+ )
+
+ SecureField(
+ "Password (stored in macOS Keychain)",
+ text: $password
+ )
+
+ TextField(
+ "Default Schema",
+ text: $profile.database
+ )
+ }
+
+ Text(
+ "Native MySQL connection. Local TCP connections and SSH tunnels are supported."
+ )
+ .font(.caption)
+ .foregroundStyle(.secondary)
+
+ HStack {
+ Spacer()
+
+ Button("Cancel") {
+ dismiss()
+ }
+
+ Button("Save") {
+ save()
+ }
+ .buttonStyle(.borderedProminent)
+ }
+ }
+ .padding(24)
+ .frame(width: 520)
+ }
+}
diff --git a/WorkbenchReplacement/MainView.swift.backup-20261016-170655 b/WorkbenchReplacement/MainView.swift.backup-20261016-170655
new file mode 100644
index 0000000..8e8179d
--- /dev/null
+++ b/WorkbenchReplacement/MainView.swift.backup-20261016-170655
@@ -0,0 +1,854 @@
+
+import SwiftUI
+import AppKit
+
+// MARK: - Classic Workbench Colours
+
+private enum WB {
+ static let toolbar = Color(nsColor: NSColor(
+ calibratedWhite: 0.88, alpha: 1
+ ))
+ static let sidebar = Color(nsColor: NSColor(
+ calibratedWhite: 0.94, alpha: 1
+ ))
+ static let border = Color(nsColor: NSColor(
+ calibratedWhite: 0.76, alpha: 1
+ ))
+ static let header = Color(nsColor: NSColor(
+ calibratedWhite: 0.91, alpha: 1
+ ))
+}
+
+// MARK: - Main Window
+
+struct MainView: View {
+ @EnvironmentObject private var model: DatabaseModel
+
+ @State private var showingEditor = false
+ @State private var editing = ConnectionProfile()
+ @State private var editingPassword = ""
+
+ @State private var expandedSchemas: Set = []
+ @State private var outputExpanded = true
+
+ var body: some View {
+ VStack(spacing: 0) {
+ mainToolbar
+
+ Rectangle()
+ .fill(WB.border)
+ .frame(height: 1)
+
+ HSplitView {
+ navigator
+ .frame(
+ minWidth: 210,
+ idealWidth: 265,
+ maxWidth: 420
+ )
+
+ workspace
+ .frame(minWidth: 600)
+ }
+
+ statusBar
+ }
+ .frame(minWidth: 1050, minHeight: 700)
+ .background(Color(nsColor: .windowBackgroundColor))
+ .sheet(isPresented: $showingEditor) {
+ ConnectionEditor(
+ profile: $editing,
+ password: $editingPassword
+ ) {
+ model.save(editing, password: editingPassword)
+ showingEditor = false
+ }
+ }
+ }
+
+ // MARK: - Main Toolbar
+
+ private var mainToolbar: some View {
+ HStack(spacing: 16) {
+ Image(systemName: "externaldrive.connected.to.line.below")
+ .font(.system(size: 23))
+ .foregroundStyle(.blue)
+
+ Text("MySQL Workbench")
+ .font(.system(size: 15, weight: .semibold))
+ .foregroundStyle(.black)
+
+ Rectangle()
+ .fill(WB.border)
+ .frame(width: 1, height: 30)
+
+ toolbarButton(
+ "New Connection",
+ icon: "plus.rectangle.on.rectangle"
+ ) {
+ editing = ConnectionProfile(name: "New Connection")
+ editingPassword = ""
+ showingEditor = true
+ }
+
+ toolbarButton(
+ "Refresh",
+ icon: "arrow.clockwise"
+ ) {
+ model.loadDatabases()
+ }
+
+ toolbarButton(
+ "Execute",
+ icon: "bolt.fill"
+ ) {
+ model.runSQL()
+ }
+ .keyboardShortcut(.return, modifiers: [.command])
+
+ toolbarButton(
+ "Format SQL",
+ icon: "text.alignleft"
+ ) {
+ model.formatSQL()
+ }
+
+ Spacer()
+
+ VStack(alignment: .trailing, spacing: 2) {
+ Text(model.selected?.name ?? "No Connection")
+ .font(.system(size: 12, weight: .semibold))
+
+ Text(model.selectedDatabase.isEmpty
+ ? "No default schema"
+ : model.selectedDatabase)
+ .font(.system(size: 11))
+ .foregroundStyle(.secondary)
+ }
+ }
+ .padding(.horizontal, 14)
+ .frame(height: 58)
+ .background(WB.toolbar)
+ }
+
+ private func toolbarButton(
+ _ title: String,
+ icon: String,
+ action: @escaping () -> Void
+ ) -> some View {
+ Button(action: action) {
+ VStack(spacing: 3) {
+ Image(systemName: icon)
+ .font(.system(size: 19))
+ .frame(height: 23)
+
+ Text(title)
+ .font(.system(size: 10))
+ }
+ .foregroundStyle(.black)
+ .frame(minWidth: 65)
+ }
+ .buttonStyle(.plain)
+ .help(title)
+ }
+
+ // MARK: - Navigator
+
+ private var navigator: some View {
+ VStack(spacing: 0) {
+ sectionHeader("NAVIGATOR")
+
+ HStack {
+ Text("SCHEMAS")
+ .font(.system(size: 11, weight: .bold))
+
+ Spacer()
+
+ Button {
+ model.loadDatabases()
+ } label: {
+ Image(systemName: "arrow.clockwise")
+ }
+ .buttonStyle(.borderless)
+ }
+ .padding(.horizontal, 12)
+ .frame(height: 34)
+
+ Divider()
+
+ ScrollView {
+ LazyVStack(alignment: .leading, spacing: 2) {
+ ForEach(model.databases, id: \.self) { db in
+ schemaNode(db)
+ }
+ }
+ .padding(.vertical, 8)
+ }
+
+ Divider()
+
+ sectionHeader("MYSQL CONNECTIONS")
+
+ ScrollView {
+ VStack(alignment: .leading, spacing: 4) {
+ ForEach(model.profiles) { profile in
+ Button {
+ model.selectedID = profile.id
+ model.loadDatabases()
+ } label: {
+ Label(
+ profile.name,
+ systemImage: "server.rack"
+ )
+ .frame(
+ maxWidth: .infinity,
+ alignment: .leading
+ )
+ .padding(6)
+ .background(
+ model.selectedID == profile.id
+ ? Color.blue.opacity(0.14)
+ : Color.clear
+ )
+ }
+ .buttonStyle(.plain)
+ .contextMenu {
+ Button("Edit Connection") {
+ edit(profile)
+ }
+ }
+ }
+ }
+ .padding(8)
+ }
+ .frame(height: 125)
+ }
+ .background(WB.sidebar)
+ }
+
+ private func schemaNode(_ db: String) -> some View {
+ VStack(alignment: .leading, spacing: 2) {
+ HStack(spacing: 5) {
+ Button {
+ toggleSchema(db)
+ } label: {
+ Image(systemName:
+ expandedSchemas.contains(db)
+ ? "chevron.down"
+ : "chevron.right"
+ )
+ .font(.system(size: 10))
+ .frame(width: 15)
+ }
+ .buttonStyle(.plain)
+
+ Image(systemName: "cylinder.split.1x2")
+ .foregroundStyle(.blue)
+
+ Text(db)
+ .font(.system(size: 12))
+ .fontWeight(
+ model.selectedDatabase == db
+ ? .bold : .regular
+ )
+
+ Spacer()
+ }
+ .padding(.horizontal, 10)
+ .padding(.vertical, 5)
+ .background(
+ model.selectedDatabase == db
+ ? Color.blue.opacity(0.13)
+ : Color.clear
+ )
+ .contentShape(Rectangle())
+ .onTapGesture(count: 2) {
+ model.selectedDatabase = db
+ expandedSchemas.insert(db)
+ model.loadTables(database: db)
+ }
+ .contextMenu {
+ Button("Set as Default Schema") {
+ model.selectedDatabase = db
+ }
+
+ Button("Refresh Tables") {
+ model.loadTables(database: db)
+ }
+ }
+
+ if expandedSchemas.contains(db) {
+ schemaChildren(db)
+ .padding(.leading, 24)
+ }
+ }
+ }
+
+ private func toggleSchema(_ db: String) {
+ if expandedSchemas.contains(db) {
+ expandedSchemas.remove(db)
+ } else {
+ expandedSchemas.insert(db)
+
+ if model.schemaTables[db] == nil {
+ model.loadTables(database: db)
+ }
+ }
+ }
+
+ private func schemaChildren(_ db: String) -> some View {
+ VStack(alignment: .leading, spacing: 5) {
+ DisclosureGroup {
+ if let tables = model.schemaTables[db] {
+ ForEach(tables, id: \.self) { table in
+ Label(table, systemImage: "tablecells")
+ .font(.system(size: 12))
+ .padding(.vertical, 3)
+ .onTapGesture(count: 2) {
+ model.selectTable(
+ database: db,
+ table: table
+ )
+ }
+ .contextMenu {
+ Button("Select First 100 Rows") {
+ model.selectTable(
+ database: db,
+ table: table
+ )
+ }
+ }
+ }
+ } else {
+ Button("Load Tables") {
+ model.loadTables(database: db)
+ }
+ .buttonStyle(.borderless)
+ }
+ } label: {
+ Label("Tables", systemImage: "folder")
+ }
+
+ Label("Views", systemImage: "folder")
+ Label("Stored Procedures", systemImage: "folder")
+ Label("Functions", systemImage: "folder")
+ }
+ .font(.system(size: 12))
+ .padding(.vertical, 3)
+ }
+
+ // MARK: - Workspace
+
+ private var workspace: some View {
+ VStack(spacing: 0) {
+ queryTabBar
+
+ Divider()
+
+ queryToolbar
+
+ Divider()
+
+ VSplitView {
+ sqlEditor
+ .frame(minHeight: 180)
+
+ bottomWorkspace
+ .frame(minHeight: 230)
+ }
+ }
+ }
+
+ private var queryTabBar: some View {
+ HStack(spacing: 0) {
+ HStack(spacing: 8) {
+ Image(systemName: "doc.text")
+ .foregroundStyle(.blue)
+
+ Text("Query 1")
+ .font(.system(size: 12, weight: .semibold))
+
+ Image(systemName: "xmark")
+ .font(.system(size: 9))
+ .foregroundStyle(.secondary)
+ }
+ .padding(.horizontal, 15)
+ .frame(height: 33)
+ .background(Color(nsColor: .textBackgroundColor))
+
+ Spacer()
+ }
+ .background(WB.header)
+ }
+
+ private var queryToolbar: some View {
+ HStack(spacing: 14) {
+ Button {
+ model.runSQL()
+ } label: {
+ Image(systemName: "bolt.fill")
+ .foregroundStyle(.orange)
+ }
+ .help("Execute SQL")
+
+ Button {
+ model.formatSQL()
+ } label: {
+ Image(systemName: "text.alignleft")
+ }
+ .help("Format SQL")
+
+ Divider()
+ .frame(height: 17)
+
+ Text("Default Schema:")
+ .font(.system(size: 11))
+
+ Text(model.selectedDatabase.isEmpty
+ ? "None"
+ : model.selectedDatabase)
+ .font(.system(size: 11, weight: .semibold))
+
+ Spacer()
+
+ Text("SQL Editor")
+ .font(.system(size: 11))
+ .foregroundStyle(.secondary)
+ }
+ .buttonStyle(.borderless)
+ .padding(.horizontal, 12)
+ .frame(height: 36)
+ .background(WB.header)
+ }
+
+ private var sqlEditor: some View {
+ HStack(spacing: 0) {
+ lineNumbers
+
+ Rectangle()
+ .fill(WB.border)
+ .frame(width: 1)
+
+ SQLTextView(text: $model.sql)
+ }
+ .background(Color(nsColor: .textBackgroundColor))
+ }
+
+ private var lineNumbers: some View {
+ ScrollView {
+ VStack(alignment: .trailing, spacing: 0) {
+ ForEach(
+ 1...max(1, model.sql.components(
+ separatedBy: "\n"
+ ).count),
+ id: \.self
+ ) { number in
+ Text("\(number)")
+ .frame(height: 19)
+ }
+ }
+ .padding(.horizontal, 9)
+ .padding(.top, 8)
+ }
+ .font(.system(size: 12, design: .monospaced))
+ .foregroundStyle(.secondary)
+ .frame(width: 48)
+ .background(WB.header)
+ .allowsHitTesting(false)
+ }
+
+ // MARK: - Results and Action Output
+
+ private var bottomWorkspace: some View {
+ VSplitView {
+ resultsPanel
+ .frame(minHeight: 150)
+
+ if outputExpanded {
+ actionOutput
+ .frame(minHeight: 85)
+ }
+ }
+ }
+
+ private var resultsPanel: some View {
+ VStack(spacing: 0) {
+ HStack {
+ Image(systemName: "tablecells")
+ Text("Result Grid")
+ .fontWeight(.semibold)
+
+ Spacer()
+
+ Text(model.result.message)
+ .foregroundStyle(.secondary)
+ }
+ .font(.system(size: 12))
+ .padding(.horizontal, 12)
+ .frame(height: 32)
+ .background(WB.header)
+
+ Divider()
+
+ if model.result.columns.isEmpty {
+ VStack {
+ Spacer()
+ Text("No query results")
+ .foregroundStyle(.secondary)
+ Spacer()
+ }
+ } else {
+ resultGrid
+ }
+ }
+ }
+
+ private var resultGrid: some View {
+ ScrollView([.horizontal, .vertical]) {
+ Grid(
+ alignment: .leading,
+ horizontalSpacing: 0,
+ verticalSpacing: 0
+ ) {
+ GridRow {
+ ForEach(
+ model.result.columns.indices,
+ id: \.self
+ ) { column in
+ Text(model.result.columns[column])
+ .fontWeight(.semibold)
+ .frame(
+ width: 170,
+ alignment: .leading
+ )
+ .padding(7)
+ .background(WB.header)
+ }
+ }
+
+ ForEach(
+ model.result.rows.indices,
+ id: \.self
+ ) { row in
+ GridRow {
+ ForEach(
+ model.result.columns.indices,
+ id: \.self
+ ) { column in
+ Text(
+ column < model.result.rows[row].count
+ ? model.result.rows[row][column]
+ : ""
+ )
+ .lineLimit(1)
+ .frame(
+ width: 170,
+ alignment: .leading
+ )
+ .padding(7)
+ .background(
+ row.isMultiple(of: 2)
+ ? Color(nsColor: .textBackgroundColor)
+ : WB.sidebar
+ )
+ }
+ }
+ }
+ }
+ .font(.system(size: 12, design: .monospaced))
+ }
+ }
+
+ private var actionOutput: some View {
+ VStack(spacing: 0) {
+ HStack {
+ Image(systemName: "list.bullet.rectangle")
+ Text("Action Output")
+ .fontWeight(.semibold)
+
+ Spacer()
+
+ Button {
+ outputExpanded = false
+ } label: {
+ Image(systemName: "chevron.down")
+ }
+ .buttonStyle(.borderless)
+ }
+ .font(.system(size: 12))
+ .padding(.horizontal, 12)
+ .frame(height: 30)
+ .background(WB.header)
+
+ Divider()
+
+ HStack(alignment: .top, spacing: 12) {
+ Image(systemName:
+ model.status == "Error"
+ ? "xmark.circle.fill"
+ : "checkmark.circle.fill"
+ )
+ .foregroundStyle(
+ model.status == "Error"
+ ? .red : .green
+ )
+
+ VStack(alignment: .leading, spacing: 5) {
+ Text(model.status)
+ .fontWeight(.semibold)
+
+ Text(model.result.message)
+ .foregroundStyle(.secondary)
+ }
+
+ Spacer()
+ }
+ .font(.system(size: 12))
+ .padding(12)
+
+ Spacer()
+ }
+ }
+
+ // MARK: - Status Bar
+
+ private var statusBar: some View {
+ HStack(spacing: 10) {
+ Circle()
+ .fill(
+ model.status == "Error"
+ ? Color.red : Color.green
+ )
+ .frame(width: 8, height: 8)
+
+ Text(model.status)
+ .lineLimit(1)
+
+ Spacer()
+
+ Text(model.selected?.name ?? "Disconnected")
+
+ Divider()
+ .frame(height: 13)
+
+ Text(model.selectedDatabase.isEmpty
+ ? "No Schema"
+ : model.selectedDatabase)
+
+ Divider()
+ .frame(height: 13)
+
+ Text("UTF-8")
+ }
+ .font(.system(size: 11))
+ .padding(.horizontal, 12)
+ .frame(height: 25)
+ .background(WB.header)
+ }
+
+ private func sectionHeader(_ title: String) -> some View {
+ HStack {
+ Text(title)
+ .font(.system(size: 11, weight: .bold))
+ .foregroundStyle(.secondary)
+
+ Spacer()
+ }
+ .padding(.horizontal, 12)
+ .frame(height: 30)
+ .background(WB.header)
+ }
+
+ private func edit(_ profile: ConnectionProfile) {
+ editing = profile
+ editingPassword = PasswordStore.read(profile.id)
+ showingEditor = true
+ }
+}
+
+// MARK: - Native SQL Text Editor
+
+struct SQLTextView: NSViewRepresentable {
+ @Binding var text: String
+
+ func makeCoordinator() -> Coordinator {
+ Coordinator(self)
+ }
+
+ func makeNSView(context: Context) -> NSScrollView {
+ let scroll = NSTextView.scrollableTextView()
+ let editor = scroll.documentView as! NSTextView
+
+ editor.isEditable = true
+ editor.isRichText = false
+ editor.allowsUndo = true
+ editor.isAutomaticQuoteSubstitutionEnabled = false
+ editor.isAutomaticDashSubstitutionEnabled = false
+ editor.font = NSFont.monospacedSystemFont(
+ ofSize: 13,
+ weight: .regular
+ )
+ editor.textContainerInset = NSSize(
+ width: 10,
+ height: 8
+ )
+ editor.delegate = context.coordinator
+ editor.string = text
+
+ context.coordinator.highlight(editor)
+
+ return scroll
+ }
+
+ func updateNSView(
+ _ scroll: NSScrollView,
+ context: Context
+ ) {
+ guard let editor = scroll.documentView as? NSTextView else {
+ return
+ }
+
+ if editor.string != text {
+ let selection = editor.selectedRange()
+ editor.string = text
+
+ let safeLocation = min(
+ selection.location,
+ (text as NSString).length
+ )
+
+ editor.setSelectedRange(
+ NSRange(location: safeLocation, length: 0)
+ )
+
+ context.coordinator.highlight(editor)
+ }
+ }
+
+ final class Coordinator: NSObject, NSTextViewDelegate {
+ var parent: SQLTextView
+
+ init(_ parent: SQLTextView) {
+ self.parent = parent
+ }
+
+ func textDidChange(_ notification: Notification) {
+ guard let editor = notification.object as? NSTextView else {
+ return
+ }
+
+ parent.text = editor.string
+ highlight(editor)
+ }
+
+ func highlight(_ editor: NSTextView) {
+ guard let storage = editor.textStorage else {
+ return
+ }
+
+ let text = editor.string as NSString
+ let range = NSRange(
+ location: 0,
+ length: text.length
+ )
+
+ storage.beginEditing()
+
+ storage.setAttributes([
+ .foregroundColor: NSColor.textColor,
+ .font: NSFont.monospacedSystemFont(
+ ofSize: 13,
+ weight: .regular
+ )
+ ], range: range)
+
+ let pattern = """
+ \\b(SELECT|FROM|WHERE|INSERT|UPDATE|DELETE|CREATE|TABLE|DATABASE|SHOW|USE|JOIN|LEFT|RIGHT|INNER|OUTER|ON|AS|AND|OR|ORDER|GROUP|BY|LIMIT|VALUES|SET|NULL|NOT|IN|LIKE|DISTINCT|ALTER|DROP|DESCRIBE|EXPLAIN)\\b
+ """
+
+ if let regex = try? NSRegularExpression(
+ pattern: pattern,
+ options: [.caseInsensitive]
+ ) {
+ for match in regex.matches(
+ in: editor.string,
+ range: range
+ ) {
+ storage.addAttribute(
+ .foregroundColor,
+ value: NSColor.systemBlue,
+ range: match.range
+ )
+ }
+ }
+
+ storage.endEditing()
+ }
+ }
+}
+
+// MARK: - Connection Editor
+
+struct ConnectionEditor: View {
+ @Binding var profile: ConnectionProfile
+ @Binding var password: String
+
+ let save: () -> Void
+
+ @Environment(\.dismiss) private var dismiss
+
+ var body: some View {
+ VStack(alignment: .leading, spacing: 14) {
+ Text("Connection Settings")
+ .font(.title2.bold())
+
+ Form {
+ TextField(
+ "Connection Name",
+ text: $profile.name
+ )
+
+ TextField(
+ "Hostname",
+ text: $profile.host
+ )
+
+ TextField(
+ "Port",
+ value: $profile.port,
+ format: .number
+ )
+
+ TextField(
+ "Username",
+ text: $profile.username
+ )
+
+ SecureField(
+ "Password (macOS Keychain)",
+ text: $password
+ )
+
+ TextField(
+ "Default Schema",
+ text: $profile.database
+ )
+ }
+
+ HStack {
+ Spacer()
+
+ Button("Cancel") {
+ dismiss()
+ }
+
+ Button("Save") {
+ save()
+ }
+ .buttonStyle(.borderedProminent)
+ }
+ }
+ .padding(24)
+ .frame(width: 520)
+ }
+}
diff --git a/WorkbenchReplacement/MainView.swift.backup-20261016-170825 b/WorkbenchReplacement/MainView.swift.backup-20261016-170825
new file mode 100644
index 0000000..3b64a37
--- /dev/null
+++ b/WorkbenchReplacement/MainView.swift.backup-20261016-170825
@@ -0,0 +1,861 @@
+
+import SwiftUI
+import AppKit
+
+// MARK: - Classic Workbench Colours
+
+private enum WB {
+ static let toolbar = Color(nsColor: NSColor(
+ calibratedWhite: 0.88, alpha: 1
+ ))
+ static let sidebar = Color(nsColor: NSColor(
+ calibratedWhite: 0.94, alpha: 1
+ ))
+ static let border = Color(nsColor: NSColor(
+ calibratedWhite: 0.76, alpha: 1
+ ))
+ static let header = Color(nsColor: NSColor(
+ calibratedWhite: 0.91, alpha: 1
+ ))
+}
+
+// MARK: - Main Window
+
+struct MainView: View {
+ @EnvironmentObject private var model: DatabaseModel
+
+ @State private var showingEditor = false
+ @State private var editing = ConnectionProfile()
+ @State private var editingPassword = ""
+
+ @State private var expandedSchemas: Set = []
+ @State private var outputExpanded = true
+
+ var body: some View {
+ VStack(spacing: 0) {
+ mainToolbar
+
+ Rectangle()
+ .fill(WB.border)
+ .frame(height: 1)
+
+ HSplitView {
+ navigator
+ .frame(
+ minWidth: 210,
+ idealWidth: 265,
+ maxWidth: 420
+ )
+
+ workspace
+ .frame(minWidth: 600)
+ }
+
+ statusBar
+ }
+ .frame(minWidth: 1050, minHeight: 700)
+ .preferredColorScheme(.light)
+ .background(Color(nsColor: .windowBackgroundColor))
+ .sheet(isPresented: $showingEditor) {
+ ConnectionEditor(
+ profile: $editing,
+ password: $editingPassword
+ ) {
+ model.save(editing, password: editingPassword)
+ showingEditor = false
+ }
+ }
+ }
+
+ // MARK: - Main Toolbar
+
+ private var mainToolbar: some View {
+ HStack(spacing: 16) {
+ Image(systemName: "externaldrive.connected.to.line.below")
+ .font(.system(size: 23))
+ .foregroundStyle(.blue)
+
+ Text("MySQL Workbench")
+ .font(.system(size: 15, weight: .semibold))
+ .foregroundStyle(.black)
+
+ Rectangle()
+ .fill(WB.border)
+ .frame(width: 1, height: 30)
+
+ toolbarButton(
+ "New Connection",
+ icon: "plus.rectangle.on.rectangle"
+ ) {
+ editing = ConnectionProfile(name: "New Connection")
+ editingPassword = ""
+ showingEditor = true
+ }
+
+ toolbarButton(
+ "Refresh",
+ icon: "arrow.clockwise"
+ ) {
+ model.loadDatabases()
+ }
+
+ toolbarButton(
+ "Execute",
+ icon: "bolt.fill"
+ ) {
+ model.runSQL()
+ }
+ .keyboardShortcut(.return, modifiers: [.command])
+
+ toolbarButton(
+ "Format SQL",
+ icon: "text.alignleft"
+ ) {
+ model.formatSQL()
+ }
+
+ Spacer()
+
+ VStack(alignment: .trailing, spacing: 2) {
+ Text(model.selected?.name ?? "No Connection")
+ .font(.system(size: 12, weight: .semibold))
+
+ Text(model.selectedDatabase.isEmpty
+ ? "No default schema"
+ : model.selectedDatabase)
+ .font(.system(size: 11))
+ .foregroundStyle(.secondary)
+ }
+ }
+ .padding(.horizontal, 14)
+ .frame(height: 58)
+ .background(WB.toolbar)
+ }
+
+ private func toolbarButton(
+ _ title: String,
+ icon: String,
+ action: @escaping () -> Void
+ ) -> some View {
+ Button(action: action) {
+ VStack(spacing: 3) {
+ Image(systemName: icon)
+ .font(.system(size: 19))
+ .frame(height: 23)
+
+ Text(title)
+ .font(.system(size: 10))
+ }
+ .foregroundStyle(.black)
+ .frame(minWidth: 65)
+ }
+ .buttonStyle(.plain)
+ .help(title)
+ }
+
+ // MARK: - Navigator
+
+ private var navigator: some View {
+ VStack(spacing: 0) {
+ sectionHeader("NAVIGATOR")
+
+ HStack {
+ Text("SCHEMAS")
+ .font(.system(size: 11, weight: .bold))
+
+ Spacer()
+
+ Button {
+ model.loadDatabases()
+ } label: {
+ Image(systemName: "arrow.clockwise")
+ }
+ .buttonStyle(.borderless)
+ }
+ .padding(.horizontal, 12)
+ .frame(height: 34)
+
+ Divider()
+
+ ScrollView {
+ LazyVStack(alignment: .leading, spacing: 2) {
+ ForEach(model.databases, id: \.self) { db in
+ schemaNode(db)
+ }
+ }
+ .padding(.vertical, 8)
+ }
+
+ Divider()
+
+ sectionHeader("MYSQL CONNECTIONS")
+
+ ScrollView {
+ VStack(alignment: .leading, spacing: 4) {
+ ForEach(model.profiles) { profile in
+ Button {
+ model.selectedID = profile.id
+ model.loadDatabases()
+ } label: {
+ Label(
+ profile.name,
+ systemImage: "server.rack"
+ )
+ .frame(
+ maxWidth: .infinity,
+ alignment: .leading
+ )
+ .padding(6)
+ .background(
+ model.selectedID == profile.id
+ ? Color.blue.opacity(0.14)
+ : Color.clear
+ )
+ }
+ .buttonStyle(.plain)
+ .contextMenu {
+ Button("Edit Connection") {
+ edit(profile)
+ }
+ }
+ }
+ }
+ .padding(8)
+ }
+ .frame(height: 125)
+ }
+ .background(WB.sidebar)
+ }
+
+ private func schemaNode(_ db: String) -> some View {
+ VStack(alignment: .leading, spacing: 2) {
+ HStack(spacing: 5) {
+ Button {
+ toggleSchema(db)
+ } label: {
+ Image(systemName:
+ expandedSchemas.contains(db)
+ ? "chevron.down"
+ : "chevron.right"
+ )
+ .font(.system(size: 10))
+ .frame(width: 15)
+ }
+ .buttonStyle(.plain)
+
+ Image(systemName: "cylinder.split.1x2")
+ .foregroundStyle(.blue)
+
+ Text(db)
+ .font(.system(size: 12))
+ .fontWeight(
+ model.selectedDatabase == db
+ ? .bold : .regular
+ )
+
+ Spacer()
+ }
+ .padding(.horizontal, 10)
+ .padding(.vertical, 5)
+ .background(
+ model.selectedDatabase == db
+ ? Color.blue.opacity(0.13)
+ : Color.clear
+ )
+ .contentShape(Rectangle())
+ .onTapGesture(count: 2) {
+ model.selectedDatabase = db
+ expandedSchemas.insert(db)
+ model.loadTables(database: db)
+ }
+ .contextMenu {
+ Button("Set as Default Schema") {
+ model.selectedDatabase = db
+ }
+
+ Button("Refresh Tables") {
+ model.loadTables(database: db)
+ }
+ }
+
+ if expandedSchemas.contains(db) {
+ schemaChildren(db)
+ .padding(.leading, 24)
+ }
+ }
+ }
+
+ private func toggleSchema(_ db: String) {
+ if expandedSchemas.contains(db) {
+ expandedSchemas.remove(db)
+ } else {
+ expandedSchemas.insert(db)
+
+ if model.schemaTables[db] == nil {
+ model.loadTables(database: db)
+ }
+ }
+ }
+
+ private func schemaChildren(_ db: String) -> some View {
+ VStack(alignment: .leading, spacing: 5) {
+ DisclosureGroup {
+ if let tables = model.schemaTables[db] {
+ ForEach(tables, id: \.self) { table in
+ Label(table, systemImage: "tablecells")
+ .font(.system(size: 12))
+ .padding(.vertical, 3)
+ .onTapGesture(count: 2) {
+ model.selectTable(
+ database: db,
+ table: table
+ )
+ }
+ .contextMenu {
+ Button("Select First 100 Rows") {
+ model.selectTable(
+ database: db,
+ table: table
+ )
+ }
+ }
+ }
+ } else {
+ Button("Load Tables") {
+ model.loadTables(database: db)
+ }
+ .buttonStyle(.borderless)
+ }
+ } label: {
+ Label("Tables", systemImage: "folder")
+ }
+
+ Label("Views", systemImage: "folder")
+ Label("Stored Procedures", systemImage: "folder")
+ Label("Functions", systemImage: "folder")
+ }
+ .font(.system(size: 12))
+ .padding(.vertical, 3)
+ }
+
+ // MARK: - Workspace
+
+ private var workspace: some View {
+ VStack(spacing: 0) {
+ queryTabBar
+
+ Divider()
+
+ queryToolbar
+
+ Divider()
+
+ VSplitView {
+ sqlEditor
+ .frame(minHeight: 180)
+
+ bottomWorkspace
+ .frame(minHeight: 230)
+ }
+ }
+ }
+
+ private var queryTabBar: some View {
+ HStack(spacing: 0) {
+ HStack(spacing: 8) {
+ Image(systemName: "doc.text")
+ .foregroundStyle(.blue)
+
+ Text("Query 1")
+ .font(.system(size: 12, weight: .semibold))
+
+ Image(systemName: "xmark")
+ .font(.system(size: 9))
+ .foregroundStyle(.secondary)
+ }
+ .padding(.horizontal, 15)
+ .frame(height: 33)
+ .background(Color(nsColor: .textBackgroundColor))
+
+ Spacer()
+ }
+ .background(WB.header)
+ }
+
+ private var queryToolbar: some View {
+ HStack(spacing: 14) {
+ Button {
+ model.runSQL()
+ } label: {
+ Image(systemName: "bolt.fill")
+ .foregroundStyle(.orange)
+ }
+ .help("Execute SQL")
+
+ Button {
+ model.formatSQL()
+ } label: {
+ Image(systemName: "text.alignleft")
+ }
+ .help("Format SQL")
+
+ Divider()
+ .frame(height: 17)
+
+ Text("Default Schema:")
+ .font(.system(size: 11))
+
+ Text(model.selectedDatabase.isEmpty
+ ? "None"
+ : model.selectedDatabase)
+ .font(.system(size: 11, weight: .semibold))
+
+ Spacer()
+
+ Text("SQL Editor")
+ .font(.system(size: 11))
+ .foregroundStyle(.secondary)
+ }
+ .buttonStyle(.borderless)
+ .padding(.horizontal, 12)
+ .frame(height: 36)
+ .background(WB.header)
+ }
+
+ private var sqlEditor: some View {
+ HStack(spacing: 0) {
+ lineNumbers
+
+ Rectangle()
+ .fill(WB.border)
+ .frame(width: 1)
+
+ SQLTextView(text: $model.sql)
+ }
+ .background(Color(nsColor: .textBackgroundColor))
+ }
+
+ private var lineNumbers: some View {
+ ScrollView {
+ VStack(alignment: .trailing, spacing: 0) {
+ ForEach(
+ 1...max(1, model.sql.components(
+ separatedBy: "\n"
+ ).count),
+ id: \.self
+ ) { number in
+ Text("\(number)")
+ .frame(height: 19)
+ }
+ }
+ .padding(.horizontal, 9)
+ .padding(.top, 8)
+ }
+ .font(.system(size: 12, design: .monospaced))
+ .foregroundStyle(.secondary)
+ .frame(width: 48)
+ .background(WB.header)
+ .allowsHitTesting(false)
+ }
+
+ // MARK: - Results and Action Output
+
+ private var bottomWorkspace: some View {
+ VSplitView {
+ resultsPanel
+ .frame(minHeight: 150)
+
+ if outputExpanded {
+ actionOutput
+ .frame(minHeight: 85)
+ }
+ }
+ }
+
+ private var resultsPanel: some View {
+ VStack(spacing: 0) {
+ HStack {
+ Image(systemName: "tablecells")
+ Text("Result Grid")
+ .fontWeight(.semibold)
+
+ Spacer()
+
+ Text(model.result.message)
+ .foregroundStyle(.secondary)
+ }
+ .font(.system(size: 12))
+ .padding(.horizontal, 12)
+ .frame(height: 32)
+ .background(WB.header)
+
+ Divider()
+
+ if model.result.columns.isEmpty {
+ VStack {
+ Spacer()
+ Text("No query results")
+ .foregroundStyle(.secondary)
+ Spacer()
+ }
+ } else {
+ resultGrid
+ }
+ }
+ }
+
+ private var resultGrid: some View {
+ ScrollView([.horizontal, .vertical]) {
+ Grid(
+ alignment: .leading,
+ horizontalSpacing: 0,
+ verticalSpacing: 0
+ ) {
+ GridRow {
+ ForEach(
+ model.result.columns.indices,
+ id: \.self
+ ) { column in
+ Text(model.result.columns[column])
+ .fontWeight(.semibold)
+ .frame(
+ width: 170,
+ alignment: .leading
+ )
+ .padding(7)
+ .background(WB.header)
+ }
+ }
+
+ ForEach(
+ model.result.rows.indices,
+ id: \.self
+ ) { row in
+ GridRow {
+ ForEach(
+ model.result.columns.indices,
+ id: \.self
+ ) { column in
+ Text(
+ column < model.result.rows[row].count
+ ? model.result.rows[row][column]
+ : ""
+ )
+ .lineLimit(1)
+ .frame(
+ width: 170,
+ alignment: .leading
+ )
+ .padding(7)
+ .background(
+ row.isMultiple(of: 2)
+ ? Color(nsColor: .textBackgroundColor)
+ : WB.sidebar
+ )
+ }
+ }
+ }
+ }
+ .font(.system(size: 12, design: .monospaced))
+ }
+ }
+
+ private var actionOutput: some View {
+ VStack(spacing: 0) {
+ HStack {
+ Image(systemName: "list.bullet.rectangle")
+ Text("Action Output")
+ .fontWeight(.semibold)
+
+ Spacer()
+
+ Button {
+ outputExpanded = false
+ } label: {
+ Image(systemName: "chevron.down")
+ }
+ .buttonStyle(.borderless)
+ }
+ .font(.system(size: 12))
+ .padding(.horizontal, 12)
+ .frame(height: 30)
+ .background(WB.header)
+
+ Divider()
+
+ HStack(alignment: .top, spacing: 12) {
+ Image(systemName:
+ model.status == "Error"
+ ? "xmark.circle.fill"
+ : "checkmark.circle.fill"
+ )
+ .foregroundStyle(
+ model.status == "Error"
+ ? .red : .green
+ )
+
+ VStack(alignment: .leading, spacing: 5) {
+ Text(model.status)
+ .fontWeight(.semibold)
+
+ Text(model.result.message)
+ .foregroundStyle(.secondary)
+ }
+
+ Spacer()
+ }
+ .font(.system(size: 12))
+ .padding(12)
+
+ Spacer()
+ }
+ }
+
+ // MARK: - Status Bar
+
+ private var statusBar: some View {
+ HStack(spacing: 10) {
+ Circle()
+ .fill(
+ model.status == "Error"
+ ? Color.red : Color.green
+ )
+ .frame(width: 8, height: 8)
+
+ Text(model.status)
+ .lineLimit(1)
+
+ Spacer()
+
+ Text(model.selected?.name ?? "Disconnected")
+
+ Divider()
+ .frame(height: 13)
+
+ Text(model.selectedDatabase.isEmpty
+ ? "No Schema"
+ : model.selectedDatabase)
+
+ Divider()
+ .frame(height: 13)
+
+ Text("UTF-8")
+ }
+ .font(.system(size: 11))
+ .padding(.horizontal, 12)
+ .frame(height: 25)
+ .background(WB.header)
+ }
+
+ private func sectionHeader(_ title: String) -> some View {
+ HStack {
+ Text(title)
+ .font(.system(size: 11, weight: .bold))
+ .foregroundStyle(.secondary)
+
+ Spacer()
+ }
+ .padding(.horizontal, 12)
+ .frame(height: 30)
+ .background(WB.header)
+ }
+
+ private func edit(_ profile: ConnectionProfile) {
+ editing = profile
+ editingPassword = PasswordStore.read(profile.id)
+ showingEditor = true
+ }
+}
+
+// MARK: - Native SQL Text Editor
+
+struct SQLTextView: NSViewRepresentable {
+ @Binding var text: String
+
+ func makeCoordinator() -> Coordinator {
+ Coordinator(self)
+ }
+
+ func makeNSView(context: Context) -> NSScrollView {
+ let scroll = NSTextView.scrollableTextView()
+ let editor = scroll.documentView as! NSTextView
+
+ scroll.appearance = NSAppearance(named: .aqua)
+ editor.appearance = NSAppearance(named: .aqua)
+ editor.backgroundColor = .white
+ editor.textColor = .black
+ scroll.backgroundColor = .white
+
+ editor.isEditable = true
+ editor.isRichText = false
+ editor.allowsUndo = true
+ editor.isAutomaticQuoteSubstitutionEnabled = false
+ editor.isAutomaticDashSubstitutionEnabled = false
+ editor.font = NSFont.monospacedSystemFont(
+ ofSize: 13,
+ weight: .regular
+ )
+ editor.textContainerInset = NSSize(
+ width: 10,
+ height: 8
+ )
+ editor.delegate = context.coordinator
+ editor.string = text
+
+ context.coordinator.highlight(editor)
+
+ return scroll
+ }
+
+ func updateNSView(
+ _ scroll: NSScrollView,
+ context: Context
+ ) {
+ guard let editor = scroll.documentView as? NSTextView else {
+ return
+ }
+
+ if editor.string != text {
+ let selection = editor.selectedRange()
+ editor.string = text
+
+ let safeLocation = min(
+ selection.location,
+ (text as NSString).length
+ )
+
+ editor.setSelectedRange(
+ NSRange(location: safeLocation, length: 0)
+ )
+
+ context.coordinator.highlight(editor)
+ }
+ }
+
+ final class Coordinator: NSObject, NSTextViewDelegate {
+ var parent: SQLTextView
+
+ init(_ parent: SQLTextView) {
+ self.parent = parent
+ }
+
+ func textDidChange(_ notification: Notification) {
+ guard let editor = notification.object as? NSTextView else {
+ return
+ }
+
+ parent.text = editor.string
+ highlight(editor)
+ }
+
+ func highlight(_ editor: NSTextView) {
+ guard let storage = editor.textStorage else {
+ return
+ }
+
+ let text = editor.string as NSString
+ let range = NSRange(
+ location: 0,
+ length: text.length
+ )
+
+ storage.beginEditing()
+
+ storage.setAttributes([
+ .foregroundColor: NSColor.textColor,
+ .font: NSFont.monospacedSystemFont(
+ ofSize: 13,
+ weight: .regular
+ )
+ ], range: range)
+
+ let pattern = """
+ \\b(SELECT|FROM|WHERE|INSERT|UPDATE|DELETE|CREATE|TABLE|DATABASE|SHOW|USE|JOIN|LEFT|RIGHT|INNER|OUTER|ON|AS|AND|OR|ORDER|GROUP|BY|LIMIT|VALUES|SET|NULL|NOT|IN|LIKE|DISTINCT|ALTER|DROP|DESCRIBE|EXPLAIN)\\b
+ """
+
+ if let regex = try? NSRegularExpression(
+ pattern: pattern,
+ options: [.caseInsensitive]
+ ) {
+ for match in regex.matches(
+ in: editor.string,
+ range: range
+ ) {
+ storage.addAttribute(
+ .foregroundColor,
+ value: NSColor.systemBlue,
+ range: match.range
+ )
+ }
+ }
+
+ storage.endEditing()
+ }
+ }
+}
+
+// MARK: - Connection Editor
+
+struct ConnectionEditor: View {
+ @Binding var profile: ConnectionProfile
+ @Binding var password: String
+
+ let save: () -> Void
+
+ @Environment(\.dismiss) private var dismiss
+
+ var body: some View {
+ VStack(alignment: .leading, spacing: 14) {
+ Text("Connection Settings")
+ .font(.title2.bold())
+
+ Form {
+ TextField(
+ "Connection Name",
+ text: $profile.name
+ )
+
+ TextField(
+ "Hostname",
+ text: $profile.host
+ )
+
+ TextField(
+ "Port",
+ value: $profile.port,
+ format: .number
+ )
+
+ TextField(
+ "Username",
+ text: $profile.username
+ )
+
+ SecureField(
+ "Password (macOS Keychain)",
+ text: $password
+ )
+
+ TextField(
+ "Default Schema",
+ text: $profile.database
+ )
+ }
+
+ HStack {
+ Spacer()
+
+ Button("Cancel") {
+ dismiss()
+ }
+
+ Button("Save") {
+ save()
+ }
+ .buttonStyle(.borderedProminent)
+ }
+ }
+ .padding(24)
+ .frame(width: 520)
+ }
+}
diff --git a/WorkbenchReplacement/MainView.swift.backup-20261016-170924 b/WorkbenchReplacement/MainView.swift.backup-20261016-170924
new file mode 100644
index 0000000..0f9d3fd
--- /dev/null
+++ b/WorkbenchReplacement/MainView.swift.backup-20261016-170924
@@ -0,0 +1,861 @@
+
+import SwiftUI
+import AppKit
+
+// MARK: - Classic Workbench Colours
+
+private enum WB {
+ static let toolbar = Color(nsColor: NSColor(
+ calibratedWhite: 0.88, alpha: 1
+ ))
+ static let sidebar = Color(nsColor: NSColor(
+ calibratedWhite: 0.94, alpha: 1
+ ))
+ static let border = Color(nsColor: NSColor(
+ calibratedWhite: 0.76, alpha: 1
+ ))
+ static let header = Color(nsColor: NSColor(
+ calibratedWhite: 0.91, alpha: 1
+ ))
+}
+
+// MARK: - Main Window
+
+struct MainView: View {
+ @EnvironmentObject private var model: DatabaseModel
+
+ @State private var showingEditor = false
+ @State private var editing = ConnectionProfile()
+ @State private var editingPassword = ""
+
+ @State private var expandedSchemas: Set = []
+ @State private var outputExpanded = true
+
+ var body: some View {
+ VStack(spacing: 0) {
+ mainToolbar
+
+ Rectangle()
+ .fill(WB.border)
+ .frame(height: 1)
+
+ HSplitView {
+ navigator
+ .frame(
+ minWidth: 210,
+ idealWidth: 265,
+ maxWidth: 420
+ )
+
+ workspace
+ .frame(minWidth: 600)
+ }
+
+ statusBar
+ }
+ .frame(minWidth: 1100, minHeight: 720)
+ .preferredColorScheme(.light)
+ .background(Color(nsColor: .windowBackgroundColor))
+ .sheet(isPresented: $showingEditor) {
+ ConnectionEditor(
+ profile: $editing,
+ password: $editingPassword
+ ) {
+ model.save(editing, password: editingPassword)
+ showingEditor = false
+ }
+ }
+ }
+
+ // MARK: - Main Toolbar
+
+ private var mainToolbar: some View {
+ HStack(spacing: 16) {
+ Image(systemName: "externaldrive.connected.to.line.below")
+ .font(.system(size: 23))
+ .foregroundStyle(.blue)
+
+ Text("MySQL Workbench")
+ .font(.system(size: 15, weight: .semibold))
+ .foregroundStyle(.black)
+
+ Rectangle()
+ .fill(WB.border)
+ .frame(width: 1, height: 30)
+
+ toolbarButton(
+ "New Connection",
+ icon: "plus.rectangle.on.rectangle"
+ ) {
+ editing = ConnectionProfile(name: "New Connection")
+ editingPassword = ""
+ showingEditor = true
+ }
+
+ toolbarButton(
+ "Refresh",
+ icon: "arrow.clockwise"
+ ) {
+ model.loadDatabases()
+ }
+
+ toolbarButton(
+ "Execute",
+ icon: "bolt.fill"
+ ) {
+ model.runSQL()
+ }
+ .keyboardShortcut(.return, modifiers: [.command])
+
+ toolbarButton(
+ "Format SQL",
+ icon: "text.alignleft"
+ ) {
+ model.formatSQL()
+ }
+
+ Spacer()
+
+ VStack(alignment: .trailing, spacing: 2) {
+ Text(model.selected?.name ?? "No Connection")
+ .font(.system(size: 12, weight: .semibold))
+
+ Text(model.selectedDatabase.isEmpty
+ ? "No default schema"
+ : model.selectedDatabase)
+ .font(.system(size: 11))
+ .foregroundStyle(.secondary)
+ }
+ }
+ .padding(.horizontal, 10)
+ .frame(height: 46)
+ .background(WB.toolbar)
+ }
+
+ private func toolbarButton(
+ _ title: String,
+ icon: String,
+ action: @escaping () -> Void
+ ) -> some View {
+ Button(action: action) {
+ VStack(spacing: 3) {
+ Image(systemName: icon)
+ .font(.system(size: 19))
+ .frame(height: 23)
+
+ Text(title)
+ .font(.system(size: 10))
+ }
+ .foregroundStyle(.black)
+ .frame(minWidth: 65)
+ }
+ .buttonStyle(.plain)
+ .help(title)
+ }
+
+ // MARK: - Navigator
+
+ private var navigator: some View {
+ VStack(spacing: 0) {
+ sectionHeader("NAVIGATOR")
+
+ HStack {
+ Text("SCHEMAS")
+ .font(.system(size: 11, weight: .bold))
+
+ Spacer()
+
+ Button {
+ model.loadDatabases()
+ } label: {
+ Image(systemName: "arrow.clockwise")
+ }
+ .buttonStyle(.borderless)
+ }
+ .padding(.horizontal, 12)
+ .frame(height: 34)
+
+ Divider()
+
+ ScrollView {
+ LazyVStack(alignment: .leading, spacing: 2) {
+ ForEach(model.databases, id: \.self) { db in
+ schemaNode(db)
+ }
+ }
+ .padding(.vertical, 8)
+ }
+
+ Divider()
+
+ sectionHeader("MYSQL CONNECTIONS")
+
+ ScrollView {
+ VStack(alignment: .leading, spacing: 4) {
+ ForEach(model.profiles) { profile in
+ Button {
+ model.selectedID = profile.id
+ model.loadDatabases()
+ } label: {
+ Label(
+ profile.name,
+ systemImage: "server.rack"
+ )
+ .frame(
+ maxWidth: .infinity,
+ alignment: .leading
+ )
+ .padding(6)
+ .background(
+ model.selectedID == profile.id
+ ? Color.blue.opacity(0.14)
+ : Color.clear
+ )
+ }
+ .buttonStyle(.plain)
+ .contextMenu {
+ Button("Edit Connection") {
+ edit(profile)
+ }
+ }
+ }
+ }
+ .padding(8)
+ }
+ .frame(height: 125)
+ }
+ .background(WB.sidebar)
+ }
+
+ private func schemaNode(_ db: String) -> some View {
+ VStack(alignment: .leading, spacing: 2) {
+ HStack(spacing: 5) {
+ Button {
+ toggleSchema(db)
+ } label: {
+ Image(systemName:
+ expandedSchemas.contains(db)
+ ? "chevron.down"
+ : "chevron.right"
+ )
+ .font(.system(size: 10))
+ .frame(width: 15)
+ }
+ .buttonStyle(.plain)
+
+ Image(systemName: "cylinder.split.1x2")
+ .foregroundStyle(.blue)
+
+ Text(db)
+ .font(.system(size: 12))
+ .fontWeight(
+ model.selectedDatabase == db
+ ? .bold : .regular
+ )
+
+ Spacer()
+ }
+ .padding(.horizontal, 10)
+ .padding(.vertical, 5)
+ .background(
+ model.selectedDatabase == db
+ ? Color.blue.opacity(0.13)
+ : Color.clear
+ )
+ .contentShape(Rectangle())
+ .onTapGesture(count: 2) {
+ model.selectedDatabase = db
+ expandedSchemas.insert(db)
+ model.loadTables(database: db)
+ }
+ .contextMenu {
+ Button("Set as Default Schema") {
+ model.selectedDatabase = db
+ }
+
+ Button("Refresh Tables") {
+ model.loadTables(database: db)
+ }
+ }
+
+ if expandedSchemas.contains(db) {
+ schemaChildren(db)
+ .padding(.leading, 24)
+ }
+ }
+ }
+
+ private func toggleSchema(_ db: String) {
+ if expandedSchemas.contains(db) {
+ expandedSchemas.remove(db)
+ } else {
+ expandedSchemas.insert(db)
+
+ if model.schemaTables[db] == nil {
+ model.loadTables(database: db)
+ }
+ }
+ }
+
+ private func schemaChildren(_ db: String) -> some View {
+ VStack(alignment: .leading, spacing: 5) {
+ DisclosureGroup {
+ if let tables = model.schemaTables[db] {
+ ForEach(tables, id: \.self) { table in
+ Label(table, systemImage: "tablecells")
+ .font(.system(size: 12))
+ .padding(.vertical, 3)
+ .onTapGesture(count: 2) {
+ model.selectTable(
+ database: db,
+ table: table
+ )
+ }
+ .contextMenu {
+ Button("Select First 100 Rows") {
+ model.selectTable(
+ database: db,
+ table: table
+ )
+ }
+ }
+ }
+ } else {
+ Button("Load Tables") {
+ model.loadTables(database: db)
+ }
+ .buttonStyle(.borderless)
+ }
+ } label: {
+ Label("Tables", systemImage: "folder")
+ }
+
+ Label("Views", systemImage: "folder")
+ Label("Stored Procedures", systemImage: "folder")
+ Label("Functions", systemImage: "folder")
+ }
+ .font(.system(size: 12))
+ .padding(.vertical, 3)
+ }
+
+ // MARK: - Workspace
+
+ private var workspace: some View {
+ VStack(spacing: 0) {
+ queryTabBar
+
+ Divider()
+
+ queryToolbar
+
+ Divider()
+
+ VSplitView {
+ sqlEditor
+ .frame(minHeight: 230)
+
+ bottomWorkspace
+ .frame(minHeight: 230)
+ }
+ }
+ }
+
+ private var queryTabBar: some View {
+ HStack(spacing: 0) {
+ HStack(spacing: 8) {
+ Image(systemName: "doc.text")
+ .foregroundStyle(.blue)
+
+ Text("Query 1")
+ .font(.system(size: 12, weight: .semibold))
+
+ Image(systemName: "xmark")
+ .font(.system(size: 9))
+ .foregroundStyle(.secondary)
+ }
+ .padding(.horizontal, 10)
+ .frame(height: 29)
+ .background(Color(nsColor: .textBackgroundColor))
+
+ Spacer()
+ }
+ .background(WB.header)
+ }
+
+ private var queryToolbar: some View {
+ HStack(spacing: 14) {
+ Button {
+ model.runSQL()
+ } label: {
+ Image(systemName: "bolt.fill")
+ .foregroundStyle(.orange)
+ }
+ .help("Execute SQL")
+
+ Button {
+ model.formatSQL()
+ } label: {
+ Image(systemName: "text.alignleft")
+ }
+ .help("Format SQL")
+
+ Divider()
+ .frame(height: 17)
+
+ Text("Default Schema:")
+ .font(.system(size: 11))
+
+ Text(model.selectedDatabase.isEmpty
+ ? "None"
+ : model.selectedDatabase)
+ .font(.system(size: 11, weight: .semibold))
+
+ Spacer()
+
+ Text("SQL Editor")
+ .font(.system(size: 11))
+ .foregroundStyle(.secondary)
+ }
+ .buttonStyle(.borderless)
+ .padding(.horizontal, 12)
+ .frame(height: 30)
+ .background(WB.header)
+ }
+
+ private var sqlEditor: some View {
+ HStack(spacing: 0) {
+ lineNumbers
+
+ Rectangle()
+ .fill(WB.border)
+ .frame(width: 1)
+
+ SQLTextView(text: $model.sql)
+ }
+ .background(Color(nsColor: .textBackgroundColor))
+ }
+
+ private var lineNumbers: some View {
+ ScrollView {
+ VStack(alignment: .trailing, spacing: 0) {
+ ForEach(
+ 1...max(1, model.sql.components(
+ separatedBy: "\n"
+ ).count),
+ id: \.self
+ ) { number in
+ Text("\(number)")
+ .frame(height: 19)
+ }
+ }
+ .padding(.horizontal, 9)
+ .padding(.top, 8)
+ }
+ .font(.system(size: 12, design: .monospaced))
+ .foregroundStyle(.secondary)
+ .frame(width: 48)
+ .background(WB.header)
+ .allowsHitTesting(false)
+ }
+
+ // MARK: - Results and Action Output
+
+ private var bottomWorkspace: some View {
+ VSplitView {
+ resultsPanel
+ .frame(minHeight: 180)
+
+ if outputExpanded {
+ actionOutput
+ .frame(minHeight: 90)
+ }
+ }
+ }
+
+ private var resultsPanel: some View {
+ VStack(spacing: 0) {
+ HStack {
+ Image(systemName: "tablecells")
+ Text("Result Grid")
+ .fontWeight(.semibold)
+
+ Spacer()
+
+ Text(model.result.message)
+ .foregroundStyle(.secondary)
+ }
+ .font(.system(size: 12))
+ .padding(.horizontal, 12)
+ .frame(height: 28)
+ .background(WB.header)
+
+ Divider()
+
+ if model.result.columns.isEmpty {
+ VStack {
+ Spacer()
+ Text("No query results")
+ .foregroundStyle(.secondary)
+ Spacer()
+ }
+ } else {
+ resultGrid
+ }
+ }
+ }
+
+ private var resultGrid: some View {
+ ScrollView([.horizontal, .vertical]) {
+ Grid(
+ alignment: .leading,
+ horizontalSpacing: 0,
+ verticalSpacing: 0
+ ) {
+ GridRow {
+ ForEach(
+ model.result.columns.indices,
+ id: \.self
+ ) { column in
+ Text(model.result.columns[column])
+ .fontWeight(.semibold)
+ .frame(
+ width: 170,
+ alignment: .leading
+ )
+ .padding(7)
+ .background(WB.header)
+ }
+ }
+
+ ForEach(
+ model.result.rows.indices,
+ id: \.self
+ ) { row in
+ GridRow {
+ ForEach(
+ model.result.columns.indices,
+ id: \.self
+ ) { column in
+ Text(
+ column < model.result.rows[row].count
+ ? model.result.rows[row][column]
+ : ""
+ )
+ .lineLimit(1)
+ .frame(
+ width: 170,
+ alignment: .leading
+ )
+ .padding(7)
+ .background(
+ row.isMultiple(of: 2)
+ ? Color(nsColor: .textBackgroundColor)
+ : WB.sidebar
+ )
+ }
+ }
+ }
+ }
+ .font(.system(size: 12, design: .monospaced))
+ }
+ }
+
+ private var actionOutput: some View {
+ VStack(spacing: 0) {
+ HStack {
+ Image(systemName: "list.bullet.rectangle")
+ Text("Action Output")
+ .fontWeight(.semibold)
+
+ Spacer()
+
+ Button {
+ outputExpanded = false
+ } label: {
+ Image(systemName: "chevron.down")
+ }
+ .buttonStyle(.borderless)
+ }
+ .font(.system(size: 12))
+ .padding(.horizontal, 12)
+ .frame(height: 26)
+ .background(WB.header)
+
+ Divider()
+
+ HStack(alignment: .top, spacing: 12) {
+ Image(systemName:
+ model.status == "Error"
+ ? "xmark.circle.fill"
+ : "checkmark.circle.fill"
+ )
+ .foregroundStyle(
+ model.status == "Error"
+ ? .red : .green
+ )
+
+ VStack(alignment: .leading, spacing: 5) {
+ Text(model.status)
+ .fontWeight(.semibold)
+
+ Text(model.result.message)
+ .foregroundStyle(.secondary)
+ }
+
+ Spacer()
+ }
+ .font(.system(size: 12))
+ .padding(12)
+
+ Spacer()
+ }
+ }
+
+ // MARK: - Status Bar
+
+ private var statusBar: some View {
+ HStack(spacing: 10) {
+ Circle()
+ .fill(
+ model.status == "Error"
+ ? Color.red : Color.green
+ )
+ .frame(width: 8, height: 8)
+
+ Text(model.status)
+ .lineLimit(1)
+
+ Spacer()
+
+ Text(model.selected?.name ?? "Disconnected")
+
+ Divider()
+ .frame(height: 13)
+
+ Text(model.selectedDatabase.isEmpty
+ ? "No Schema"
+ : model.selectedDatabase)
+
+ Divider()
+ .frame(height: 13)
+
+ Text("UTF-8")
+ }
+ .font(.system(size: 11))
+ .padding(.horizontal, 12)
+ .frame(height: 22)
+ .background(WB.header)
+ }
+
+ private func sectionHeader(_ title: String) -> some View {
+ HStack {
+ Text(title)
+ .font(.system(size: 11, weight: .bold))
+ .foregroundStyle(.secondary)
+
+ Spacer()
+ }
+ .padding(.horizontal, 12)
+ .frame(height: 30)
+ .background(WB.header)
+ }
+
+ private func edit(_ profile: ConnectionProfile) {
+ editing = profile
+ editingPassword = PasswordStore.read(profile.id)
+ showingEditor = true
+ }
+}
+
+// MARK: - Native SQL Text Editor
+
+struct SQLTextView: NSViewRepresentable {
+ @Binding var text: String
+
+ func makeCoordinator() -> Coordinator {
+ Coordinator(self)
+ }
+
+ func makeNSView(context: Context) -> NSScrollView {
+ let scroll = NSTextView.scrollableTextView()
+ let editor = scroll.documentView as! NSTextView
+
+ scroll.appearance = NSAppearance(named: .aqua)
+ editor.appearance = NSAppearance(named: .aqua)
+ editor.backgroundColor = .white
+ editor.textColor = .black
+ scroll.backgroundColor = .white
+
+ editor.isEditable = true
+ editor.isRichText = false
+ editor.allowsUndo = true
+ editor.isAutomaticQuoteSubstitutionEnabled = false
+ editor.isAutomaticDashSubstitutionEnabled = false
+ editor.font = NSFont.monospacedSystemFont(
+ ofSize: 13,
+ weight: .regular
+ )
+ editor.textContainerInset = NSSize(
+ width: 10,
+ height: 8
+ )
+ editor.delegate = context.coordinator
+ editor.string = text
+
+ context.coordinator.highlight(editor)
+
+ return scroll
+ }
+
+ func updateNSView(
+ _ scroll: NSScrollView,
+ context: Context
+ ) {
+ guard let editor = scroll.documentView as? NSTextView else {
+ return
+ }
+
+ if editor.string != text {
+ let selection = editor.selectedRange()
+ editor.string = text
+
+ let safeLocation = min(
+ selection.location,
+ (text as NSString).length
+ )
+
+ editor.setSelectedRange(
+ NSRange(location: safeLocation, length: 0)
+ )
+
+ context.coordinator.highlight(editor)
+ }
+ }
+
+ final class Coordinator: NSObject, NSTextViewDelegate {
+ var parent: SQLTextView
+
+ init(_ parent: SQLTextView) {
+ self.parent = parent
+ }
+
+ func textDidChange(_ notification: Notification) {
+ guard let editor = notification.object as? NSTextView else {
+ return
+ }
+
+ parent.text = editor.string
+ highlight(editor)
+ }
+
+ func highlight(_ editor: NSTextView) {
+ guard let storage = editor.textStorage else {
+ return
+ }
+
+ let text = editor.string as NSString
+ let range = NSRange(
+ location: 0,
+ length: text.length
+ )
+
+ storage.beginEditing()
+
+ storage.setAttributes([
+ .foregroundColor: NSColor.textColor,
+ .font: NSFont.monospacedSystemFont(
+ ofSize: 13,
+ weight: .regular
+ )
+ ], range: range)
+
+ let pattern = """
+ \\b(SELECT|FROM|WHERE|INSERT|UPDATE|DELETE|CREATE|TABLE|DATABASE|SHOW|USE|JOIN|LEFT|RIGHT|INNER|OUTER|ON|AS|AND|OR|ORDER|GROUP|BY|LIMIT|VALUES|SET|NULL|NOT|IN|LIKE|DISTINCT|ALTER|DROP|DESCRIBE|EXPLAIN)\\b
+ """
+
+ if let regex = try? NSRegularExpression(
+ pattern: pattern,
+ options: [.caseInsensitive]
+ ) {
+ for match in regex.matches(
+ in: editor.string,
+ range: range
+ ) {
+ storage.addAttribute(
+ .foregroundColor,
+ value: NSColor.systemBlue,
+ range: match.range
+ )
+ }
+ }
+
+ storage.endEditing()
+ }
+ }
+}
+
+// MARK: - Connection Editor
+
+struct ConnectionEditor: View {
+ @Binding var profile: ConnectionProfile
+ @Binding var password: String
+
+ let save: () -> Void
+
+ @Environment(\.dismiss) private var dismiss
+
+ var body: some View {
+ VStack(alignment: .leading, spacing: 14) {
+ Text("Connection Settings")
+ .font(.title2.bold())
+
+ Form {
+ TextField(
+ "Connection Name",
+ text: $profile.name
+ )
+
+ TextField(
+ "Hostname",
+ text: $profile.host
+ )
+
+ TextField(
+ "Port",
+ value: $profile.port,
+ format: .number
+ )
+
+ TextField(
+ "Username",
+ text: $profile.username
+ )
+
+ SecureField(
+ "Password (macOS Keychain)",
+ text: $password
+ )
+
+ TextField(
+ "Default Schema",
+ text: $profile.database
+ )
+ }
+
+ HStack {
+ Spacer()
+
+ Button("Cancel") {
+ dismiss()
+ }
+
+ Button("Save") {
+ save()
+ }
+ .buttonStyle(.borderedProminent)
+ }
+ }
+ .padding(24)
+ .frame(width: 520)
+ }
+}
diff --git a/WorkbenchReplacement/MainView.swift.backup-20261016-171209 b/WorkbenchReplacement/MainView.swift.backup-20261016-171209
new file mode 100644
index 0000000..0e57561
--- /dev/null
+++ b/WorkbenchReplacement/MainView.swift.backup-20261016-171209
@@ -0,0 +1,861 @@
+
+import SwiftUI
+import AppKit
+
+// MARK: - Classic Workbench Colours
+
+private enum WB {
+ static let toolbar = Color(nsColor: NSColor(
+ calibratedWhite: 0.88, alpha: 1
+ ))
+ static let sidebar = Color(nsColor: NSColor(
+ calibratedWhite: 0.94, alpha: 1
+ ))
+ static let border = Color(nsColor: NSColor(
+ calibratedWhite: 0.76, alpha: 1
+ ))
+ static let header = Color(nsColor: NSColor(
+ calibratedWhite: 0.91, alpha: 1
+ ))
+}
+
+// MARK: - Main Window
+
+struct MainView: View {
+ @EnvironmentObject private var model: DatabaseModel
+
+ @State private var showingEditor = false
+ @State private var editing = ConnectionProfile()
+ @State private var editingPassword = ""
+
+ @State private var expandedSchemas: Set = []
+ @State private var outputExpanded = true
+
+ var body: some View {
+ VStack(spacing: 0) {
+ mainToolbar
+
+ Rectangle()
+ .fill(WB.border)
+ .frame(height: 1)
+
+ HSplitView {
+ navigator
+ .frame(
+ minWidth: 210,
+ idealWidth: 265,
+ maxWidth: 420
+ )
+
+ workspace
+ .frame(minWidth: 600)
+ }
+
+ statusBar
+ }
+ .frame(minWidth: 1100, minHeight: 720)
+ .preferredColorScheme(.light)
+ .background(Color(nsColor: .windowBackgroundColor))
+ .sheet(isPresented: $showingEditor) {
+ ConnectionEditor(
+ profile: $editing,
+ password: $editingPassword
+ ) {
+ model.save(editing, password: editingPassword)
+ showingEditor = false
+ }
+ }
+ }
+
+ // MARK: - Main Toolbar
+
+ private var mainToolbar: some View {
+ HStack(spacing: 16) {
+ Image(systemName: "externaldrive.connected.to.line.below")
+ .font(.system(size: 23))
+ .foregroundStyle(.blue)
+
+ Text("MySQL Workbench")
+ .font(.system(size: 15, weight: .semibold))
+ .foregroundStyle(.black)
+
+ Rectangle()
+ .fill(WB.border)
+ .frame(width: 1, height: 30)
+
+ toolbarButton(
+ "New Connection",
+ icon: "plus.rectangle.on.rectangle"
+ ) {
+ editing = ConnectionProfile(name: "New Connection")
+ editingPassword = ""
+ showingEditor = true
+ }
+
+ toolbarButton(
+ "Refresh",
+ icon: "arrow.clockwise"
+ ) {
+ model.loadDatabases()
+ }
+
+ toolbarButton(
+ "Execute",
+ icon: "bolt.fill"
+ ) {
+ model.runSQL()
+ }
+ .keyboardShortcut(.return, modifiers: [.command])
+
+ toolbarButton(
+ "Format SQL",
+ icon: "text.alignleft"
+ ) {
+ model.formatSQL()
+ }
+
+ Spacer()
+
+ VStack(alignment: .trailing, spacing: 2) {
+ Text(model.selected?.name ?? "No Connection")
+ .font(.system(size: 12, weight: .semibold))
+
+ Text(model.selectedDatabase.isEmpty
+ ? "No default schema"
+ : model.selectedDatabase)
+ .font(.system(size: 11))
+ .foregroundStyle(.secondary)
+ }
+ }
+ .padding(.horizontal, 10)
+ .frame(height: 46)
+ .background(WB.toolbar)
+ }
+
+ private func toolbarButton(
+ _ title: String,
+ icon: String,
+ action: @escaping () -> Void
+ ) -> some View {
+ Button(action: action) {
+ VStack(spacing: 3) {
+ Image(systemName: icon)
+ .font(.system(size: 19))
+ .frame(height: 23)
+
+ Text(title)
+ .font(.system(size: 10))
+ }
+ .foregroundStyle(.black)
+ .frame(minWidth: 65)
+ }
+ .buttonStyle(.plain)
+ .help(title)
+ }
+
+ // MARK: - Navigator
+
+ private var navigator: some View {
+ VStack(spacing: 0) {
+ sectionHeader("NAVIGATOR")
+
+ HStack {
+ Text("SCHEMAS")
+ .font(.system(size: 11, weight: .bold))
+
+ Spacer()
+
+ Button {
+ model.loadDatabases()
+ } label: {
+ Image(systemName: "arrow.clockwise")
+ }
+ .buttonStyle(.borderless)
+ }
+ .padding(.horizontal, 12)
+ .frame(height: 34)
+
+ Divider()
+
+ ScrollView {
+ LazyVStack(alignment: .leading, spacing: 2) {
+ ForEach(model.databases, id: \.self) { db in
+ schemaNode(db)
+ }
+ }
+ .padding(.vertical, 8)
+ }
+
+ Divider()
+
+ sectionHeader("MYSQL CONNECTIONS")
+
+ ScrollView {
+ VStack(alignment: .leading, spacing: 4) {
+ ForEach(model.profiles) { profile in
+ Button {
+ model.selectedID = profile.id
+ model.loadDatabases()
+ } label: {
+ Label(
+ profile.name,
+ systemImage: "server.rack"
+ )
+ .frame(
+ maxWidth: .infinity,
+ alignment: .leading
+ )
+ .padding(6)
+ .background(
+ model.selectedID == profile.id
+ ? Color.blue.opacity(0.14)
+ : Color.clear
+ )
+ }
+ .buttonStyle(.plain)
+ .contextMenu {
+ Button("Edit Connection") {
+ edit(profile)
+ }
+ }
+ }
+ }
+ .padding(8)
+ }
+ .frame(height: 125)
+ }
+ .background(WB.sidebar)
+ }
+
+ private func schemaNode(_ db: String) -> some View {
+ VStack(alignment: .leading, spacing: 2) {
+ HStack(spacing: 5) {
+ Button {
+ toggleSchema(db)
+ } label: {
+ Image(systemName:
+ expandedSchemas.contains(db)
+ ? "chevron.down"
+ : "chevron.right"
+ )
+ .font(.system(size: 10))
+ .frame(width: 15)
+ }
+ .buttonStyle(.plain)
+
+ Image(systemName: "cylinder.split.1x2")
+ .foregroundStyle(.blue)
+
+ Text(db)
+ .font(.system(size: 12))
+ .fontWeight(
+ model.selectedDatabase == db
+ ? .bold : .regular
+ )
+
+ Spacer()
+ }
+ .padding(.horizontal, 10)
+ .padding(.vertical, 5)
+ .background(
+ model.selectedDatabase == db
+ ? Color.blue.opacity(0.13)
+ : Color.clear
+ )
+ .contentShape(Rectangle())
+ .onTapGesture(count: 2) {
+ model.selectedDatabase = db
+ expandedSchemas.insert(db)
+ model.loadTables(database: db)
+ }
+ .contextMenu {
+ Button("Set as Default Schema") {
+ model.selectedDatabase = db
+ }
+
+ Button("Refresh Tables") {
+ model.loadTables(database: db)
+ }
+ }
+
+ if expandedSchemas.contains(db) {
+ schemaChildren(db)
+ .padding(.leading, 24)
+ }
+ }
+ }
+
+ private func toggleSchema(_ db: String) {
+ if expandedSchemas.contains(db) {
+ expandedSchemas.remove(db)
+ } else {
+ expandedSchemas.insert(db)
+
+ if model.schemaTables[db] == nil {
+ model.loadTables(database: db)
+ }
+ }
+ }
+
+ private func schemaChildren(_ db: String) -> some View {
+ VStack(alignment: .leading, spacing: 5) {
+ DisclosureGroup {
+ if let tables = model.schemaTables[db] {
+ ForEach(tables, id: \.self) { table in
+ Label(table, systemImage: "tablecells")
+ .font(.system(size: 12))
+ .padding(.vertical, 3)
+ .onTapGesture(count: 2) {
+ model.selectTable(
+ database: db,
+ table: table
+ )
+ }
+ .contextMenu {
+ Button("Select First 100 Rows") {
+ model.selectTable(
+ database: db,
+ table: table
+ )
+ }
+ }
+ }
+ } else {
+ Button("Load Tables") {
+ model.loadTables(database: db)
+ }
+ .buttonStyle(.borderless)
+ }
+ } label: {
+ Label("Tables", systemImage: "folder")
+ }
+
+ Label("Views", systemImage: "folder")
+ Label("Stored Procedures", systemImage: "folder")
+ Label("Functions", systemImage: "folder")
+ }
+ .font(.system(size: 12))
+ .padding(.vertical, 3)
+ }
+
+ // MARK: - Workspace
+
+ private var workspace: some View {
+ VStack(spacing: 0) {
+ queryTabBar
+
+ Divider()
+
+ queryToolbar
+
+ Divider()
+
+ VSplitView {
+ sqlEditor
+ .frame(minHeight: 230)
+
+ bottomWorkspace
+ .frame(minHeight: 230)
+ }
+ }
+ }
+
+ private var queryTabBar: some View {
+ HStack(spacing: 0) {
+ HStack(spacing: 8) {
+ Image(systemName: "doc.text")
+ .foregroundStyle(.blue)
+
+ Text("Query 1")
+ .font(.system(size: 12, weight: .semibold))
+
+ Image(systemName: "xmark")
+ .font(.system(size: 9))
+ .foregroundStyle(.secondary)
+ }
+ .padding(.horizontal, 10)
+ .frame(height: 29)
+ .background(Color(nsColor: .textBackgroundColor))
+
+ Spacer()
+ }
+ .background(WB.header)
+ }
+
+ private var queryToolbar: some View {
+ HStack(spacing: 14) {
+ Button {
+ model.runSQL()
+ } label: {
+ Image(systemName: "bolt.fill")
+ .foregroundStyle(.orange)
+ }
+ .help("Execute SQL")
+
+ Button {
+ model.formatSQL()
+ } label: {
+ Image(systemName: "text.alignleft")
+ }
+ .help("Format SQL")
+
+ Divider()
+ .frame(height: 17)
+
+ Text("Default Schema:")
+ .font(.system(size: 11))
+
+ Text(model.selectedDatabase.isEmpty
+ ? "None"
+ : model.selectedDatabase)
+ .font(.system(size: 11, weight: .semibold))
+
+ Spacer()
+
+ Text("SQL Editor")
+ .font(.system(size: 11))
+ .foregroundStyle(.secondary)
+ }
+ .buttonStyle(.borderless)
+ .padding(.horizontal, 12)
+ .frame(height: 30)
+ .background(WB.header)
+ }
+
+ private var sqlEditor: some View {
+ HStack(spacing: 0) {
+ lineNumbers
+
+ Rectangle()
+ .fill(WB.border)
+ .frame(width: 1)
+
+ SQLTextView(text: $model.sql)
+ }
+ .background(Color(nsColor: .textBackgroundColor))
+ }
+
+ private var lineNumbers: some View {
+ ScrollView {
+ VStack(alignment: .trailing, spacing: 0) {
+ ForEach(
+ 1...max(1, model.sql.components(
+ separatedBy: "\n"
+ ).count),
+ id: \.self
+ ) { number in
+ Text("\(number)")
+ .frame(height: 19)
+ }
+ }
+ .padding(.horizontal, 9)
+ .padding(.top, 8)
+ }
+ .font(.system(size: 12, design: .monospaced))
+ .foregroundStyle(.secondary)
+ .frame(width: 48)
+ .background(WB.header)
+ .allowsHitTesting(false)
+ }
+
+ // MARK: - Results and Action Output
+
+ private var bottomWorkspace: some View {
+ VSplitView {
+ resultsPanel
+ .frame(minHeight: 230)
+
+ if outputExpanded {
+ actionOutput
+ .frame(minHeight: 90)
+ }
+ }
+ }
+
+ private var resultsPanel: some View {
+ VStack(spacing: 0) {
+ HStack {
+ Image(systemName: "tablecells")
+ Text("Result Grid")
+ .fontWeight(.semibold)
+
+ Spacer()
+
+ Text(model.result.message)
+ .foregroundStyle(.secondary)
+ }
+ .font(.system(size: 12))
+ .padding(.horizontal, 12)
+ .frame(height: 28)
+ .background(WB.header)
+
+ Divider()
+
+ if model.result.columns.isEmpty {
+ VStack {
+ Spacer()
+ Text("No query results")
+ .foregroundStyle(.secondary)
+ Spacer()
+ }
+ } else {
+ resultGrid
+ }
+ }
+ }
+
+ private var resultGrid: some View {
+ ScrollView([.horizontal, .vertical]) {
+ Grid(
+ alignment: .leading,
+ horizontalSpacing: 0,
+ verticalSpacing: 0
+ ) {
+ GridRow {
+ ForEach(
+ model.result.columns.indices,
+ id: \.self
+ ) { column in
+ Text(model.result.columns[column])
+ .fontWeight(.semibold)
+ .frame(
+ width: 170,
+ alignment: .leading
+ )
+ .padding(7)
+ .background(WB.header)
+ }
+ }
+
+ ForEach(
+ model.result.rows.indices,
+ id: \.self
+ ) { row in
+ GridRow {
+ ForEach(
+ model.result.columns.indices,
+ id: \.self
+ ) { column in
+ Text(
+ column < model.result.rows[row].count
+ ? model.result.rows[row][column]
+ : ""
+ )
+ .lineLimit(1)
+ .frame(
+ width: 170,
+ alignment: .leading
+ )
+ .padding(7)
+ .background(
+ row.isMultiple(of: 2)
+ ? Color(nsColor: .textBackgroundColor)
+ : WB.sidebar
+ )
+ }
+ }
+ }
+ }
+ .font(.system(size: 12, design: .monospaced))
+ }
+ }
+
+ private var actionOutput: some View {
+ VStack(spacing: 0) {
+ HStack {
+ Image(systemName: "list.bullet.rectangle")
+ Text("Action Output")
+ .fontWeight(.semibold)
+
+ Spacer()
+
+ Button {
+ outputExpanded = false
+ } label: {
+ Image(systemName: "chevron.down")
+ }
+ .buttonStyle(.borderless)
+ }
+ .font(.system(size: 12))
+ .padding(.horizontal, 12)
+ .frame(height: 26)
+ .background(WB.header)
+
+ Divider()
+
+ HStack(alignment: .top, spacing: 12) {
+ Image(systemName:
+ model.status == "Error"
+ ? "xmark.circle.fill"
+ : "checkmark.circle.fill"
+ )
+ .foregroundStyle(
+ model.status == "Error"
+ ? .red : .green
+ )
+
+ VStack(alignment: .leading, spacing: 5) {
+ Text(model.status)
+ .fontWeight(.semibold)
+
+ Text(model.result.message)
+ .foregroundStyle(.secondary)
+ }
+
+ Spacer()
+ }
+ .font(.system(size: 12))
+ .padding(12)
+
+ Spacer()
+ }
+ }
+
+ // MARK: - Status Bar
+
+ private var statusBar: some View {
+ HStack(spacing: 10) {
+ Circle()
+ .fill(
+ model.status == "Error"
+ ? Color.red : Color.green
+ )
+ .frame(width: 8, height: 8)
+
+ Text(model.status)
+ .lineLimit(1)
+
+ Spacer()
+
+ Text(model.selected?.name ?? "Disconnected")
+
+ Divider()
+ .frame(height: 13)
+
+ Text(model.selectedDatabase.isEmpty
+ ? "No Schema"
+ : model.selectedDatabase)
+
+ Divider()
+ .frame(height: 13)
+
+ Text("UTF-8")
+ }
+ .font(.system(size: 11))
+ .padding(.horizontal, 12)
+ .frame(height: 22)
+ .background(WB.header)
+ }
+
+ private func sectionHeader(_ title: String) -> some View {
+ HStack {
+ Text(title)
+ .font(.system(size: 11, weight: .bold))
+ .foregroundStyle(.secondary)
+
+ Spacer()
+ }
+ .padding(.horizontal, 12)
+ .frame(height: 30)
+ .background(WB.header)
+ }
+
+ private func edit(_ profile: ConnectionProfile) {
+ editing = profile
+ editingPassword = PasswordStore.read(profile.id)
+ showingEditor = true
+ }
+}
+
+// MARK: - Native SQL Text Editor
+
+struct SQLTextView: NSViewRepresentable {
+ @Binding var text: String
+
+ func makeCoordinator() -> Coordinator {
+ Coordinator(self)
+ }
+
+ func makeNSView(context: Context) -> NSScrollView {
+ let scroll = NSTextView.scrollableTextView()
+ let editor = scroll.documentView as! NSTextView
+
+ scroll.appearance = NSAppearance(named: .aqua)
+ editor.appearance = NSAppearance(named: .aqua)
+ editor.backgroundColor = .white
+ editor.textColor = .black
+ scroll.backgroundColor = .white
+
+ editor.isEditable = true
+ editor.isRichText = false
+ editor.allowsUndo = true
+ editor.isAutomaticQuoteSubstitutionEnabled = false
+ editor.isAutomaticDashSubstitutionEnabled = false
+ editor.font = NSFont.monospacedSystemFont(
+ ofSize: 13,
+ weight: .regular
+ )
+ editor.textContainerInset = NSSize(
+ width: 10,
+ height: 8
+ )
+ editor.delegate = context.coordinator
+ editor.string = text
+
+ context.coordinator.highlight(editor)
+
+ return scroll
+ }
+
+ func updateNSView(
+ _ scroll: NSScrollView,
+ context: Context
+ ) {
+ guard let editor = scroll.documentView as? NSTextView else {
+ return
+ }
+
+ if editor.string != text {
+ let selection = editor.selectedRange()
+ editor.string = text
+
+ let safeLocation = min(
+ selection.location,
+ (text as NSString).length
+ )
+
+ editor.setSelectedRange(
+ NSRange(location: safeLocation, length: 0)
+ )
+
+ context.coordinator.highlight(editor)
+ }
+ }
+
+ final class Coordinator: NSObject, NSTextViewDelegate {
+ var parent: SQLTextView
+
+ init(_ parent: SQLTextView) {
+ self.parent = parent
+ }
+
+ func textDidChange(_ notification: Notification) {
+ guard let editor = notification.object as? NSTextView else {
+ return
+ }
+
+ parent.text = editor.string
+ highlight(editor)
+ }
+
+ func highlight(_ editor: NSTextView) {
+ guard let storage = editor.textStorage else {
+ return
+ }
+
+ let text = editor.string as NSString
+ let range = NSRange(
+ location: 0,
+ length: text.length
+ )
+
+ storage.beginEditing()
+
+ storage.setAttributes([
+ .foregroundColor: NSColor.textColor,
+ .font: NSFont.monospacedSystemFont(
+ ofSize: 13,
+ weight: .regular
+ )
+ ], range: range)
+
+ let pattern = """
+ \\b(SELECT|FROM|WHERE|INSERT|UPDATE|DELETE|CREATE|TABLE|DATABASE|SHOW|USE|JOIN|LEFT|RIGHT|INNER|OUTER|ON|AS|AND|OR|ORDER|GROUP|BY|LIMIT|VALUES|SET|NULL|NOT|IN|LIKE|DISTINCT|ALTER|DROP|DESCRIBE|EXPLAIN)\\b
+ """
+
+ if let regex = try? NSRegularExpression(
+ pattern: pattern,
+ options: [.caseInsensitive]
+ ) {
+ for match in regex.matches(
+ in: editor.string,
+ range: range
+ ) {
+ storage.addAttribute(
+ .foregroundColor,
+ value: NSColor.systemBlue,
+ range: match.range
+ )
+ }
+ }
+
+ storage.endEditing()
+ }
+ }
+}
+
+// MARK: - Connection Editor
+
+struct ConnectionEditor: View {
+ @Binding var profile: ConnectionProfile
+ @Binding var password: String
+
+ let save: () -> Void
+
+ @Environment(\.dismiss) private var dismiss
+
+ var body: some View {
+ VStack(alignment: .leading, spacing: 14) {
+ Text("Connection Settings")
+ .font(.title2.bold())
+
+ Form {
+ TextField(
+ "Connection Name",
+ text: $profile.name
+ )
+
+ TextField(
+ "Hostname",
+ text: $profile.host
+ )
+
+ TextField(
+ "Port",
+ value: $profile.port,
+ format: .number
+ )
+
+ TextField(
+ "Username",
+ text: $profile.username
+ )
+
+ SecureField(
+ "Password (macOS Keychain)",
+ text: $password
+ )
+
+ TextField(
+ "Default Schema",
+ text: $profile.database
+ )
+ }
+
+ HStack {
+ Spacer()
+
+ Button("Cancel") {
+ dismiss()
+ }
+
+ Button("Save") {
+ save()
+ }
+ .buttonStyle(.borderedProminent)
+ }
+ }
+ .padding(24)
+ .frame(width: 520)
+ }
+}
diff --git a/WorkbenchReplacement/MainView.swift.backup-20261016-171257 b/WorkbenchReplacement/MainView.swift.backup-20261016-171257
new file mode 100644
index 0000000..d4eb648
--- /dev/null
+++ b/WorkbenchReplacement/MainView.swift.backup-20261016-171257
@@ -0,0 +1,883 @@
+
+import SwiftUI
+import AppKit
+
+// MARK: - Classic Workbench Colours
+
+private enum WB {
+ static let toolbar = Color(nsColor: NSColor(
+ calibratedWhite: 0.88, alpha: 1
+ ))
+ static let sidebar = Color(nsColor: NSColor(
+ calibratedWhite: 0.94, alpha: 1
+ ))
+ static let border = Color(nsColor: NSColor(
+ calibratedWhite: 0.76, alpha: 1
+ ))
+ static let header = Color(nsColor: NSColor(
+ calibratedWhite: 0.91, alpha: 1
+ ))
+}
+
+// MARK: - Main Window
+
+struct MainView: View {
+ @EnvironmentObject private var model: DatabaseModel
+
+ @State private var showingEditor = false
+ @State private var editing = ConnectionProfile()
+ @State private var editingPassword = ""
+
+ @State private var expandedSchemas: Set = []
+ @State private var outputExpanded = true
+
+ var body: some View {
+ VStack(spacing: 0) {
+ mainToolbar
+
+ Rectangle()
+ .fill(WB.border)
+ .frame(height: 1)
+
+ HSplitView {
+ navigator
+ .frame(
+ minWidth: 210,
+ idealWidth: 265,
+ maxWidth: 420
+ )
+
+ workspace
+ .frame(minWidth: 600)
+ }
+
+ statusBar
+ }
+ .frame(minWidth: 1100, minHeight: 720)
+ .preferredColorScheme(.light)
+ .background(Color(nsColor: .windowBackgroundColor))
+ .sheet(isPresented: $showingEditor) {
+ ConnectionEditor(
+ profile: $editing,
+ password: $editingPassword
+ ) {
+ model.save(editing, password: editingPassword)
+ showingEditor = false
+ }
+ }
+ }
+
+ // MARK: - Main Toolbar
+
+ private var mainToolbar: some View {
+ HStack(spacing: 16) {
+ Image(systemName: "externaldrive.connected.to.line.below")
+ .font(.system(size: 23))
+ .foregroundStyle(.blue)
+
+ Text("MySQL Workbench")
+ .font(.system(size: 15, weight: .semibold))
+ .foregroundStyle(.black)
+
+ Rectangle()
+ .fill(WB.border)
+ .frame(width: 1, height: 30)
+
+ toolbarButton(
+ "New Connection",
+ icon: "plus.rectangle.on.rectangle"
+ ) {
+ editing = ConnectionProfile(name: "New Connection")
+ editingPassword = ""
+ showingEditor = true
+ }
+
+ toolbarButton(
+ "Refresh",
+ icon: "arrow.clockwise"
+ ) {
+ model.loadDatabases()
+ }
+
+ toolbarButton(
+ "Execute",
+ icon: "bolt.fill"
+ ) {
+ model.runSQL()
+ }
+ .keyboardShortcut(.return, modifiers: [.command])
+
+ toolbarButton(
+ "Format SQL",
+ icon: "text.alignleft"
+ ) {
+ model.formatSQL()
+ }
+
+ Spacer()
+
+ VStack(alignment: .trailing, spacing: 2) {
+ Text(model.selected?.name ?? "No Connection")
+ .font(.system(size: 12, weight: .semibold))
+
+ Text(model.selectedDatabase.isEmpty
+ ? "No default schema"
+ : model.selectedDatabase)
+ .font(.system(size: 11))
+ .foregroundStyle(.secondary)
+ }
+ }
+ .padding(.horizontal, 10)
+ .frame(height: 46)
+ .background(WB.toolbar)
+ }
+
+ private func toolbarButton(
+ _ title: String,
+ icon: String,
+ action: @escaping () -> Void
+ ) -> some View {
+ Button(action: action) {
+ VStack(spacing: 3) {
+ Image(systemName: icon)
+ .font(.system(size: 19))
+ .frame(height: 23)
+
+ Text(title)
+ .font(.system(size: 10))
+ }
+ .foregroundStyle(.black)
+ .frame(minWidth: 65)
+ }
+ .buttonStyle(.plain)
+ .help(title)
+ }
+
+ // MARK: - Navigator
+
+ private var navigator: some View {
+ VStack(spacing: 0) {
+ sectionHeader("NAVIGATOR")
+
+ HStack {
+ Text("SCHEMAS")
+ .font(.system(size: 11, weight: .bold))
+
+ Spacer()
+
+ Button {
+ model.loadDatabases()
+ } label: {
+ Image(systemName: "arrow.clockwise")
+ }
+ .buttonStyle(.borderless)
+ }
+ .padding(.horizontal, 12)
+ .frame(height: 34)
+
+ Divider()
+
+ ScrollView {
+ LazyVStack(alignment: .leading, spacing: 2) {
+ ForEach(model.databases, id: \.self) { db in
+ schemaNode(db)
+ }
+ }
+ .padding(.vertical, 8)
+ }
+
+ Divider()
+
+ sectionHeader("MYSQL CONNECTIONS")
+
+ ScrollView {
+ VStack(alignment: .leading, spacing: 4) {
+ ForEach(model.profiles) { profile in
+ Button {
+ model.selectedID = profile.id
+ model.loadDatabases()
+ } label: {
+ Label(
+ profile.name,
+ systemImage: "server.rack"
+ )
+ .frame(
+ maxWidth: .infinity,
+ alignment: .leading
+ )
+ .padding(6)
+ .background(
+ model.selectedID == profile.id
+ ? Color.blue.opacity(0.14)
+ : Color.clear
+ )
+ }
+ .buttonStyle(.plain)
+ .contextMenu {
+ Button("Edit Connection") {
+ edit(profile)
+ }
+ }
+ }
+ }
+ .padding(8)
+ }
+ .frame(height: 125)
+ }
+ .background(WB.sidebar)
+ }
+
+ private func schemaNode(_ db: String) -> some View {
+ VStack(alignment: .leading, spacing: 2) {
+ HStack(spacing: 5) {
+ Button {
+ toggleSchema(db)
+ } label: {
+ Image(systemName:
+ expandedSchemas.contains(db)
+ ? "chevron.down"
+ : "chevron.right"
+ )
+ .font(.system(size: 10))
+ .frame(width: 15)
+ }
+ .buttonStyle(.plain)
+
+ Image(systemName: "cylinder.split.1x2")
+ .foregroundStyle(.blue)
+
+ Text(db)
+ .font(.system(size: 12))
+ .fontWeight(
+ model.selectedDatabase == db
+ ? .bold : .regular
+ )
+
+ Spacer()
+ }
+ .padding(.horizontal, 10)
+ .padding(.vertical, 5)
+ .background(
+ model.selectedDatabase == db
+ ? Color.blue.opacity(0.13)
+ : Color.clear
+ )
+ .contentShape(Rectangle())
+ .onTapGesture(count: 2) {
+ model.selectedDatabase = db
+ expandedSchemas.insert(db)
+ model.loadTables(database: db)
+ }
+ .contextMenu {
+ Button("Set as Default Schema") {
+ model.selectedDatabase = db
+ }
+
+ Button("Refresh Tables") {
+ model.loadTables(database: db)
+ }
+ }
+
+ if expandedSchemas.contains(db) {
+ schemaChildren(db)
+ .padding(.leading, 24)
+ }
+ }
+ }
+
+ private func toggleSchema(_ db: String) {
+ if expandedSchemas.contains(db) {
+ expandedSchemas.remove(db)
+ } else {
+ expandedSchemas.insert(db)
+
+ if model.schemaTables[db] == nil {
+ model.loadTables(database: db)
+ }
+ }
+ }
+
+
+ private func schemaChildren(_ db: String) -> some View {
+ VStack(alignment: .leading, spacing: 2) {
+
+ DisclosureGroup {
+ if let tables = model.schemaTables[db] {
+ ForEach(tables, id: \\.self) { table in
+ HStack(spacing: 6) {
+ Image(systemName: "tablecells")
+ .foregroundStyle(.secondary)
+ .frame(width: 16)
+
+ Text(table)
+ .lineLimit(1)
+ .truncationMode(.middle)
+
+ Spacer(minLength: 0)
+ }
+ .frame(maxWidth: .infinity, alignment: .leading)
+ .padding(.leading, 4)
+ .padding(.vertical, 3)
+ .contentShape(Rectangle())
+ .onTapGesture(count: 2) {
+ model.selectTable(
+ database: db,
+ table: table
+ )
+ }
+ .contextMenu {
+ Button("Select First 100 Rows") {
+ model.selectTable(
+ database: db,
+ table: table
+ )
+ }
+ }
+ }
+ } else {
+ Button("Load Tables") {
+ model.loadTables(database: db)
+ }
+ .frame(maxWidth: .infinity, alignment: .leading)
+ .buttonStyle(.borderless)
+ }
+ } label: {
+ Label("Tables", systemImage: "folder")
+ .frame(maxWidth: .infinity, alignment: .leading)
+ }
+
+ Label("Views", systemImage: "folder")
+ .frame(maxWidth: .infinity, alignment: .leading)
+
+ Label("Stored Procedures", systemImage: "folder")
+ .frame(maxWidth: .infinity, alignment: .leading)
+
+ Label("Functions", systemImage: "folder")
+ .frame(maxWidth: .infinity, alignment: .leading)
+ }
+ .frame(maxWidth: .infinity, alignment: .leading)
+ .font(.system(size: 12))
+ .padding(.vertical, 3)
+ }
+
+ // MARK: - Workspace
+
+ private var workspace: some View {
+ VStack(spacing: 0) {
+ queryTabBar
+
+ Divider()
+
+ queryToolbar
+
+ Divider()
+
+ VSplitView {
+ sqlEditor
+ .frame(minHeight: 230)
+
+ bottomWorkspace
+ .frame(minHeight: 230)
+ }
+ }
+ }
+
+ private var queryTabBar: some View {
+ HStack(spacing: 0) {
+ HStack(spacing: 8) {
+ Image(systemName: "doc.text")
+ .foregroundStyle(.blue)
+
+ Text("Query 1")
+ .font(.system(size: 12, weight: .semibold))
+
+ Image(systemName: "xmark")
+ .font(.system(size: 9))
+ .foregroundStyle(.secondary)
+ }
+ .padding(.horizontal, 10)
+ .frame(height: 29)
+ .background(Color(nsColor: .textBackgroundColor))
+
+ Spacer()
+ }
+ .background(WB.header)
+ }
+
+ private var queryToolbar: some View {
+ HStack(spacing: 14) {
+ Button {
+ model.runSQL()
+ } label: {
+ Image(systemName: "bolt.fill")
+ .foregroundStyle(.orange)
+ }
+ .help("Execute SQL")
+
+ Button {
+ model.formatSQL()
+ } label: {
+ Image(systemName: "text.alignleft")
+ }
+ .help("Format SQL")
+
+ Divider()
+ .frame(height: 17)
+
+ Text("Default Schema:")
+ .font(.system(size: 11))
+
+ Text(model.selectedDatabase.isEmpty
+ ? "None"
+ : model.selectedDatabase)
+ .font(.system(size: 11, weight: .semibold))
+
+ Spacer()
+
+ Text("SQL Editor")
+ .font(.system(size: 11))
+ .foregroundStyle(.secondary)
+ }
+ .buttonStyle(.borderless)
+ .padding(.horizontal, 12)
+ .frame(height: 30)
+ .background(WB.header)
+ }
+
+ private var sqlEditor: some View {
+ HStack(spacing: 0) {
+ lineNumbers
+
+ Rectangle()
+ .fill(WB.border)
+ .frame(width: 1)
+
+ SQLTextView(text: $model.sql)
+ }
+ .background(Color(nsColor: .textBackgroundColor))
+ }
+
+ private var lineNumbers: some View {
+ ScrollView {
+ VStack(alignment: .trailing, spacing: 0) {
+ ForEach(
+ 1...max(1, model.sql.components(
+ separatedBy: "\n"
+ ).count),
+ id: \.self
+ ) { number in
+ Text("\(number)")
+ .frame(height: 19)
+ }
+ }
+ .padding(.horizontal, 9)
+ .padding(.top, 8)
+ }
+ .font(.system(size: 12, design: .monospaced))
+ .foregroundStyle(.secondary)
+ .frame(width: 48)
+ .background(WB.header)
+ .allowsHitTesting(false)
+ }
+
+ // MARK: - Results and Action Output
+
+ private var bottomWorkspace: some View {
+ VSplitView {
+ resultsPanel
+ .frame(minHeight: 230)
+
+ if outputExpanded {
+ actionOutput
+ .frame(minHeight: 90)
+ }
+ }
+ }
+
+ private var resultsPanel: some View {
+ VStack(spacing: 0) {
+ HStack {
+ Image(systemName: "tablecells")
+ Text("Result Grid")
+ .fontWeight(.semibold)
+
+ Spacer()
+
+ Text(model.result.message)
+ .foregroundStyle(.secondary)
+ }
+ .font(.system(size: 12))
+ .padding(.horizontal, 12)
+ .frame(height: 28)
+ .background(WB.header)
+
+ Divider()
+
+ if model.result.columns.isEmpty {
+ VStack {
+ Spacer()
+ Text("No query results")
+ .foregroundStyle(.secondary)
+ Spacer()
+ }
+ } else {
+ resultGrid
+ }
+ }
+ }
+
+ private var resultGrid: some View {
+ ScrollView([.horizontal, .vertical]) {
+ Grid(
+ alignment: .leading,
+ horizontalSpacing: 0,
+ verticalSpacing: 0
+ ) {
+ GridRow {
+ ForEach(
+ model.result.columns.indices,
+ id: \.self
+ ) { column in
+ Text(model.result.columns[column])
+ .fontWeight(.semibold)
+ .frame(
+ width: 170,
+ alignment: .leading
+ )
+ .padding(7)
+ .background(WB.header)
+ }
+ }
+
+ ForEach(
+ model.result.rows.indices,
+ id: \.self
+ ) { row in
+ GridRow {
+ ForEach(
+ model.result.columns.indices,
+ id: \.self
+ ) { column in
+ Text(
+ column < model.result.rows[row].count
+ ? model.result.rows[row][column]
+ : ""
+ )
+ .lineLimit(1)
+ .frame(
+ width: 170,
+ alignment: .leading
+ )
+ .padding(7)
+ .background(
+ row.isMultiple(of: 2)
+ ? Color(nsColor: .textBackgroundColor)
+ : WB.sidebar
+ )
+ }
+ }
+ }
+ }
+ .font(.system(size: 12, design: .monospaced))
+ }
+ }
+
+ private var actionOutput: some View {
+ VStack(spacing: 0) {
+ HStack {
+ Image(systemName: "list.bullet.rectangle")
+ Text("Action Output")
+ .fontWeight(.semibold)
+
+ Spacer()
+
+ Button {
+ outputExpanded = false
+ } label: {
+ Image(systemName: "chevron.down")
+ }
+ .buttonStyle(.borderless)
+ }
+ .font(.system(size: 12))
+ .padding(.horizontal, 12)
+ .frame(height: 26)
+ .background(WB.header)
+
+ Divider()
+
+ HStack(alignment: .top, spacing: 12) {
+ Image(systemName:
+ model.status == "Error"
+ ? "xmark.circle.fill"
+ : "checkmark.circle.fill"
+ )
+ .foregroundStyle(
+ model.status == "Error"
+ ? .red : .green
+ )
+
+ VStack(alignment: .leading, spacing: 5) {
+ Text(model.status)
+ .fontWeight(.semibold)
+
+ Text(model.result.message)
+ .foregroundStyle(.secondary)
+ }
+
+ Spacer()
+ }
+ .font(.system(size: 12))
+ .padding(12)
+
+ Spacer()
+ }
+ }
+
+ // MARK: - Status Bar
+
+ private var statusBar: some View {
+ HStack(spacing: 10) {
+ Circle()
+ .fill(
+ model.status == "Error"
+ ? Color.red : Color.green
+ )
+ .frame(width: 8, height: 8)
+
+ Text(model.status)
+ .lineLimit(1)
+
+ Spacer()
+
+ Text(model.selected?.name ?? "Disconnected")
+
+ Divider()
+ .frame(height: 13)
+
+ Text(model.selectedDatabase.isEmpty
+ ? "No Schema"
+ : model.selectedDatabase)
+
+ Divider()
+ .frame(height: 13)
+
+ Text("UTF-8")
+ }
+ .font(.system(size: 11))
+ .padding(.horizontal, 12)
+ .frame(height: 22)
+ .background(WB.header)
+ }
+
+ private func sectionHeader(_ title: String) -> some View {
+ HStack {
+ Text(title)
+ .font(.system(size: 11, weight: .bold))
+ .foregroundStyle(.secondary)
+
+ Spacer()
+ }
+ .padding(.horizontal, 12)
+ .frame(height: 30)
+ .background(WB.header)
+ }
+
+ private func edit(_ profile: ConnectionProfile) {
+ editing = profile
+ editingPassword = PasswordStore.read(profile.id)
+ showingEditor = true
+ }
+}
+
+// MARK: - Native SQL Text Editor
+
+struct SQLTextView: NSViewRepresentable {
+ @Binding var text: String
+
+ func makeCoordinator() -> Coordinator {
+ Coordinator(self)
+ }
+
+ func makeNSView(context: Context) -> NSScrollView {
+ let scroll = NSTextView.scrollableTextView()
+ let editor = scroll.documentView as! NSTextView
+
+ scroll.appearance = NSAppearance(named: .aqua)
+ editor.appearance = NSAppearance(named: .aqua)
+ editor.backgroundColor = .white
+ editor.textColor = .black
+ scroll.backgroundColor = .white
+
+ editor.isEditable = true
+ editor.isRichText = false
+ editor.allowsUndo = true
+ editor.isAutomaticQuoteSubstitutionEnabled = false
+ editor.isAutomaticDashSubstitutionEnabled = false
+ editor.font = NSFont.monospacedSystemFont(
+ ofSize: 13,
+ weight: .regular
+ )
+ editor.textContainerInset = NSSize(
+ width: 10,
+ height: 8
+ )
+ editor.delegate = context.coordinator
+ editor.string = text
+
+ context.coordinator.highlight(editor)
+
+ return scroll
+ }
+
+ func updateNSView(
+ _ scroll: NSScrollView,
+ context: Context
+ ) {
+ guard let editor = scroll.documentView as? NSTextView else {
+ return
+ }
+
+ if editor.string != text {
+ let selection = editor.selectedRange()
+ editor.string = text
+
+ let safeLocation = min(
+ selection.location,
+ (text as NSString).length
+ )
+
+ editor.setSelectedRange(
+ NSRange(location: safeLocation, length: 0)
+ )
+
+ context.coordinator.highlight(editor)
+ }
+ }
+
+ final class Coordinator: NSObject, NSTextViewDelegate {
+ var parent: SQLTextView
+
+ init(_ parent: SQLTextView) {
+ self.parent = parent
+ }
+
+ func textDidChange(_ notification: Notification) {
+ guard let editor = notification.object as? NSTextView else {
+ return
+ }
+
+ parent.text = editor.string
+ highlight(editor)
+ }
+
+ func highlight(_ editor: NSTextView) {
+ guard let storage = editor.textStorage else {
+ return
+ }
+
+ let text = editor.string as NSString
+ let range = NSRange(
+ location: 0,
+ length: text.length
+ )
+
+ storage.beginEditing()
+
+ storage.setAttributes([
+ .foregroundColor: NSColor.textColor,
+ .font: NSFont.monospacedSystemFont(
+ ofSize: 13,
+ weight: .regular
+ )
+ ], range: range)
+
+ let pattern = """
+ \\b(SELECT|FROM|WHERE|INSERT|UPDATE|DELETE|CREATE|TABLE|DATABASE|SHOW|USE|JOIN|LEFT|RIGHT|INNER|OUTER|ON|AS|AND|OR|ORDER|GROUP|BY|LIMIT|VALUES|SET|NULL|NOT|IN|LIKE|DISTINCT|ALTER|DROP|DESCRIBE|EXPLAIN)\\b
+ """
+
+ if let regex = try? NSRegularExpression(
+ pattern: pattern,
+ options: [.caseInsensitive]
+ ) {
+ for match in regex.matches(
+ in: editor.string,
+ range: range
+ ) {
+ storage.addAttribute(
+ .foregroundColor,
+ value: NSColor.systemBlue,
+ range: match.range
+ )
+ }
+ }
+
+ storage.endEditing()
+ }
+ }
+}
+
+// MARK: - Connection Editor
+
+struct ConnectionEditor: View {
+ @Binding var profile: ConnectionProfile
+ @Binding var password: String
+
+ let save: () -> Void
+
+ @Environment(\.dismiss) private var dismiss
+
+ var body: some View {
+ VStack(alignment: .leading, spacing: 14) {
+ Text("Connection Settings")
+ .font(.title2.bold())
+
+ Form {
+ TextField(
+ "Connection Name",
+ text: $profile.name
+ )
+
+ TextField(
+ "Hostname",
+ text: $profile.host
+ )
+
+ TextField(
+ "Port",
+ value: $profile.port,
+ format: .number
+ )
+
+ TextField(
+ "Username",
+ text: $profile.username
+ )
+
+ SecureField(
+ "Password (macOS Keychain)",
+ text: $password
+ )
+
+ TextField(
+ "Default Schema",
+ text: $profile.database
+ )
+ }
+
+ HStack {
+ Spacer()
+
+ Button("Cancel") {
+ dismiss()
+ }
+
+ Button("Save") {
+ save()
+ }
+ .buttonStyle(.borderedProminent)
+ }
+ }
+ .padding(24)
+ .frame(width: 520)
+ }
+}
diff --git a/WorkbenchReplacement/MainView.swift.backup-20261016-171647 b/WorkbenchReplacement/MainView.swift.backup-20261016-171647
new file mode 100644
index 0000000..9246cfc
--- /dev/null
+++ b/WorkbenchReplacement/MainView.swift.backup-20261016-171647
@@ -0,0 +1,883 @@
+
+import SwiftUI
+import AppKit
+
+// MARK: - Classic Workbench Colours
+
+private enum WB {
+ static let toolbar = Color(nsColor: NSColor(
+ calibratedWhite: 0.88, alpha: 1
+ ))
+ static let sidebar = Color(nsColor: NSColor(
+ calibratedWhite: 0.94, alpha: 1
+ ))
+ static let border = Color(nsColor: NSColor(
+ calibratedWhite: 0.76, alpha: 1
+ ))
+ static let header = Color(nsColor: NSColor(
+ calibratedWhite: 0.91, alpha: 1
+ ))
+}
+
+// MARK: - Main Window
+
+struct MainView: View {
+ @EnvironmentObject private var model: DatabaseModel
+
+ @State private var showingEditor = false
+ @State private var editing = ConnectionProfile()
+ @State private var editingPassword = ""
+
+ @State private var expandedSchemas: Set = []
+ @State private var outputExpanded = true
+
+ var body: some View {
+ VStack(spacing: 0) {
+ mainToolbar
+
+ Rectangle()
+ .fill(WB.border)
+ .frame(height: 1)
+
+ HSplitView {
+ navigator
+ .frame(
+ minWidth: 210,
+ idealWidth: 265,
+ maxWidth: 420
+ )
+
+ workspace
+ .frame(minWidth: 600)
+ }
+
+ statusBar
+ }
+ .frame(minWidth: 1100, minHeight: 720)
+ .preferredColorScheme(.light)
+ .background(Color(nsColor: .windowBackgroundColor))
+ .sheet(isPresented: $showingEditor) {
+ ConnectionEditor(
+ profile: $editing,
+ password: $editingPassword
+ ) {
+ model.save(editing, password: editingPassword)
+ showingEditor = false
+ }
+ }
+ }
+
+ // MARK: - Main Toolbar
+
+ private var mainToolbar: some View {
+ HStack(spacing: 16) {
+ Image(systemName: "externaldrive.connected.to.line.below")
+ .font(.system(size: 23))
+ .foregroundStyle(.blue)
+
+ Text("MySQL Workbench")
+ .font(.system(size: 15, weight: .semibold))
+ .foregroundStyle(.black)
+
+ Rectangle()
+ .fill(WB.border)
+ .frame(width: 1, height: 30)
+
+ toolbarButton(
+ "New Connection",
+ icon: "plus.rectangle.on.rectangle"
+ ) {
+ editing = ConnectionProfile(name: "New Connection")
+ editingPassword = ""
+ showingEditor = true
+ }
+
+ toolbarButton(
+ "Refresh",
+ icon: "arrow.clockwise"
+ ) {
+ model.loadDatabases()
+ }
+
+ toolbarButton(
+ "Execute",
+ icon: "bolt.fill"
+ ) {
+ model.runSQL()
+ }
+ .keyboardShortcut(.return, modifiers: [.command])
+
+ toolbarButton(
+ "Format SQL",
+ icon: "text.alignleft"
+ ) {
+ model.formatSQL()
+ }
+
+ Spacer()
+
+ VStack(alignment: .trailing, spacing: 2) {
+ Text(model.selected?.name ?? "No Connection")
+ .font(.system(size: 12, weight: .semibold))
+
+ Text(model.selectedDatabase.isEmpty
+ ? "No default schema"
+ : model.selectedDatabase)
+ .font(.system(size: 11))
+ .foregroundStyle(.secondary)
+ }
+ }
+ .padding(.horizontal, 10)
+ .frame(height: 46)
+ .background(WB.toolbar)
+ }
+
+ private func toolbarButton(
+ _ title: String,
+ icon: String,
+ action: @escaping () -> Void
+ ) -> some View {
+ Button(action: action) {
+ VStack(spacing: 3) {
+ Image(systemName: icon)
+ .font(.system(size: 19))
+ .frame(height: 23)
+
+ Text(title)
+ .font(.system(size: 10))
+ }
+ .foregroundStyle(.black)
+ .frame(minWidth: 65)
+ }
+ .buttonStyle(.plain)
+ .help(title)
+ }
+
+ // MARK: - Navigator
+
+ private var navigator: some View {
+ VStack(spacing: 0) {
+ sectionHeader("NAVIGATOR")
+
+ HStack {
+ Text("SCHEMAS")
+ .font(.system(size: 11, weight: .bold))
+
+ Spacer()
+
+ Button {
+ model.loadDatabases()
+ } label: {
+ Image(systemName: "arrow.clockwise")
+ }
+ .buttonStyle(.borderless)
+ }
+ .padding(.horizontal, 12)
+ .frame(height: 34)
+
+ Divider()
+
+ ScrollView {
+ LazyVStack(alignment: .leading, spacing: 2) {
+ ForEach(model.databases, id: \.self) { db in
+ schemaNode(db)
+ }
+ }
+ .padding(.vertical, 8)
+ }
+
+ Divider()
+
+ sectionHeader("MYSQL CONNECTIONS")
+
+ ScrollView {
+ VStack(alignment: .leading, spacing: 4) {
+ ForEach(model.profiles) { profile in
+ Button {
+ model.selectedID = profile.id
+ model.loadDatabases()
+ } label: {
+ Label(
+ profile.name,
+ systemImage: "server.rack"
+ )
+ .frame(
+ maxWidth: .infinity,
+ alignment: .leading
+ )
+ .padding(6)
+ .background(
+ model.selectedID == profile.id
+ ? Color.blue.opacity(0.14)
+ : Color.clear
+ )
+ }
+ .buttonStyle(.plain)
+ .contextMenu {
+ Button("Edit Connection") {
+ edit(profile)
+ }
+ }
+ }
+ }
+ .padding(8)
+ }
+ .frame(height: 125)
+ }
+ .background(WB.sidebar)
+ }
+
+ private func schemaNode(_ db: String) -> some View {
+ VStack(alignment: .leading, spacing: 2) {
+ HStack(spacing: 5) {
+ Button {
+ toggleSchema(db)
+ } label: {
+ Image(systemName:
+ expandedSchemas.contains(db)
+ ? "chevron.down"
+ : "chevron.right"
+ )
+ .font(.system(size: 10))
+ .frame(width: 15)
+ }
+ .buttonStyle(.plain)
+
+ Image(systemName: "cylinder.split.1x2")
+ .foregroundStyle(.blue)
+
+ Text(db)
+ .font(.system(size: 12))
+ .fontWeight(
+ model.selectedDatabase == db
+ ? .bold : .regular
+ )
+
+ Spacer()
+ }
+ .padding(.horizontal, 10)
+ .padding(.vertical, 5)
+ .background(
+ model.selectedDatabase == db
+ ? Color.blue.opacity(0.13)
+ : Color.clear
+ )
+ .contentShape(Rectangle())
+ .onTapGesture(count: 2) {
+ model.selectedDatabase = db
+ expandedSchemas.insert(db)
+ model.loadTables(database: db)
+ }
+ .contextMenu {
+ Button("Set as Default Schema") {
+ model.selectedDatabase = db
+ }
+
+ Button("Refresh Tables") {
+ model.loadTables(database: db)
+ }
+ }
+
+ if expandedSchemas.contains(db) {
+ schemaChildren(db)
+ .padding(.leading, 24)
+ }
+ }
+ }
+
+ private func toggleSchema(_ db: String) {
+ if expandedSchemas.contains(db) {
+ expandedSchemas.remove(db)
+ } else {
+ expandedSchemas.insert(db)
+
+ if model.schemaTables[db] == nil {
+ model.loadTables(database: db)
+ }
+ }
+ }
+
+
+ private func schemaChildren(_ db: String) -> some View {
+ VStack(alignment: .leading, spacing: 2) {
+
+ DisclosureGroup {
+ if let tables = model.schemaTables[db] {
+ ForEach(tables, id: \.self) { table in
+ HStack(spacing: 6) {
+ Image(systemName: "tablecells")
+ .foregroundStyle(.secondary)
+ .frame(width: 16)
+
+ Text(table)
+ .lineLimit(1)
+ .truncationMode(.middle)
+
+ Spacer(minLength: 0)
+ }
+ .frame(maxWidth: .infinity, alignment: .leading)
+ .padding(.leading, 4)
+ .padding(.vertical, 3)
+ .contentShape(Rectangle())
+ .onTapGesture(count: 2) {
+ model.selectTable(
+ database: db,
+ table: table
+ )
+ }
+ .contextMenu {
+ Button("Select First 100 Rows") {
+ model.selectTable(
+ database: db,
+ table: table
+ )
+ }
+ }
+ }
+ } else {
+ Button("Load Tables") {
+ model.loadTables(database: db)
+ }
+ .frame(maxWidth: .infinity, alignment: .leading)
+ .buttonStyle(.borderless)
+ }
+ } label: {
+ Label("Tables", systemImage: "folder")
+ .frame(maxWidth: .infinity, alignment: .leading)
+ }
+
+ Label("Views", systemImage: "folder")
+ .frame(maxWidth: .infinity, alignment: .leading)
+
+ Label("Stored Procedures", systemImage: "folder")
+ .frame(maxWidth: .infinity, alignment: .leading)
+
+ Label("Functions", systemImage: "folder")
+ .frame(maxWidth: .infinity, alignment: .leading)
+ }
+ .frame(maxWidth: .infinity, alignment: .leading)
+ .font(.system(size: 12))
+ .padding(.vertical, 3)
+ }
+
+ // MARK: - Workspace
+
+ private var workspace: some View {
+ VStack(spacing: 0) {
+ queryTabBar
+
+ Divider()
+
+ queryToolbar
+
+ Divider()
+
+ VSplitView {
+ sqlEditor
+ .frame(minHeight: 230)
+
+ bottomWorkspace
+ .frame(minHeight: 230)
+ }
+ }
+ }
+
+ private var queryTabBar: some View {
+ HStack(spacing: 0) {
+ HStack(spacing: 8) {
+ Image(systemName: "doc.text")
+ .foregroundStyle(.blue)
+
+ Text("Query 1")
+ .font(.system(size: 12, weight: .semibold))
+
+ Image(systemName: "xmark")
+ .font(.system(size: 9))
+ .foregroundStyle(.secondary)
+ }
+ .padding(.horizontal, 10)
+ .frame(height: 29)
+ .background(Color(nsColor: .textBackgroundColor))
+
+ Spacer()
+ }
+ .background(WB.header)
+ }
+
+ private var queryToolbar: some View {
+ HStack(spacing: 14) {
+ Button {
+ model.runSQL()
+ } label: {
+ Image(systemName: "bolt.fill")
+ .foregroundStyle(.orange)
+ }
+ .help("Execute SQL")
+
+ Button {
+ model.formatSQL()
+ } label: {
+ Image(systemName: "text.alignleft")
+ }
+ .help("Format SQL")
+
+ Divider()
+ .frame(height: 17)
+
+ Text("Default Schema:")
+ .font(.system(size: 11))
+
+ Text(model.selectedDatabase.isEmpty
+ ? "None"
+ : model.selectedDatabase)
+ .font(.system(size: 11, weight: .semibold))
+
+ Spacer()
+
+ Text("SQL Editor")
+ .font(.system(size: 11))
+ .foregroundStyle(.secondary)
+ }
+ .buttonStyle(.borderless)
+ .padding(.horizontal, 12)
+ .frame(height: 30)
+ .background(WB.header)
+ }
+
+ private var sqlEditor: some View {
+ HStack(spacing: 0) {
+ lineNumbers
+
+ Rectangle()
+ .fill(WB.border)
+ .frame(width: 1)
+
+ SQLTextView(text: $model.sql)
+ }
+ .background(Color(nsColor: .textBackgroundColor))
+ }
+
+ private var lineNumbers: some View {
+ ScrollView {
+ VStack(alignment: .trailing, spacing: 0) {
+ ForEach(
+ 1...max(1, model.sql.components(
+ separatedBy: "\n"
+ ).count),
+ id: \.self
+ ) { number in
+ Text("\(number)")
+ .frame(height: 19)
+ }
+ }
+ .padding(.horizontal, 9)
+ .padding(.top, 8)
+ }
+ .font(.system(size: 12, design: .monospaced))
+ .foregroundStyle(.secondary)
+ .frame(width: 48)
+ .background(WB.header)
+ .allowsHitTesting(false)
+ }
+
+ // MARK: - Results and Action Output
+
+ private var bottomWorkspace: some View {
+ VSplitView {
+ resultsPanel
+ .frame(minHeight: 230)
+
+ if outputExpanded {
+ actionOutput
+ .frame(minHeight: 90)
+ }
+ }
+ }
+
+ private var resultsPanel: some View {
+ VStack(spacing: 0) {
+ HStack {
+ Image(systemName: "tablecells")
+ Text("Result Grid")
+ .fontWeight(.semibold)
+
+ Spacer()
+
+ Text(model.result.message)
+ .foregroundStyle(.secondary)
+ }
+ .font(.system(size: 12))
+ .padding(.horizontal, 12)
+ .frame(height: 28)
+ .background(WB.header)
+
+ Divider()
+
+ if model.result.columns.isEmpty {
+ VStack {
+ Spacer()
+ Text("No query results")
+ .foregroundStyle(.secondary)
+ Spacer()
+ }
+ } else {
+ resultGrid
+ }
+ }
+ }
+
+ private var resultGrid: some View {
+ ScrollView([.horizontal, .vertical]) {
+ Grid(
+ alignment: .leading,
+ horizontalSpacing: 0,
+ verticalSpacing: 0
+ ) {
+ GridRow {
+ ForEach(
+ model.result.columns.indices,
+ id: \.self
+ ) { column in
+ Text(model.result.columns[column])
+ .fontWeight(.semibold)
+ .frame(
+ width: 170,
+ alignment: .leading
+ )
+ .padding(7)
+ .background(WB.header)
+ }
+ }
+
+ ForEach(
+ model.result.rows.indices,
+ id: \.self
+ ) { row in
+ GridRow {
+ ForEach(
+ model.result.columns.indices,
+ id: \.self
+ ) { column in
+ Text(
+ column < model.result.rows[row].count
+ ? model.result.rows[row][column]
+ : ""
+ )
+ .lineLimit(1)
+ .frame(
+ width: 170,
+ alignment: .leading
+ )
+ .padding(7)
+ .background(
+ row.isMultiple(of: 2)
+ ? Color(nsColor: .textBackgroundColor)
+ : WB.sidebar
+ )
+ }
+ }
+ }
+ }
+ .font(.system(size: 12, design: .monospaced))
+ }
+ }
+
+ private var actionOutput: some View {
+ VStack(spacing: 0) {
+ HStack {
+ Image(systemName: "list.bullet.rectangle")
+ Text("Action Output")
+ .fontWeight(.semibold)
+
+ Spacer()
+
+ Button {
+ outputExpanded = false
+ } label: {
+ Image(systemName: "chevron.down")
+ }
+ .buttonStyle(.borderless)
+ }
+ .font(.system(size: 12))
+ .padding(.horizontal, 12)
+ .frame(height: 26)
+ .background(WB.header)
+
+ Divider()
+
+ HStack(alignment: .top, spacing: 12) {
+ Image(systemName:
+ model.status == "Error"
+ ? "xmark.circle.fill"
+ : "checkmark.circle.fill"
+ )
+ .foregroundStyle(
+ model.status == "Error"
+ ? .red : .green
+ )
+
+ VStack(alignment: .leading, spacing: 5) {
+ Text(model.status)
+ .fontWeight(.semibold)
+
+ Text(model.result.message)
+ .foregroundStyle(.secondary)
+ }
+
+ Spacer()
+ }
+ .font(.system(size: 12))
+ .padding(12)
+
+ Spacer()
+ }
+ }
+
+ // MARK: - Status Bar
+
+ private var statusBar: some View {
+ HStack(spacing: 10) {
+ Circle()
+ .fill(
+ model.status == "Error"
+ ? Color.red : Color.green
+ )
+ .frame(width: 8, height: 8)
+
+ Text(model.status)
+ .lineLimit(1)
+
+ Spacer()
+
+ Text(model.selected?.name ?? "Disconnected")
+
+ Divider()
+ .frame(height: 13)
+
+ Text(model.selectedDatabase.isEmpty
+ ? "No Schema"
+ : model.selectedDatabase)
+
+ Divider()
+ .frame(height: 13)
+
+ Text("UTF-8")
+ }
+ .font(.system(size: 11))
+ .padding(.horizontal, 12)
+ .frame(height: 22)
+ .background(WB.header)
+ }
+
+ private func sectionHeader(_ title: String) -> some View {
+ HStack {
+ Text(title)
+ .font(.system(size: 11, weight: .bold))
+ .foregroundStyle(.secondary)
+
+ Spacer()
+ }
+ .padding(.horizontal, 12)
+ .frame(height: 30)
+ .background(WB.header)
+ }
+
+ private func edit(_ profile: ConnectionProfile) {
+ editing = profile
+ editingPassword = PasswordStore.read(profile.id)
+ showingEditor = true
+ }
+}
+
+// MARK: - Native SQL Text Editor
+
+struct SQLTextView: NSViewRepresentable {
+ @Binding var text: String
+
+ func makeCoordinator() -> Coordinator {
+ Coordinator(self)
+ }
+
+ func makeNSView(context: Context) -> NSScrollView {
+ let scroll = NSTextView.scrollableTextView()
+ let editor = scroll.documentView as! NSTextView
+
+ scroll.appearance = NSAppearance(named: .aqua)
+ editor.appearance = NSAppearance(named: .aqua)
+ editor.backgroundColor = .white
+ editor.textColor = .black
+ scroll.backgroundColor = .white
+
+ editor.isEditable = true
+ editor.isRichText = false
+ editor.allowsUndo = true
+ editor.isAutomaticQuoteSubstitutionEnabled = false
+ editor.isAutomaticDashSubstitutionEnabled = false
+ editor.font = NSFont.monospacedSystemFont(
+ ofSize: 13,
+ weight: .regular
+ )
+ editor.textContainerInset = NSSize(
+ width: 10,
+ height: 8
+ )
+ editor.delegate = context.coordinator
+ editor.string = text
+
+ context.coordinator.highlight(editor)
+
+ return scroll
+ }
+
+ func updateNSView(
+ _ scroll: NSScrollView,
+ context: Context
+ ) {
+ guard let editor = scroll.documentView as? NSTextView else {
+ return
+ }
+
+ if editor.string != text {
+ let selection = editor.selectedRange()
+ editor.string = text
+
+ let safeLocation = min(
+ selection.location,
+ (text as NSString).length
+ )
+
+ editor.setSelectedRange(
+ NSRange(location: safeLocation, length: 0)
+ )
+
+ context.coordinator.highlight(editor)
+ }
+ }
+
+ final class Coordinator: NSObject, NSTextViewDelegate {
+ var parent: SQLTextView
+
+ init(_ parent: SQLTextView) {
+ self.parent = parent
+ }
+
+ func textDidChange(_ notification: Notification) {
+ guard let editor = notification.object as? NSTextView else {
+ return
+ }
+
+ parent.text = editor.string
+ highlight(editor)
+ }
+
+ func highlight(_ editor: NSTextView) {
+ guard let storage = editor.textStorage else {
+ return
+ }
+
+ let text = editor.string as NSString
+ let range = NSRange(
+ location: 0,
+ length: text.length
+ )
+
+ storage.beginEditing()
+
+ storage.setAttributes([
+ .foregroundColor: NSColor.textColor,
+ .font: NSFont.monospacedSystemFont(
+ ofSize: 13,
+ weight: .regular
+ )
+ ], range: range)
+
+ let pattern = """
+ \\b(SELECT|FROM|WHERE|INSERT|UPDATE|DELETE|CREATE|TABLE|DATABASE|SHOW|USE|JOIN|LEFT|RIGHT|INNER|OUTER|ON|AS|AND|OR|ORDER|GROUP|BY|LIMIT|VALUES|SET|NULL|NOT|IN|LIKE|DISTINCT|ALTER|DROP|DESCRIBE|EXPLAIN)\\b
+ """
+
+ if let regex = try? NSRegularExpression(
+ pattern: pattern,
+ options: [.caseInsensitive]
+ ) {
+ for match in regex.matches(
+ in: editor.string,
+ range: range
+ ) {
+ storage.addAttribute(
+ .foregroundColor,
+ value: NSColor.systemBlue,
+ range: match.range
+ )
+ }
+ }
+
+ storage.endEditing()
+ }
+ }
+}
+
+// MARK: - Connection Editor
+
+struct ConnectionEditor: View {
+ @Binding var profile: ConnectionProfile
+ @Binding var password: String
+
+ let save: () -> Void
+
+ @Environment(\.dismiss) private var dismiss
+
+ var body: some View {
+ VStack(alignment: .leading, spacing: 14) {
+ Text("Connection Settings")
+ .font(.title2.bold())
+
+ Form {
+ TextField(
+ "Connection Name",
+ text: $profile.name
+ )
+
+ TextField(
+ "Hostname",
+ text: $profile.host
+ )
+
+ TextField(
+ "Port",
+ value: $profile.port,
+ format: .number
+ )
+
+ TextField(
+ "Username",
+ text: $profile.username
+ )
+
+ SecureField(
+ "Password (macOS Keychain)",
+ text: $password
+ )
+
+ TextField(
+ "Default Schema",
+ text: $profile.database
+ )
+ }
+
+ HStack {
+ Spacer()
+
+ Button("Cancel") {
+ dismiss()
+ }
+
+ Button("Save") {
+ save()
+ }
+ .buttonStyle(.borderedProminent)
+ }
+ }
+ .padding(24)
+ .frame(width: 520)
+ }
+}
diff --git a/WorkbenchReplacement/MainView.swift.backup-20261016-171907 b/WorkbenchReplacement/MainView.swift.backup-20261016-171907
new file mode 100644
index 0000000..73e9e01
--- /dev/null
+++ b/WorkbenchReplacement/MainView.swift.backup-20261016-171907
@@ -0,0 +1,960 @@
+
+import SwiftUI
+import AppKit
+
+// MARK: - Classic Workbench Colours
+
+private enum WB {
+ static let toolbar = Color(nsColor: NSColor(
+ calibratedWhite: 0.88, alpha: 1
+ ))
+ static let sidebar = Color(nsColor: NSColor(
+ calibratedWhite: 0.94, alpha: 1
+ ))
+ static let border = Color(nsColor: NSColor(
+ calibratedWhite: 0.76, alpha: 1
+ ))
+ static let header = Color(nsColor: NSColor(
+ calibratedWhite: 0.91, alpha: 1
+ ))
+}
+
+// MARK: - Main Window
+
+struct MainView: View {
+ @EnvironmentObject private var model: DatabaseModel
+
+ @State private var showingEditor = false
+ @State private var editing = ConnectionProfile()
+ @State private var editingPassword = ""
+
+ @State private var expandedSchemas: Set = []
+ @State private var expandedTableFolders: Set = []
+ @State private var selectedTableName: String? = nil
+ @State private var outputExpanded = true
+
+ var body: some View {
+ VStack(spacing: 0) {
+ mainToolbar
+
+ Rectangle()
+ .fill(WB.border)
+ .frame(height: 1)
+
+ HSplitView {
+ navigator
+ .frame(
+ minWidth: 210,
+ idealWidth: 265,
+ maxWidth: 420
+ )
+
+ workspace
+ .frame(minWidth: 600)
+ }
+
+ statusBar
+ }
+ .frame(minWidth: 1100, minHeight: 720)
+ .preferredColorScheme(.light)
+ .background(Color(nsColor: .windowBackgroundColor))
+ .sheet(isPresented: $showingEditor) {
+ ConnectionEditor(
+ profile: $editing,
+ password: $editingPassword
+ ) {
+ model.save(editing, password: editingPassword)
+ showingEditor = false
+ }
+ }
+ }
+
+ // MARK: - Main Toolbar
+
+ private var mainToolbar: some View {
+ HStack(spacing: 16) {
+ Image(systemName: "externaldrive.connected.to.line.below")
+ .font(.system(size: 23))
+ .foregroundStyle(.blue)
+
+ Text("MySQL Workbench")
+ .font(.system(size: 15, weight: .semibold))
+ .foregroundStyle(.black)
+
+ Rectangle()
+ .fill(WB.border)
+ .frame(width: 1, height: 30)
+
+ toolbarButton(
+ "New Connection",
+ icon: "plus.rectangle.on.rectangle"
+ ) {
+ editing = ConnectionProfile(name: "New Connection")
+ editingPassword = ""
+ showingEditor = true
+ }
+
+ toolbarButton(
+ "Refresh",
+ icon: "arrow.clockwise"
+ ) {
+ model.loadDatabases()
+ }
+
+ toolbarButton(
+ "Execute",
+ icon: "bolt.fill"
+ ) {
+ model.runSQL()
+ }
+ .keyboardShortcut(.return, modifiers: [.command])
+
+ toolbarButton(
+ "Format SQL",
+ icon: "text.alignleft"
+ ) {
+ model.formatSQL()
+ }
+
+ Spacer()
+
+ VStack(alignment: .trailing, spacing: 2) {
+ Text(model.selected?.name ?? "No Connection")
+ .font(.system(size: 12, weight: .semibold))
+
+ Text(model.selectedDatabase.isEmpty
+ ? "No default schema"
+ : model.selectedDatabase)
+ .font(.system(size: 11))
+ .foregroundStyle(.secondary)
+ }
+ }
+ .padding(.horizontal, 10)
+ .frame(height: 46)
+ .background(WB.toolbar)
+ }
+
+ private func toolbarButton(
+ _ title: String,
+ icon: String,
+ action: @escaping () -> Void
+ ) -> some View {
+ Button(action: action) {
+ VStack(spacing: 3) {
+ Image(systemName: icon)
+ .font(.system(size: 19))
+ .frame(height: 23)
+
+ Text(title)
+ .font(.system(size: 10))
+ }
+ .foregroundStyle(.black)
+ .frame(minWidth: 65)
+ }
+ .buttonStyle(.plain)
+ .help(title)
+ }
+
+ // MARK: - Navigator
+
+ private var navigator: some View {
+ VStack(spacing: 0) {
+ sectionHeader("NAVIGATOR")
+
+ HStack {
+ Text("SCHEMAS")
+ .font(.system(size: 11, weight: .bold))
+
+ Spacer()
+
+ Button {
+ model.loadDatabases()
+ } label: {
+ Image(systemName: "arrow.clockwise")
+ }
+ .buttonStyle(.borderless)
+ }
+ .padding(.horizontal, 12)
+ .frame(height: 34)
+
+ Divider()
+
+ ScrollView {
+ LazyVStack(alignment: .leading, spacing: 2) {
+ ForEach(model.databases, id: \.self) { db in
+ schemaNode(db)
+ }
+ }
+ .padding(.vertical, 8)
+ }
+
+ Divider()
+
+ sectionHeader("MYSQL CONNECTIONS")
+
+ ScrollView {
+ VStack(alignment: .leading, spacing: 4) {
+ ForEach(model.profiles) { profile in
+ Button {
+ model.selectedID = profile.id
+ model.loadDatabases()
+ } label: {
+ Label(
+ profile.name,
+ systemImage: "server.rack"
+ )
+ .frame(
+ maxWidth: .infinity,
+ alignment: .leading
+ )
+ .padding(6)
+ .background(
+ model.selectedID == profile.id
+ ? Color.blue.opacity(0.14)
+ : Color.clear
+ )
+ }
+ .buttonStyle(.plain)
+ .contextMenu {
+ Button("Edit Connection") {
+ edit(profile)
+ }
+ }
+ }
+ }
+ .padding(8)
+ }
+ .frame(height: 125)
+ }
+ .background(WB.sidebar)
+ }
+
+
+ // MARK: - Compact Schema Tree
+
+ private func schemaNode(_ db: String) -> some View {
+ VStack(alignment: .leading, spacing: 0) {
+
+ HStack(spacing: 5) {
+ Image(systemName:
+ expandedSchemas.contains(db)
+ ? "chevron.down"
+ : "chevron.right"
+ )
+ .font(.system(size: 10))
+ .foregroundStyle(.secondary)
+ .frame(width: 13)
+
+ Image(systemName: "cylinder.split.1x2")
+ .foregroundStyle(.blue)
+ .frame(width: 17)
+
+ Text(db)
+ .font(.system(size: 12, weight:
+ model.selectedDatabase == db
+ ? .semibold : .regular))
+ .lineLimit(1)
+
+ Spacer(minLength: 0)
+ }
+ .frame(height: 25)
+ .padding(.leading, 10)
+ .contentShape(Rectangle())
+ .onTapGesture {
+ toggleSchema(db)
+ }
+ .onTapGesture(count: 2) {
+ model.selectedDatabase = db
+ expandedSchemas.insert(db)
+ expandedTableFolders.insert(db)
+
+ if model.schemaTables[db] == nil {
+ model.loadTables(database: db)
+ }
+ }
+ .contextMenu {
+ Button("Set as Default Schema") {
+ model.selectedDatabase = db
+ }
+
+ Button("Refresh Tables") {
+ model.loadTables(database: db)
+ }
+ }
+
+ if expandedSchemas.contains(db) {
+ schemaChildren(db)
+ }
+ }
+ .frame(maxWidth: .infinity, alignment: .leading)
+ }
+
+ private func toggleSchema(_ db: String) {
+ if expandedSchemas.contains(db) {
+ expandedSchemas.remove(db)
+ } else {
+ expandedSchemas.insert(db)
+ expandedTableFolders.insert(db)
+
+ if model.schemaTables[db] == nil {
+ model.loadTables(database: db)
+ }
+ }
+ }
+
+ private func schemaChildren(_ db: String) -> some View {
+ VStack(alignment: .leading, spacing: 0) {
+
+ // Tables folder
+ HStack(spacing: 5) {
+ Image(systemName:
+ expandedTableFolders.contains(db)
+ ? "chevron.down"
+ : "chevron.right"
+ )
+ .font(.system(size: 10))
+ .foregroundStyle(.secondary)
+ .frame(width: 13)
+
+ Image(systemName: "folder")
+ .foregroundStyle(.secondary)
+ .frame(width: 17)
+
+ Text("Tables")
+ .font(.system(size: 12))
+
+ Spacer(minLength: 0)
+ }
+ .frame(height: 24)
+ .padding(.leading, 28)
+ .contentShape(Rectangle())
+ .onTapGesture {
+ if expandedTableFolders.contains(db) {
+ expandedTableFolders.remove(db)
+ } else {
+ expandedTableFolders.insert(db)
+
+ if model.schemaTables[db] == nil {
+ model.loadTables(database: db)
+ }
+ }
+ }
+
+ // Tables
+ if expandedTableFolders.contains(db) {
+ if let tables = model.schemaTables[db] {
+ ForEach(tables, id: \.self) { table in
+ tableNode(database: db, table: table)
+ }
+ } else {
+ Button("Load Tables") {
+ model.loadTables(database: db)
+ }
+ .font(.system(size: 11))
+ .buttonStyle(.borderless)
+ .padding(.leading, 66)
+ .frame(height: 24)
+ }
+ }
+
+ folderNode("Views", indent: 28)
+ folderNode("Stored Procedures", indent: 28)
+ folderNode("Functions", indent: 28)
+ }
+ .frame(maxWidth: .infinity, alignment: .leading)
+ }
+
+ private func tableNode(database: String, table: String) -> some View {
+ let identifier = database + "." + table
+ let isSelected = selectedTableName == identifier
+
+ return HStack(spacing: 5) {
+ Image(systemName: "tablecells")
+ .font(.system(size: 11))
+ .foregroundStyle(
+ isSelected ? Color.blue : Color.gray
+ )
+ .frame(width: 17)
+
+ Text(table)
+ .font(.system(size: 12))
+ .lineLimit(1)
+ .truncationMode(.middle)
+
+ Spacer(minLength: 0)
+ }
+ .frame(height: 23)
+ .padding(.leading, 49)
+ .frame(maxWidth: .infinity, alignment: .leading)
+ .background(
+ isSelected
+ ? Color.blue.opacity(0.14)
+ : Color.clear
+ )
+ .contentShape(Rectangle())
+ .onTapGesture {
+ selectedTableName = identifier
+ }
+ .onTapGesture(count: 2) {
+ selectedTableName = identifier
+ model.selectTable(
+ database: database,
+ table: table
+ )
+ }
+ .contextMenu {
+ Button("Select First 100 Rows") {
+ selectedTableName = identifier
+ model.selectTable(
+ database: database,
+ table: table
+ )
+ }
+ }
+ }
+
+ private func folderNode(
+ _ title: String,
+ indent: CGFloat
+ ) -> some View {
+ HStack(spacing: 5) {
+ Image(systemName: "chevron.right")
+ .font(.system(size: 10))
+ .foregroundStyle(.secondary)
+ .frame(width: 13)
+
+ Image(systemName: "folder")
+ .foregroundStyle(.secondary)
+ .frame(width: 17)
+
+ Text(title)
+ .font(.system(size: 12))
+
+ Spacer(minLength: 0)
+ }
+ .frame(height: 24)
+ .padding(.leading, indent)
+ .frame(maxWidth: .infinity, alignment: .leading)
+ }
+
+ // MARK: - Workspace
+
+ private var workspace: some View {
+ VStack(spacing: 0) {
+ queryTabBar
+
+ Divider()
+
+ queryToolbar
+
+ Divider()
+
+ VSplitView {
+ sqlEditor
+ .frame(minHeight: 230)
+
+ bottomWorkspace
+ .frame(minHeight: 230)
+ }
+ }
+ }
+
+ private var queryTabBar: some View {
+ HStack(spacing: 0) {
+ HStack(spacing: 8) {
+ Image(systemName: "doc.text")
+ .foregroundStyle(.blue)
+
+ Text("Query 1")
+ .font(.system(size: 12, weight: .semibold))
+
+ Image(systemName: "xmark")
+ .font(.system(size: 9))
+ .foregroundStyle(.secondary)
+ }
+ .padding(.horizontal, 10)
+ .frame(height: 29)
+ .background(Color(nsColor: .textBackgroundColor))
+
+ Spacer()
+ }
+ .background(WB.header)
+ }
+
+ private var queryToolbar: some View {
+ HStack(spacing: 14) {
+ Button {
+ model.runSQL()
+ } label: {
+ Image(systemName: "bolt.fill")
+ .foregroundStyle(.orange)
+ }
+ .help("Execute SQL")
+
+ Button {
+ model.formatSQL()
+ } label: {
+ Image(systemName: "text.alignleft")
+ }
+ .help("Format SQL")
+
+ Divider()
+ .frame(height: 17)
+
+ Text("Default Schema:")
+ .font(.system(size: 11))
+
+ Text(model.selectedDatabase.isEmpty
+ ? "None"
+ : model.selectedDatabase)
+ .font(.system(size: 11, weight: .semibold))
+
+ Spacer()
+
+ Text("SQL Editor")
+ .font(.system(size: 11))
+ .foregroundStyle(.secondary)
+ }
+ .buttonStyle(.borderless)
+ .padding(.horizontal, 12)
+ .frame(height: 30)
+ .background(WB.header)
+ }
+
+ private var sqlEditor: some View {
+ HStack(spacing: 0) {
+ lineNumbers
+
+ Rectangle()
+ .fill(WB.border)
+ .frame(width: 1)
+
+ SQLTextView(text: $model.sql)
+ }
+ .background(Color(nsColor: .textBackgroundColor))
+ }
+
+ private var lineNumbers: some View {
+ ScrollView {
+ VStack(alignment: .trailing, spacing: 0) {
+ ForEach(
+ 1...max(1, model.sql.components(
+ separatedBy: "\n"
+ ).count),
+ id: \.self
+ ) { number in
+ Text("\(number)")
+ .frame(height: 19)
+ }
+ }
+ .padding(.horizontal, 9)
+ .padding(.top, 8)
+ }
+ .font(.system(size: 12, design: .monospaced))
+ .foregroundStyle(.secondary)
+ .frame(width: 48)
+ .background(WB.header)
+ .allowsHitTesting(false)
+ }
+
+ // MARK: - Results and Action Output
+
+ private var bottomWorkspace: some View {
+ VSplitView {
+ resultsPanel
+ .frame(minHeight: 230)
+
+ if outputExpanded {
+ actionOutput
+ .frame(minHeight: 90)
+ }
+ }
+ }
+
+ private var resultsPanel: some View {
+ VStack(spacing: 0) {
+ HStack {
+ Image(systemName: "tablecells")
+ Text("Result Grid")
+ .fontWeight(.semibold)
+
+ Spacer()
+
+ Text(model.result.message)
+ .foregroundStyle(.secondary)
+ }
+ .font(.system(size: 12))
+ .padding(.horizontal, 12)
+ .frame(height: 28)
+ .background(WB.header)
+
+ Divider()
+
+ if model.result.columns.isEmpty {
+ VStack {
+ Spacer()
+ Text("No query results")
+ .foregroundStyle(.secondary)
+ Spacer()
+ }
+ } else {
+ resultGrid
+ }
+ }
+ }
+
+ private var resultGrid: some View {
+ ScrollView([.horizontal, .vertical]) {
+ Grid(
+ alignment: .leading,
+ horizontalSpacing: 0,
+ verticalSpacing: 0
+ ) {
+ GridRow {
+ ForEach(
+ model.result.columns.indices,
+ id: \.self
+ ) { column in
+ Text(model.result.columns[column])
+ .fontWeight(.semibold)
+ .frame(
+ width: 170,
+ alignment: .leading
+ )
+ .padding(7)
+ .background(WB.header)
+ }
+ }
+
+ ForEach(
+ model.result.rows.indices,
+ id: \.self
+ ) { row in
+ GridRow {
+ ForEach(
+ model.result.columns.indices,
+ id: \.self
+ ) { column in
+ Text(
+ column < model.result.rows[row].count
+ ? model.result.rows[row][column]
+ : ""
+ )
+ .lineLimit(1)
+ .frame(
+ width: 170,
+ alignment: .leading
+ )
+ .padding(7)
+ .background(
+ row.isMultiple(of: 2)
+ ? Color(nsColor: .textBackgroundColor)
+ : WB.sidebar
+ )
+ }
+ }
+ }
+ }
+ .font(.system(size: 12, design: .monospaced))
+ }
+ }
+
+ private var actionOutput: some View {
+ VStack(spacing: 0) {
+ HStack {
+ Image(systemName: "list.bullet.rectangle")
+ Text("Action Output")
+ .fontWeight(.semibold)
+
+ Spacer()
+
+ Button {
+ outputExpanded = false
+ } label: {
+ Image(systemName: "chevron.down")
+ }
+ .buttonStyle(.borderless)
+ }
+ .font(.system(size: 12))
+ .padding(.horizontal, 12)
+ .frame(height: 26)
+ .background(WB.header)
+
+ Divider()
+
+ HStack(alignment: .top, spacing: 12) {
+ Image(systemName:
+ model.status == "Error"
+ ? "xmark.circle.fill"
+ : "checkmark.circle.fill"
+ )
+ .foregroundStyle(
+ model.status == "Error"
+ ? .red : .green
+ )
+
+ VStack(alignment: .leading, spacing: 5) {
+ Text(model.status)
+ .fontWeight(.semibold)
+
+ Text(model.result.message)
+ .foregroundStyle(.secondary)
+ }
+
+ Spacer()
+ }
+ .font(.system(size: 12))
+ .padding(12)
+
+ Spacer()
+ }
+ }
+
+ // MARK: - Status Bar
+
+ private var statusBar: some View {
+ HStack(spacing: 10) {
+ Circle()
+ .fill(
+ model.status == "Error"
+ ? Color.red : Color.green
+ )
+ .frame(width: 8, height: 8)
+
+ Text(model.status)
+ .lineLimit(1)
+
+ Spacer()
+
+ Text(model.selected?.name ?? "Disconnected")
+
+ Divider()
+ .frame(height: 13)
+
+ Text(model.selectedDatabase.isEmpty
+ ? "No Schema"
+ : model.selectedDatabase)
+
+ Divider()
+ .frame(height: 13)
+
+ Text("UTF-8")
+ }
+ .font(.system(size: 11))
+ .padding(.horizontal, 12)
+ .frame(height: 22)
+ .background(WB.header)
+ }
+
+ private func sectionHeader(_ title: String) -> some View {
+ HStack {
+ Text(title)
+ .font(.system(size: 11, weight: .bold))
+ .foregroundStyle(.secondary)
+
+ Spacer()
+ }
+ .padding(.horizontal, 12)
+ .frame(height: 30)
+ .background(WB.header)
+ }
+
+ private func edit(_ profile: ConnectionProfile) {
+ editing = profile
+ editingPassword = PasswordStore.read(profile.id)
+ showingEditor = true
+ }
+}
+
+// MARK: - Native SQL Text Editor
+
+struct SQLTextView: NSViewRepresentable {
+ @Binding var text: String
+
+ func makeCoordinator() -> Coordinator {
+ Coordinator(self)
+ }
+
+ func makeNSView(context: Context) -> NSScrollView {
+ let scroll = NSTextView.scrollableTextView()
+ let editor = scroll.documentView as! NSTextView
+
+ scroll.appearance = NSAppearance(named: .aqua)
+ editor.appearance = NSAppearance(named: .aqua)
+ editor.backgroundColor = .white
+ editor.textColor = .black
+ scroll.backgroundColor = .white
+
+ editor.isEditable = true
+ editor.isRichText = false
+ editor.allowsUndo = true
+ editor.isAutomaticQuoteSubstitutionEnabled = false
+ editor.isAutomaticDashSubstitutionEnabled = false
+ editor.font = NSFont.monospacedSystemFont(
+ ofSize: 13,
+ weight: .regular
+ )
+ editor.textContainerInset = NSSize(
+ width: 10,
+ height: 8
+ )
+ editor.delegate = context.coordinator
+ editor.string = text
+
+ context.coordinator.highlight(editor)
+
+ return scroll
+ }
+
+ func updateNSView(
+ _ scroll: NSScrollView,
+ context: Context
+ ) {
+ guard let editor = scroll.documentView as? NSTextView else {
+ return
+ }
+
+ if editor.string != text {
+ let selection = editor.selectedRange()
+ editor.string = text
+
+ let safeLocation = min(
+ selection.location,
+ (text as NSString).length
+ )
+
+ editor.setSelectedRange(
+ NSRange(location: safeLocation, length: 0)
+ )
+
+ context.coordinator.highlight(editor)
+ }
+ }
+
+ final class Coordinator: NSObject, NSTextViewDelegate {
+ var parent: SQLTextView
+
+ init(_ parent: SQLTextView) {
+ self.parent = parent
+ }
+
+ func textDidChange(_ notification: Notification) {
+ guard let editor = notification.object as? NSTextView else {
+ return
+ }
+
+ parent.text = editor.string
+ highlight(editor)
+ }
+
+ func highlight(_ editor: NSTextView) {
+ guard let storage = editor.textStorage else {
+ return
+ }
+
+ let text = editor.string as NSString
+ let range = NSRange(
+ location: 0,
+ length: text.length
+ )
+
+ storage.beginEditing()
+
+ storage.setAttributes([
+ .foregroundColor: NSColor.textColor,
+ .font: NSFont.monospacedSystemFont(
+ ofSize: 13,
+ weight: .regular
+ )
+ ], range: range)
+
+ let pattern = """
+ \\b(SELECT|FROM|WHERE|INSERT|UPDATE|DELETE|CREATE|TABLE|DATABASE|SHOW|USE|JOIN|LEFT|RIGHT|INNER|OUTER|ON|AS|AND|OR|ORDER|GROUP|BY|LIMIT|VALUES|SET|NULL|NOT|IN|LIKE|DISTINCT|ALTER|DROP|DESCRIBE|EXPLAIN)\\b
+ """
+
+ if let regex = try? NSRegularExpression(
+ pattern: pattern,
+ options: [.caseInsensitive]
+ ) {
+ for match in regex.matches(
+ in: editor.string,
+ range: range
+ ) {
+ storage.addAttribute(
+ .foregroundColor,
+ value: NSColor.systemBlue,
+ range: match.range
+ )
+ }
+ }
+
+ storage.endEditing()
+ }
+ }
+}
+
+// MARK: - Connection Editor
+
+struct ConnectionEditor: View {
+ @Binding var profile: ConnectionProfile
+ @Binding var password: String
+
+ let save: () -> Void
+
+ @Environment(\.dismiss) private var dismiss
+
+ var body: some View {
+ VStack(alignment: .leading, spacing: 14) {
+ Text("Connection Settings")
+ .font(.title2.bold())
+
+ Form {
+ TextField(
+ "Connection Name",
+ text: $profile.name
+ )
+
+ TextField(
+ "Hostname",
+ text: $profile.host
+ )
+
+ TextField(
+ "Port",
+ value: $profile.port,
+ format: .number
+ )
+
+ TextField(
+ "Username",
+ text: $profile.username
+ )
+
+ SecureField(
+ "Password (macOS Keychain)",
+ text: $password
+ )
+
+ TextField(
+ "Default Schema",
+ text: $profile.database
+ )
+ }
+
+ HStack {
+ Spacer()
+
+ Button("Cancel") {
+ dismiss()
+ }
+
+ Button("Save") {
+ save()
+ }
+ .buttonStyle(.borderedProminent)
+ }
+ }
+ .padding(24)
+ .frame(width: 520)
+ }
+}
diff --git a/WorkbenchReplacement/MainView.swift.backup-20261016-172024 b/WorkbenchReplacement/MainView.swift.backup-20261016-172024
new file mode 100644
index 0000000..0515f28
--- /dev/null
+++ b/WorkbenchReplacement/MainView.swift.backup-20261016-172024
@@ -0,0 +1,1083 @@
+
+import SwiftUI
+import AppKit
+
+// MARK: - Classic Workbench Colours
+
+private enum WB {
+ static let toolbar = Color(nsColor: NSColor(
+ calibratedWhite: 0.88, alpha: 1
+ ))
+ static let sidebar = Color(nsColor: NSColor(
+ calibratedWhite: 0.94, alpha: 1
+ ))
+ static let border = Color(nsColor: NSColor(
+ calibratedWhite: 0.76, alpha: 1
+ ))
+ static let header = Color(nsColor: NSColor(
+ calibratedWhite: 0.91, alpha: 1
+ ))
+}
+
+// MARK: - Main Window
+
+struct MainView: View {
+ @EnvironmentObject private var model: DatabaseModel
+
+ @State private var showingEditor = false
+ @State private var editing = ConnectionProfile()
+ @State private var editingPassword = ""
+
+ @State private var expandedSchemas: Set = []
+ @State private var expandedTableFolders: Set = []
+ @State private var selectedTableName: String? = nil
+ @State private var outputExpanded = true
+
+ var body: some View {
+ VStack(spacing: 0) {
+ mainToolbar
+
+ Rectangle()
+ .fill(WB.border)
+ .frame(height: 1)
+
+ HSplitView {
+ navigator
+ .frame(
+ minWidth: 210,
+ idealWidth: 265,
+ maxWidth: 420
+ )
+
+ workspace
+ .frame(minWidth: 600)
+ }
+
+ statusBar
+ }
+ .frame(minWidth: 1100, minHeight: 720)
+ .preferredColorScheme(.light)
+ .background(Color(nsColor: .windowBackgroundColor))
+ .sheet(isPresented: $showingEditor) {
+ ConnectionEditor(
+ profile: $editing,
+ password: $editingPassword
+ ) {
+ model.save(editing, password: editingPassword)
+ showingEditor = false
+ }
+ }
+ }
+
+ // MARK: - Main Toolbar
+
+ private var mainToolbar: some View {
+ HStack(spacing: 16) {
+ Image(systemName: "externaldrive.connected.to.line.below")
+ .font(.system(size: 23))
+ .foregroundStyle(.blue)
+
+ Text("MySQL Workbench")
+ .font(.system(size: 15, weight: .semibold))
+ .foregroundStyle(.black)
+
+ Rectangle()
+ .fill(WB.border)
+ .frame(width: 1, height: 30)
+
+ toolbarButton(
+ "New Connection",
+ icon: "plus.rectangle.on.rectangle"
+ ) {
+ editing = ConnectionProfile(name: "New Connection")
+ editingPassword = ""
+ showingEditor = true
+ }
+
+ toolbarButton(
+ "Refresh",
+ icon: "arrow.clockwise"
+ ) {
+ model.loadDatabases()
+ }
+
+ toolbarButton(
+ "Execute",
+ icon: "bolt.fill"
+ ) {
+ model.runSQL()
+ }
+ .keyboardShortcut(.return, modifiers: [.command])
+
+ toolbarButton(
+ "Format SQL",
+ icon: "text.alignleft"
+ ) {
+ model.formatSQL()
+ }
+
+ Spacer()
+
+ VStack(alignment: .trailing, spacing: 2) {
+ Text(model.selected?.name ?? "No Connection")
+ .font(.system(size: 12, weight: .semibold))
+
+ Text(model.selectedDatabase.isEmpty
+ ? "No default schema"
+ : model.selectedDatabase)
+ .font(.system(size: 11))
+ .foregroundStyle(.secondary)
+ }
+ }
+ .padding(.horizontal, 10)
+ .frame(height: 46)
+ .background(WB.toolbar)
+ }
+
+ private func toolbarButton(
+ _ title: String,
+ icon: String,
+ action: @escaping () -> Void
+ ) -> some View {
+ Button(action: action) {
+ VStack(spacing: 3) {
+ Image(systemName: icon)
+ .font(.system(size: 19))
+ .frame(height: 23)
+
+ Text(title)
+ .font(.system(size: 10))
+ }
+ .foregroundStyle(.black)
+ .frame(minWidth: 65)
+ }
+ .buttonStyle(.plain)
+ .help(title)
+ }
+
+ // MARK: - Navigator
+
+ private var navigator: some View {
+ VStack(spacing: 0) {
+ sectionHeader("NAVIGATOR")
+
+ HStack {
+ Text("SCHEMAS")
+ .font(.system(size: 11, weight: .bold))
+
+ Spacer()
+
+ Button {
+ model.loadDatabases()
+ } label: {
+ Image(systemName: "arrow.clockwise")
+ }
+ .buttonStyle(.borderless)
+ }
+ .padding(.horizontal, 12)
+ .frame(height: 34)
+
+ Divider()
+
+ ScrollView {
+ LazyVStack(alignment: .leading, spacing: 2) {
+ ForEach(model.databases, id: \.self) { db in
+ schemaNode(db)
+ }
+ }
+ .padding(.vertical, 8)
+ }
+
+ Divider()
+
+ sectionHeader("MYSQL CONNECTIONS")
+
+ ScrollView {
+ VStack(alignment: .leading, spacing: 4) {
+ ForEach(model.profiles) { profile in
+ Button {
+ model.selectedID = profile.id
+ model.loadDatabases()
+ } label: {
+ Label(
+ profile.name,
+ systemImage: "server.rack"
+ )
+ .frame(
+ maxWidth: .infinity,
+ alignment: .leading
+ )
+ .padding(6)
+ .background(
+ model.selectedID == profile.id
+ ? Color.blue.opacity(0.14)
+ : Color.clear
+ )
+ }
+ .buttonStyle(.plain)
+ .contextMenu {
+ Button("Edit Connection") {
+ edit(profile)
+ }
+ }
+ }
+ }
+ .padding(8)
+ }
+ .frame(height: 125)
+ }
+ .background(WB.sidebar)
+ }
+
+
+ // MARK: - Compact Schema Tree
+
+ private func schemaNode(_ db: String) -> some View {
+ VStack(alignment: .leading, spacing: 0) {
+
+ HStack(spacing: 5) {
+ Image(systemName:
+ expandedSchemas.contains(db)
+ ? "chevron.down"
+ : "chevron.right"
+ )
+ .font(.system(size: 10))
+ .foregroundStyle(.secondary)
+ .frame(width: 13)
+
+ Image(systemName: "cylinder.split.1x2")
+ .foregroundStyle(.blue)
+ .frame(width: 17)
+
+ Text(db)
+ .font(.system(size: 12, weight:
+ model.selectedDatabase == db
+ ? .semibold : .regular))
+ .lineLimit(1)
+
+ Spacer(minLength: 0)
+ }
+ .frame(height: 25)
+ .padding(.leading, 10)
+ .contentShape(Rectangle())
+ .onTapGesture {
+ toggleSchema(db)
+ }
+ .onTapGesture(count: 2) {
+ model.selectedDatabase = db
+ expandedSchemas.insert(db)
+ expandedTableFolders.insert(db)
+
+ if model.schemaTables[db] == nil {
+ model.loadTables(database: db)
+ }
+ }
+ .contextMenu {
+ Button("Set as Default Schema") {
+ model.selectedDatabase = db
+ }
+
+ Button("Refresh Tables") {
+ model.loadTables(database: db)
+ }
+ }
+
+ if expandedSchemas.contains(db) {
+ schemaChildren(db)
+ }
+ }
+ .frame(maxWidth: .infinity, alignment: .leading)
+ }
+
+ private func toggleSchema(_ db: String) {
+ if expandedSchemas.contains(db) {
+ expandedSchemas.remove(db)
+ } else {
+ expandedSchemas.insert(db)
+ expandedTableFolders.insert(db)
+
+ if model.schemaTables[db] == nil {
+ model.loadTables(database: db)
+ }
+ }
+ }
+
+ private func schemaChildren(_ db: String) -> some View {
+ VStack(alignment: .leading, spacing: 0) {
+
+ // Tables folder
+ HStack(spacing: 5) {
+ Image(systemName:
+ expandedTableFolders.contains(db)
+ ? "chevron.down"
+ : "chevron.right"
+ )
+ .font(.system(size: 10))
+ .foregroundStyle(.secondary)
+ .frame(width: 13)
+
+ Image(systemName: "folder")
+ .foregroundStyle(.secondary)
+ .frame(width: 17)
+
+ Text("Tables")
+ .font(.system(size: 12))
+
+ Spacer(minLength: 0)
+ }
+ .frame(height: 24)
+ .padding(.leading, 28)
+ .contentShape(Rectangle())
+ .onTapGesture {
+ if expandedTableFolders.contains(db) {
+ expandedTableFolders.remove(db)
+ } else {
+ expandedTableFolders.insert(db)
+
+ if model.schemaTables[db] == nil {
+ model.loadTables(database: db)
+ }
+ }
+ }
+
+ // Tables
+ if expandedTableFolders.contains(db) {
+ if let tables = model.schemaTables[db] {
+ ForEach(tables, id: \.self) { table in
+ tableNode(database: db, table: table)
+ }
+ } else {
+ Button("Load Tables") {
+ model.loadTables(database: db)
+ }
+ .font(.system(size: 11))
+ .buttonStyle(.borderless)
+ .padding(.leading, 66)
+ .frame(height: 24)
+ }
+ }
+
+ folderNode("Views", indent: 28)
+ folderNode("Stored Procedures", indent: 28)
+ folderNode("Functions", indent: 28)
+ }
+ .frame(maxWidth: .infinity, alignment: .leading)
+ }
+
+ private func tableNode(database: String, table: String) -> some View {
+ let identifier = database + "." + table
+ let isSelected = selectedTableName == identifier
+
+ return HStack(spacing: 5) {
+ Image(systemName: "tablecells")
+ .font(.system(size: 11))
+ .foregroundStyle(
+ isSelected ? Color.blue : Color.gray
+ )
+ .frame(width: 17)
+
+ Text(table)
+ .font(.system(size: 12))
+ .lineLimit(1)
+ .truncationMode(.middle)
+
+ Spacer(minLength: 0)
+ }
+ .frame(height: 23)
+ .padding(.leading, 49)
+ .frame(maxWidth: .infinity, alignment: .leading)
+ .background(
+ isSelected
+ ? Color.blue.opacity(0.14)
+ : Color.clear
+ )
+ .contentShape(Rectangle())
+ .onTapGesture {
+ selectedTableName = identifier
+ }
+ .onTapGesture(count: 2) {
+ selectedTableName = identifier
+ model.selectTable(
+ database: database,
+ table: table
+ )
+ }
+ .contextMenu {
+ Button("Select First 100 Rows") {
+ selectedTableName = identifier
+ model.selectTable(
+ database: database,
+ table: table
+ )
+ }
+ }
+ }
+
+ private func folderNode(
+ _ title: String,
+ indent: CGFloat
+ ) -> some View {
+ HStack(spacing: 5) {
+ Image(systemName: "chevron.right")
+ .font(.system(size: 10))
+ .foregroundStyle(.secondary)
+ .frame(width: 13)
+
+ Image(systemName: "folder")
+ .foregroundStyle(.secondary)
+ .frame(width: 17)
+
+ Text(title)
+ .font(.system(size: 12))
+
+ Spacer(minLength: 0)
+ }
+ .frame(height: 24)
+ .padding(.leading, indent)
+ .frame(maxWidth: .infinity, alignment: .leading)
+ }
+
+ // MARK: - Workspace
+
+ private var workspace: some View {
+ VStack(spacing: 0) {
+ queryTabBar
+
+ Divider()
+
+ queryToolbar
+
+ Divider()
+
+ VSplitView {
+ sqlEditor
+ .frame(minHeight: 230)
+
+ bottomWorkspace
+ .frame(minHeight: 230)
+ }
+ }
+ }
+
+ private var queryTabBar: some View {
+ HStack(spacing: 0) {
+ HStack(spacing: 8) {
+ Image(systemName: "doc.text")
+ .foregroundStyle(.blue)
+
+ Text("Query 1")
+ .font(.system(size: 12, weight: .semibold))
+
+ Image(systemName: "xmark")
+ .font(.system(size: 9))
+ .foregroundStyle(.secondary)
+ }
+ .padding(.horizontal, 10)
+ .frame(height: 29)
+ .background(Color(nsColor: .textBackgroundColor))
+
+ Spacer()
+ }
+ .background(WB.header)
+ }
+
+ private var queryToolbar: some View {
+ HStack(spacing: 14) {
+ Button {
+ model.runSQL()
+ } label: {
+ Image(systemName: "bolt.fill")
+ .foregroundStyle(.orange)
+ }
+ .help("Execute SQL")
+
+ Button {
+ model.formatSQL()
+ } label: {
+ Image(systemName: "text.alignleft")
+ }
+ .help("Format SQL")
+
+ Divider()
+ .frame(height: 17)
+
+ Text("Default Schema:")
+ .font(.system(size: 11))
+
+ Text(model.selectedDatabase.isEmpty
+ ? "None"
+ : model.selectedDatabase)
+ .font(.system(size: 11, weight: .semibold))
+
+ Spacer()
+
+ Text("SQL Editor")
+ .font(.system(size: 11))
+ .foregroundStyle(.secondary)
+ }
+ .buttonStyle(.borderless)
+ .padding(.horizontal, 12)
+ .frame(height: 30)
+ .background(WB.header)
+ }
+
+ private var sqlEditor: some View {
+ HStack(spacing: 0) {
+ lineNumbers
+
+ Rectangle()
+ .fill(WB.border)
+ .frame(width: 1)
+
+ SQLTextView(text: $model.sql)
+ }
+ .background(Color(nsColor: .textBackgroundColor))
+ }
+
+ private var lineNumbers: some View {
+ ScrollView {
+ VStack(alignment: .trailing, spacing: 0) {
+ ForEach(
+ 1...max(1, model.sql.components(
+ separatedBy: "\n"
+ ).count),
+ id: \.self
+ ) { number in
+ Text("\(number)")
+ .frame(height: 19)
+ }
+ }
+ .padding(.horizontal, 9)
+ .padding(.top, 8)
+ }
+ .font(.system(size: 12, design: .monospaced))
+ .foregroundStyle(.secondary)
+ .frame(width: 48)
+ .background(WB.header)
+ .allowsHitTesting(false)
+ }
+
+ // MARK: - Results and Action Output
+
+ private var bottomWorkspace: some View {
+ VSplitView {
+ resultsPanel
+ .frame(minHeight: 230)
+
+ if outputExpanded {
+ actionOutput
+ .frame(minHeight: 90)
+ }
+ }
+ }
+
+ private var resultsPanel: some View {
+ VStack(spacing: 0) {
+ HStack {
+ Image(systemName: "tablecells")
+ Text("Result Grid")
+ .fontWeight(.semibold)
+
+ Spacer()
+
+ Text(model.result.message)
+ .foregroundStyle(.secondary)
+ }
+ .font(.system(size: 12))
+ .padding(.horizontal, 12)
+ .frame(height: 28)
+ .background(WB.header)
+
+ Divider()
+
+ if model.result.columns.isEmpty {
+ VStack {
+ Spacer()
+ Text("No query results")
+ .foregroundStyle(.secondary)
+ Spacer()
+ }
+ } else {
+ resultGrid
+ }
+ }
+ }
+
+
+ private var resultGrid: some View {
+ NativeResultGrid(
+ columns: model.result.columns,
+ rows: model.result.rows
+ )
+ }
+
+ private var actionOutput: some View {
+ VStack(spacing: 0) {
+ HStack {
+ Image(systemName: "list.bullet.rectangle")
+ Text("Action Output")
+ .fontWeight(.semibold)
+
+ Spacer()
+
+ Button {
+ outputExpanded = false
+ } label: {
+ Image(systemName: "chevron.down")
+ }
+ .buttonStyle(.borderless)
+ }
+ .font(.system(size: 12))
+ .padding(.horizontal, 12)
+ .frame(height: 26)
+ .background(WB.header)
+
+ Divider()
+
+ HStack(alignment: .top, spacing: 12) {
+ Image(systemName:
+ model.status == "Error"
+ ? "xmark.circle.fill"
+ : "checkmark.circle.fill"
+ )
+ .foregroundStyle(
+ model.status == "Error"
+ ? .red : .green
+ )
+
+ VStack(alignment: .leading, spacing: 5) {
+ Text(model.status)
+ .fontWeight(.semibold)
+
+ Text(model.result.message)
+ .foregroundStyle(.secondary)
+ }
+
+ Spacer()
+ }
+ .font(.system(size: 12))
+ .padding(12)
+
+ Spacer()
+ }
+ }
+
+ // MARK: - Status Bar
+
+ private var statusBar: some View {
+ HStack(spacing: 10) {
+ Circle()
+ .fill(
+ model.status == "Error"
+ ? Color.red : Color.green
+ )
+ .frame(width: 8, height: 8)
+
+ Text(model.status)
+ .lineLimit(1)
+
+ Spacer()
+
+ Text(model.selected?.name ?? "Disconnected")
+
+ Divider()
+ .frame(height: 13)
+
+ Text(model.selectedDatabase.isEmpty
+ ? "No Schema"
+ : model.selectedDatabase)
+
+ Divider()
+ .frame(height: 13)
+
+ Text("UTF-8")
+ }
+ .font(.system(size: 11))
+ .padding(.horizontal, 12)
+ .frame(height: 22)
+ .background(WB.header)
+ }
+
+ private func sectionHeader(_ title: String) -> some View {
+ HStack {
+ Text(title)
+ .font(.system(size: 11, weight: .bold))
+ .foregroundStyle(.secondary)
+
+ Spacer()
+ }
+ .padding(.horizontal, 12)
+ .frame(height: 30)
+ .background(WB.header)
+ }
+
+ private func edit(_ profile: ConnectionProfile) {
+ editing = profile
+ editingPassword = PasswordStore.read(profile.id)
+ showingEditor = true
+ }
+}
+
+// MARK: - Native SQL Text Editor
+
+struct SQLTextView: NSViewRepresentable {
+ @Binding var text: String
+
+ func makeCoordinator() -> Coordinator {
+ Coordinator(self)
+ }
+
+ func makeNSView(context: Context) -> NSScrollView {
+ let scroll = NSTextView.scrollableTextView()
+ let editor = scroll.documentView as! NSTextView
+
+ scroll.appearance = NSAppearance(named: .aqua)
+ editor.appearance = NSAppearance(named: .aqua)
+ editor.backgroundColor = .white
+ editor.textColor = .black
+ scroll.backgroundColor = .white
+
+ editor.isEditable = true
+ editor.isRichText = false
+ editor.allowsUndo = true
+ editor.isAutomaticQuoteSubstitutionEnabled = false
+ editor.isAutomaticDashSubstitutionEnabled = false
+ editor.font = NSFont.monospacedSystemFont(
+ ofSize: 13,
+ weight: .regular
+ )
+ editor.textContainerInset = NSSize(
+ width: 10,
+ height: 8
+ )
+ editor.delegate = context.coordinator
+ editor.string = text
+
+ context.coordinator.highlight(editor)
+
+ return scroll
+ }
+
+ func updateNSView(
+ _ scroll: NSScrollView,
+ context: Context
+ ) {
+ guard let editor = scroll.documentView as? NSTextView else {
+ return
+ }
+
+ if editor.string != text {
+ let selection = editor.selectedRange()
+ editor.string = text
+
+ let safeLocation = min(
+ selection.location,
+ (text as NSString).length
+ )
+
+ editor.setSelectedRange(
+ NSRange(location: safeLocation, length: 0)
+ )
+
+ context.coordinator.highlight(editor)
+ }
+ }
+
+ final class Coordinator: NSObject, NSTextViewDelegate {
+ var parent: SQLTextView
+
+ init(_ parent: SQLTextView) {
+ self.parent = parent
+ }
+
+ func textDidChange(_ notification: Notification) {
+ guard let editor = notification.object as? NSTextView else {
+ return
+ }
+
+ parent.text = editor.string
+ highlight(editor)
+ }
+
+ func highlight(_ editor: NSTextView) {
+ guard let storage = editor.textStorage else {
+ return
+ }
+
+ let text = editor.string as NSString
+ let range = NSRange(
+ location: 0,
+ length: text.length
+ )
+
+ storage.beginEditing()
+
+ storage.setAttributes([
+ .foregroundColor: NSColor.textColor,
+ .font: NSFont.monospacedSystemFont(
+ ofSize: 13,
+ weight: .regular
+ )
+ ], range: range)
+
+ let pattern = """
+ \\b(SELECT|FROM|WHERE|INSERT|UPDATE|DELETE|CREATE|TABLE|DATABASE|SHOW|USE|JOIN|LEFT|RIGHT|INNER|OUTER|ON|AS|AND|OR|ORDER|GROUP|BY|LIMIT|VALUES|SET|NULL|NOT|IN|LIKE|DISTINCT|ALTER|DROP|DESCRIBE|EXPLAIN)\\b
+ """
+
+ if let regex = try? NSRegularExpression(
+ pattern: pattern,
+ options: [.caseInsensitive]
+ ) {
+ for match in regex.matches(
+ in: editor.string,
+ range: range
+ ) {
+ storage.addAttribute(
+ .foregroundColor,
+ value: NSColor.systemBlue,
+ range: match.range
+ )
+ }
+ }
+
+ storage.endEditing()
+ }
+ }
+}
+
+// MARK: - Connection Editor
+
+struct ConnectionEditor: View {
+ @Binding var profile: ConnectionProfile
+ @Binding var password: String
+
+ let save: () -> Void
+
+ @Environment(\.dismiss) private var dismiss
+
+ var body: some View {
+ VStack(alignment: .leading, spacing: 14) {
+ Text("Connection Settings")
+ .font(.title2.bold())
+
+ Form {
+ TextField(
+ "Connection Name",
+ text: $profile.name
+ )
+
+ TextField(
+ "Hostname",
+ text: $profile.host
+ )
+
+ TextField(
+ "Port",
+ value: $profile.port,
+ format: .number
+ )
+
+ TextField(
+ "Username",
+ text: $profile.username
+ )
+
+ SecureField(
+ "Password (macOS Keychain)",
+ text: $password
+ )
+
+ TextField(
+ "Default Schema",
+ text: $profile.database
+ )
+ }
+
+ HStack {
+ Spacer()
+
+ Button("Cancel") {
+ dismiss()
+ }
+
+ Button("Save") {
+ save()
+ }
+ .buttonStyle(.borderedProminent)
+ }
+ }
+ .padding(24)
+ .frame(width: 520)
+ }
+}
+
+
+// MARK: - High Performance Native Result Grid
+
+struct NativeResultGrid: NSViewRepresentable {
+
+ let columns: [String]
+ let rows: [[String]]
+
+ func makeCoordinator() -> Coordinator {
+ Coordinator()
+ }
+
+ func makeNSView(context: Context) -> NSScrollView {
+ let scroll = NSScrollView()
+
+ scroll.hasVerticalScroller = true
+ scroll.hasHorizontalScroller = true
+ scroll.autohidesScrollers = true
+ scroll.drawsBackground = true
+
+ let table = NSTableView()
+
+ table.usesAlternatingRowBackgroundColors = true
+ table.rowHeight = 24
+ table.intercellSpacing = NSSize(width: 1, height: 1)
+ table.allowsMultipleSelection = true
+ table.allowsColumnReordering = true
+ table.allowsColumnResizing = true
+ table.columnAutoresizingStyle = .noColumnAutoresizing
+
+ table.delegate = context.coordinator
+ table.dataSource = context.coordinator
+
+ scroll.documentView = table
+
+ context.coordinator.rows = rows
+ context.coordinator.columns = columns
+
+ configureColumns(table)
+
+ return scroll
+ }
+
+ func updateNSView(
+ _ scroll: NSScrollView,
+ context: Context
+ ) {
+ guard let table = scroll.documentView as? NSTableView else {
+ return
+ }
+
+ let oldColumns = context.coordinator.columns
+
+ context.coordinator.rows = rows
+ context.coordinator.columns = columns
+
+ if oldColumns != columns {
+ configureColumns(table)
+ }
+
+ table.reloadData()
+ }
+
+ private func configureColumns(_ table: NSTableView) {
+ for column in table.tableColumns {
+ table.removeTableColumn(column)
+ }
+
+ for (index, name) in columns.enumerated() {
+ let column = NSTableColumn(
+ identifier: NSUserInterfaceItemIdentifier(
+ "column_\\(index)"
+ )
+ )
+
+ column.title = name
+ column.width = 170
+ column.minWidth = 70
+ column.maxWidth = 600
+
+ table.addTableColumn(column)
+ }
+ }
+
+ final class Coordinator: NSObject,
+ NSTableViewDataSource,
+ NSTableViewDelegate {
+
+ var columns: [String] = []
+ var rows: [[String]] = []
+
+ func numberOfRows(in tableView: NSTableView) -> Int {
+ rows.count
+ }
+
+ func tableView(
+ _ tableView: NSTableView,
+ viewFor tableColumn: NSTableColumn?,
+ row: Int
+ ) -> NSView? {
+
+ guard let tableColumn,
+ row >= 0,
+ row < rows.count else {
+ return nil
+ }
+
+ let identifier = tableColumn.identifier.rawValue
+
+ guard let columnIndex = Int(
+ identifier.replacingOccurrences(
+ of: "column_",
+ with: ""
+ )
+ ) else {
+ return nil
+ }
+
+ let cellIdentifier =
+ NSUserInterfaceItemIdentifier("ResultCell")
+
+ let cell: NSTableCellView
+
+ if let reused = tableView.makeView(
+ withIdentifier: cellIdentifier,
+ owner: nil
+ ) as? NSTableCellView {
+ cell = reused
+ } else {
+ cell = NSTableCellView()
+ cell.identifier = cellIdentifier
+
+ let textField = NSTextField(labelWithString: "")
+ textField.font = NSFont.monospacedSystemFont(
+ ofSize: 12,
+ weight: .regular
+ )
+
+ textField.lineBreakMode = .byTruncatingTail
+ textField.translatesAutoresizingMaskIntoConstraints = false
+
+ cell.addSubview(textField)
+ cell.textField = textField
+
+ NSLayoutConstraint.activate([
+ textField.leadingAnchor.constraint(
+ equalTo: cell.leadingAnchor,
+ constant: 7
+ ),
+ textField.trailingAnchor.constraint(
+ equalTo: cell.trailingAnchor,
+ constant: -7
+ ),
+ textField.centerYAnchor.constraint(
+ equalTo: cell.centerYAnchor
+ )
+ ])
+ }
+
+ let values = rows[row]
+
+ cell.textField?.stringValue =
+ columnIndex < values.count
+ ? values[columnIndex]
+ : ""
+
+ return cell
+ }
+ }
+}
diff --git a/WorkbenchReplacement/MainView.swift.backup-20261016-172158 b/WorkbenchReplacement/MainView.swift.backup-20261016-172158
new file mode 100644
index 0000000..f8d56ef
--- /dev/null
+++ b/WorkbenchReplacement/MainView.swift.backup-20261016-172158
@@ -0,0 +1,1082 @@
+
+import SwiftUI
+import AppKit
+
+// MARK: - Classic Workbench Colours
+
+private enum WB {
+ static let toolbar = Color(nsColor: NSColor(
+ calibratedWhite: 0.88, alpha: 1
+ ))
+ static let sidebar = Color(nsColor: NSColor(
+ calibratedWhite: 0.94, alpha: 1
+ ))
+ static let border = Color(nsColor: NSColor(
+ calibratedWhite: 0.76, alpha: 1
+ ))
+ static let header = Color(nsColor: NSColor(
+ calibratedWhite: 0.91, alpha: 1
+ ))
+}
+
+// MARK: - Main Window
+
+struct MainView: View {
+ @EnvironmentObject private var model: DatabaseModel
+
+ @State private var showingEditor = false
+ @State private var editing = ConnectionProfile()
+ @State private var editingPassword = ""
+
+ @State private var expandedSchemas: Set = []
+ @State private var expandedTableFolders: Set = []
+ @State private var selectedTableName: String? = nil
+ @State private var outputExpanded = true
+
+ var body: some View {
+ VStack(spacing: 0) {
+ mainToolbar
+
+ Rectangle()
+ .fill(WB.border)
+ .frame(height: 1)
+
+ HSplitView {
+ navigator
+ .frame(
+ minWidth: 210,
+ idealWidth: 265,
+ maxWidth: 420
+ )
+
+ workspace
+ .frame(minWidth: 600)
+ }
+
+ statusBar
+ }
+ .frame(minWidth: 1100, minHeight: 720)
+ .preferredColorScheme(.light)
+ .background(Color(nsColor: .windowBackgroundColor))
+ .sheet(isPresented: $showingEditor) {
+ ConnectionEditor(
+ profile: $editing,
+ password: $editingPassword
+ ) {
+ model.save(editing, password: editingPassword)
+ showingEditor = false
+ }
+ }
+ }
+
+ // MARK: - Main Toolbar
+
+ private var mainToolbar: some View {
+ HStack(spacing: 16) {
+ Image(systemName: "externaldrive.connected.to.line.below")
+ .font(.system(size: 23))
+ .foregroundStyle(.blue)
+
+ Text("MySQL Workbench")
+ .font(.system(size: 15, weight: .semibold))
+ .foregroundStyle(.black)
+
+ Rectangle()
+ .fill(WB.border)
+ .frame(width: 1, height: 30)
+
+ toolbarButton(
+ "New Connection",
+ icon: "plus.rectangle.on.rectangle"
+ ) {
+ editing = ConnectionProfile(name: "New Connection")
+ editingPassword = ""
+ showingEditor = true
+ }
+
+ toolbarButton(
+ "Refresh",
+ icon: "arrow.clockwise"
+ ) {
+ model.loadDatabases()
+ }
+
+ toolbarButton(
+ "Execute",
+ icon: "bolt.fill"
+ ) {
+ model.runSQL()
+ }
+ .keyboardShortcut(.return, modifiers: [.command])
+
+ toolbarButton(
+ "Format SQL",
+ icon: "text.alignleft"
+ ) {
+ model.formatSQL()
+ }
+
+ Spacer()
+
+ VStack(alignment: .trailing, spacing: 2) {
+ Text(model.selected?.name ?? "No Connection")
+ .font(.system(size: 12, weight: .semibold))
+
+ Text(model.selectedDatabase.isEmpty
+ ? "No default schema"
+ : model.selectedDatabase)
+ .font(.system(size: 11))
+ .foregroundStyle(.secondary)
+ }
+ }
+ .padding(.horizontal, 10)
+ .frame(height: 46)
+ .background(WB.toolbar)
+ }
+
+ private func toolbarButton(
+ _ title: String,
+ icon: String,
+ action: @escaping () -> Void
+ ) -> some View {
+ Button(action: action) {
+ VStack(spacing: 3) {
+ Image(systemName: icon)
+ .font(.system(size: 19))
+ .frame(height: 23)
+
+ Text(title)
+ .font(.system(size: 10))
+ }
+ .foregroundStyle(.black)
+ .frame(minWidth: 65)
+ }
+ .buttonStyle(.plain)
+ .help(title)
+ }
+
+ // MARK: - Navigator
+
+ private var navigator: some View {
+ VStack(spacing: 0) {
+ sectionHeader("NAVIGATOR")
+
+ HStack {
+ Text("SCHEMAS")
+ .font(.system(size: 11, weight: .bold))
+
+ Spacer()
+
+ Button {
+ model.loadDatabases()
+ } label: {
+ Image(systemName: "arrow.clockwise")
+ }
+ .buttonStyle(.borderless)
+ }
+ .padding(.horizontal, 12)
+ .frame(height: 34)
+
+ Divider()
+
+ ScrollView {
+ LazyVStack(alignment: .leading, spacing: 2) {
+ ForEach(model.databases, id: \.self) { db in
+ schemaNode(db)
+ }
+ }
+ .padding(.vertical, 8)
+ }
+
+ Divider()
+
+ sectionHeader("MYSQL CONNECTIONS")
+
+ ScrollView {
+ VStack(alignment: .leading, spacing: 4) {
+ ForEach(model.profiles) { profile in
+ Button {
+ model.selectedID = profile.id
+ model.loadDatabases()
+ } label: {
+ Label(
+ profile.name,
+ systemImage: "server.rack"
+ )
+ .frame(
+ maxWidth: .infinity,
+ alignment: .leading
+ )
+ .padding(6)
+ .background(
+ model.selectedID == profile.id
+ ? Color.blue.opacity(0.14)
+ : Color.clear
+ )
+ }
+ .buttonStyle(.plain)
+ .contextMenu {
+ Button("Edit Connection") {
+ edit(profile)
+ }
+ }
+ }
+ }
+ .padding(8)
+ }
+ .frame(height: 125)
+ }
+ .background(WB.sidebar)
+ }
+
+
+ // MARK: - Compact Schema Tree
+
+ private func schemaNode(_ db: String) -> some View {
+ VStack(alignment: .leading, spacing: 0) {
+
+ HStack(spacing: 5) {
+ Image(systemName:
+ expandedSchemas.contains(db)
+ ? "chevron.down"
+ : "chevron.right"
+ )
+ .font(.system(size: 10))
+ .foregroundStyle(.secondary)
+ .frame(width: 13)
+
+ Image(systemName: "cylinder.split.1x2")
+ .foregroundStyle(.blue)
+ .frame(width: 17)
+
+ Text(db)
+ .font(.system(size: 12, weight:
+ model.selectedDatabase == db
+ ? .semibold : .regular))
+ .lineLimit(1)
+
+ Spacer(minLength: 0)
+ }
+ .frame(height: 25)
+ .padding(.leading, 10)
+ .contentShape(Rectangle())
+ .onTapGesture {
+ toggleSchema(db)
+ }
+ .onTapGesture(count: 2) {
+ model.selectedDatabase = db
+ expandedSchemas.insert(db)
+ expandedTableFolders.insert(db)
+
+ if model.schemaTables[db] == nil {
+ model.loadTables(database: db)
+ }
+ }
+ .contextMenu {
+ Button("Set as Default Schema") {
+ model.selectedDatabase = db
+ }
+
+ Button("Refresh Tables") {
+ model.loadTables(database: db)
+ }
+ }
+
+ if expandedSchemas.contains(db) {
+ schemaChildren(db)
+ }
+ }
+ .frame(maxWidth: .infinity, alignment: .leading)
+ }
+
+ private func toggleSchema(_ db: String) {
+ if expandedSchemas.contains(db) {
+ expandedSchemas.remove(db)
+ } else {
+ expandedSchemas.insert(db)
+ expandedTableFolders.insert(db)
+
+ if model.schemaTables[db] == nil {
+ model.loadTables(database: db)
+ }
+ }
+ }
+
+ private func schemaChildren(_ db: String) -> some View {
+ VStack(alignment: .leading, spacing: 0) {
+
+ // Tables folder
+ HStack(spacing: 5) {
+ Image(systemName:
+ expandedTableFolders.contains(db)
+ ? "chevron.down"
+ : "chevron.right"
+ )
+ .font(.system(size: 10))
+ .foregroundStyle(.secondary)
+ .frame(width: 13)
+
+ Image(systemName: "folder")
+ .foregroundStyle(.secondary)
+ .frame(width: 17)
+
+ Text("Tables")
+ .font(.system(size: 12))
+
+ Spacer(minLength: 0)
+ }
+ .frame(height: 24)
+ .padding(.leading, 28)
+ .contentShape(Rectangle())
+ .onTapGesture {
+ if expandedTableFolders.contains(db) {
+ expandedTableFolders.remove(db)
+ } else {
+ expandedTableFolders.insert(db)
+
+ if model.schemaTables[db] == nil {
+ model.loadTables(database: db)
+ }
+ }
+ }
+
+ // Tables
+ if expandedTableFolders.contains(db) {
+ if let tables = model.schemaTables[db] {
+ ForEach(tables, id: \.self) { table in
+ tableNode(database: db, table: table)
+ }
+ } else {
+ Button("Load Tables") {
+ model.loadTables(database: db)
+ }
+ .font(.system(size: 11))
+ .buttonStyle(.borderless)
+ .padding(.leading, 66)
+ .frame(height: 24)
+ }
+ }
+
+ folderNode("Views", indent: 28)
+ folderNode("Stored Procedures", indent: 28)
+ folderNode("Functions", indent: 28)
+ }
+ .frame(maxWidth: .infinity, alignment: .leading)
+ }
+
+ private func tableNode(database: String, table: String) -> some View {
+ let identifier = database + "." + table
+ let isSelected = selectedTableName == identifier
+
+ return HStack(spacing: 5) {
+ Image(systemName: "tablecells")
+ .font(.system(size: 11))
+ .foregroundStyle(
+ isSelected ? Color.blue : Color.gray
+ )
+ .frame(width: 17)
+
+ Text(table)
+ .font(.system(size: 12))
+ .lineLimit(1)
+ .truncationMode(.middle)
+
+ Spacer(minLength: 0)
+ }
+ .frame(height: 23)
+ .padding(.leading, 49)
+ .frame(maxWidth: .infinity, alignment: .leading)
+ .background(
+ isSelected
+ ? Color.blue.opacity(0.14)
+ : Color.clear
+ )
+ .contentShape(Rectangle())
+ .onTapGesture {
+ selectedTableName = identifier
+ }
+ .onTapGesture(count: 2) {
+ selectedTableName = identifier
+ model.selectTable(
+ database: database,
+ table: table
+ )
+ }
+ .contextMenu {
+ Button("Select First 100 Rows") {
+ selectedTableName = identifier
+ model.selectTable(
+ database: database,
+ table: table
+ )
+ }
+ }
+ }
+
+ private func folderNode(
+ _ title: String,
+ indent: CGFloat
+ ) -> some View {
+ HStack(spacing: 5) {
+ Image(systemName: "chevron.right")
+ .font(.system(size: 10))
+ .foregroundStyle(.secondary)
+ .frame(width: 13)
+
+ Image(systemName: "folder")
+ .foregroundStyle(.secondary)
+ .frame(width: 17)
+
+ Text(title)
+ .font(.system(size: 12))
+
+ Spacer(minLength: 0)
+ }
+ .frame(height: 24)
+ .padding(.leading, indent)
+ .frame(maxWidth: .infinity, alignment: .leading)
+ }
+
+ // MARK: - Workspace
+
+ private var workspace: some View {
+ VStack(spacing: 0) {
+ queryTabBar
+
+ Divider()
+
+ queryToolbar
+
+ Divider()
+
+ VSplitView {
+ sqlEditor
+ .frame(minHeight: 230)
+
+ bottomWorkspace
+ .frame(minHeight: 230)
+ }
+ }
+ }
+
+ private var queryTabBar: some View {
+ HStack(spacing: 0) {
+ HStack(spacing: 8) {
+ Image(systemName: "doc.text")
+ .foregroundStyle(.blue)
+
+ Text("Query 1")
+ .font(.system(size: 12, weight: .semibold))
+
+ Image(systemName: "xmark")
+ .font(.system(size: 9))
+ .foregroundStyle(.secondary)
+ }
+ .padding(.horizontal, 10)
+ .frame(height: 29)
+ .background(Color(nsColor: .textBackgroundColor))
+
+ Spacer()
+ }
+ .background(WB.header)
+ }
+
+ private var queryToolbar: some View {
+ HStack(spacing: 14) {
+ Button {
+ model.runSQL()
+ } label: {
+ Image(systemName: "bolt.fill")
+ .foregroundStyle(.orange)
+ }
+ .help("Execute SQL")
+
+ Button {
+ model.formatSQL()
+ } label: {
+ Image(systemName: "text.alignleft")
+ }
+ .help("Format SQL")
+
+ Divider()
+ .frame(height: 17)
+
+ Text("Default Schema:")
+ .font(.system(size: 11))
+
+ Text(model.selectedDatabase.isEmpty
+ ? "None"
+ : model.selectedDatabase)
+ .font(.system(size: 11, weight: .semibold))
+
+ Spacer()
+
+ Text("SQL Editor")
+ .font(.system(size: 11))
+ .foregroundStyle(.secondary)
+ }
+ .buttonStyle(.borderless)
+ .padding(.horizontal, 12)
+ .frame(height: 30)
+ .background(WB.header)
+ }
+
+ private var sqlEditor: some View {
+ HStack(spacing: 0) {
+ lineNumbers
+
+ Rectangle()
+ .fill(WB.border)
+ .frame(width: 1)
+
+ SQLTextView(text: $model.sql)
+ }
+ .background(Color(nsColor: .textBackgroundColor))
+ }
+
+ private var lineNumbers: some View {
+ ScrollView {
+ VStack(alignment: .trailing, spacing: 0) {
+ ForEach(
+ 1...max(1, model.sql.components(
+ separatedBy: "\n"
+ ).count),
+ id: \.self
+ ) { number in
+ Text("\(number)")
+ .frame(height: 19)
+ }
+ }
+ .padding(.horizontal, 9)
+ .padding(.top, 8)
+ }
+ .font(.system(size: 12, design: .monospaced))
+ .foregroundStyle(.secondary)
+ .frame(width: 48)
+ .background(WB.header)
+ .allowsHitTesting(false)
+ }
+
+ // MARK: - Results and Action Output
+
+ private var bottomWorkspace: some View {
+ VSplitView {
+ resultsPanel
+ .frame(minHeight: 230)
+
+ if outputExpanded {
+ actionOutput
+ .frame(minHeight: 90)
+ }
+ }
+ }
+
+ private var resultsPanel: some View {
+ VStack(spacing: 0) {
+ HStack {
+ Image(systemName: "tablecells")
+ Text("Result Grid")
+ .fontWeight(.semibold)
+
+ Spacer()
+
+ Text(model.result.message)
+ .foregroundStyle(.secondary)
+ }
+ .font(.system(size: 12))
+ .padding(.horizontal, 12)
+ .frame(height: 28)
+ .background(WB.header)
+
+ Divider()
+
+ if model.result.columns.isEmpty {
+ VStack {
+ Spacer()
+ Text("No query results")
+ .foregroundStyle(.secondary)
+ Spacer()
+ }
+ } else {
+ resultGrid
+ }
+ }
+ }
+
+
+ private var resultGrid: some View {
+ NativeResultGrid(
+ columns: model.result.columns,
+ rows: model.result.rows
+ )
+ }
+
+ private var actionOutput: some View {
+ VStack(spacing: 0) {
+ HStack {
+ Image(systemName: "list.bullet.rectangle")
+ Text("Action Output")
+ .fontWeight(.semibold)
+
+ Spacer()
+
+ Button {
+ outputExpanded = false
+ } label: {
+ Image(systemName: "chevron.down")
+ }
+ .buttonStyle(.borderless)
+ }
+ .font(.system(size: 12))
+ .padding(.horizontal, 12)
+ .frame(height: 26)
+ .background(WB.header)
+
+ Divider()
+
+ HStack(alignment: .top, spacing: 12) {
+ Image(systemName:
+ model.status == "Error"
+ ? "xmark.circle.fill"
+ : "checkmark.circle.fill"
+ )
+ .foregroundStyle(
+ model.status == "Error"
+ ? .red : .green
+ )
+
+ VStack(alignment: .leading, spacing: 5) {
+ Text(model.status)
+ .fontWeight(.semibold)
+
+ Text(model.result.message)
+ .foregroundStyle(.secondary)
+ }
+
+ Spacer()
+ }
+ .font(.system(size: 12))
+ .padding(12)
+
+ Spacer()
+ }
+ }
+
+ // MARK: - Status Bar
+
+ private var statusBar: some View {
+ HStack(spacing: 10) {
+ Circle()
+ .fill(
+ model.status == "Error"
+ ? Color.red : Color.green
+ )
+ .frame(width: 8, height: 8)
+
+ Text(model.status)
+ .lineLimit(1)
+
+ Spacer()
+
+ Text(model.selected?.name ?? "Disconnected")
+
+ Divider()
+ .frame(height: 13)
+
+ Text(model.selectedDatabase.isEmpty
+ ? "No Schema"
+ : model.selectedDatabase)
+
+ Divider()
+ .frame(height: 13)
+
+ Text("UTF-8")
+ }
+ .font(.system(size: 11))
+ .padding(.horizontal, 12)
+ .frame(height: 22)
+ .background(WB.header)
+ }
+
+ private func sectionHeader(_ title: String) -> some View {
+ HStack {
+ Text(title)
+ .font(.system(size: 11, weight: .bold))
+ .foregroundStyle(.secondary)
+
+ Spacer()
+ }
+ .padding(.horizontal, 12)
+ .frame(height: 30)
+ .background(WB.header)
+ }
+
+ private func edit(_ profile: ConnectionProfile) {
+ editing = profile
+ editingPassword = PasswordStore.read(profile.id)
+ showingEditor = true
+ }
+}
+
+// MARK: - Native SQL Text Editor
+
+struct SQLTextView: NSViewRepresentable {
+ @Binding var text: String
+
+ func makeCoordinator() -> Coordinator {
+ Coordinator(self)
+ }
+
+ func makeNSView(context: Context) -> NSScrollView {
+ let scroll = NSTextView.scrollableTextView()
+ let editor = scroll.documentView as! NSTextView
+
+ scroll.appearance = NSAppearance(named: .aqua)
+ editor.appearance = NSAppearance(named: .aqua)
+ editor.backgroundColor = .white
+ editor.textColor = .black
+ scroll.backgroundColor = .white
+
+ editor.isEditable = true
+ editor.isRichText = false
+ editor.allowsUndo = true
+ editor.isAutomaticQuoteSubstitutionEnabled = false
+ editor.isAutomaticDashSubstitutionEnabled = false
+ editor.font = NSFont.monospacedSystemFont(
+ ofSize: 13,
+ weight: .regular
+ )
+ editor.textContainerInset = NSSize(
+ width: 10,
+ height: 8
+ )
+ editor.delegate = context.coordinator
+ editor.string = text
+
+ context.coordinator.highlight(editor)
+
+ return scroll
+ }
+
+ func updateNSView(
+ _ scroll: NSScrollView,
+ context: Context
+ ) {
+ guard let editor = scroll.documentView as? NSTextView else {
+ return
+ }
+
+ if editor.string != text {
+ let selection = editor.selectedRange()
+ editor.string = text
+
+ let safeLocation = min(
+ selection.location,
+ (text as NSString).length
+ )
+
+ editor.setSelectedRange(
+ NSRange(location: safeLocation, length: 0)
+ )
+
+ context.coordinator.highlight(editor)
+ }
+ }
+
+ final class Coordinator: NSObject, NSTextViewDelegate {
+ var parent: SQLTextView
+
+ init(_ parent: SQLTextView) {
+ self.parent = parent
+ }
+
+ func textDidChange(_ notification: Notification) {
+ guard let editor = notification.object as? NSTextView else {
+ return
+ }
+
+ parent.text = editor.string
+ highlight(editor)
+ }
+
+ func highlight(_ editor: NSTextView) {
+ guard let storage = editor.textStorage else {
+ return
+ }
+
+ let text = editor.string as NSString
+ let range = NSRange(
+ location: 0,
+ length: text.length
+ )
+
+ storage.beginEditing()
+
+ storage.setAttributes([
+ .foregroundColor: NSColor.textColor,
+ .font: NSFont.monospacedSystemFont(
+ ofSize: 13,
+ weight: .regular
+ )
+ ], range: range)
+
+ let pattern = """
+ \\b(SELECT|FROM|WHERE|INSERT|UPDATE|DELETE|CREATE|TABLE|DATABASE|SHOW|USE|JOIN|LEFT|RIGHT|INNER|OUTER|ON|AS|AND|OR|ORDER|GROUP|BY|LIMIT|VALUES|SET|NULL|NOT|IN|LIKE|DISTINCT|ALTER|DROP|DESCRIBE|EXPLAIN)\\b
+ """
+
+ if let regex = try? NSRegularExpression(
+ pattern: pattern,
+ options: [.caseInsensitive]
+ ) {
+ for match in regex.matches(
+ in: editor.string,
+ range: range
+ ) {
+ storage.addAttribute(
+ .foregroundColor,
+ value: NSColor.systemBlue,
+ range: match.range
+ )
+ }
+ }
+
+ storage.endEditing()
+ }
+ }
+}
+
+// MARK: - Connection Editor
+
+struct ConnectionEditor: View {
+ @Binding var profile: ConnectionProfile
+ @Binding var password: String
+
+ let save: () -> Void
+
+ @Environment(\.dismiss) private var dismiss
+
+ var body: some View {
+ VStack(alignment: .leading, spacing: 14) {
+ Text("Connection Settings")
+ .font(.title2.bold())
+
+ Form {
+ TextField(
+ "Connection Name",
+ text: $profile.name
+ )
+
+ TextField(
+ "Hostname",
+ text: $profile.host
+ )
+
+ TextField(
+ "Port",
+ value: $profile.port,
+ format: .number
+ )
+
+ TextField(
+ "Username",
+ text: $profile.username
+ )
+
+ SecureField(
+ "Password (macOS Keychain)",
+ text: $password
+ )
+
+ TextField(
+ "Default Schema",
+ text: $profile.database
+ )
+ }
+
+ HStack {
+ Spacer()
+
+ Button("Cancel") {
+ dismiss()
+ }
+
+ Button("Save") {
+ save()
+ }
+ .buttonStyle(.borderedProminent)
+ }
+ }
+ .padding(24)
+ .frame(width: 520)
+ }
+}
+
+
+
+// MARK: - High Performance Native Result Grid
+
+struct NativeResultGrid: NSViewRepresentable {
+
+ let columns: [String]
+ let rows: [[String]]
+
+ func makeCoordinator() -> Coordinator {
+ Coordinator()
+ }
+
+ func makeNSView(context: Context) -> NSScrollView {
+ let scroll = NSScrollView()
+ scroll.hasVerticalScroller = true
+ scroll.hasHorizontalScroller = true
+ scroll.autohidesScrollers = true
+ scroll.drawsBackground = true
+ scroll.backgroundColor = .white
+
+ let table = NSTableView()
+ table.headerView = NSTableHeaderView()
+ table.usesAlternatingRowBackgroundColors = true
+ table.rowHeight = 23
+ table.intercellSpacing = NSSize(width: 1, height: 1)
+ table.allowsMultipleSelection = true
+ table.allowsColumnResizing = true
+ table.allowsColumnReordering = true
+ table.columnAutoresizingStyle = .noColumnAutoresizing
+ table.backgroundColor = .white
+
+ context.coordinator.columns = columns
+ context.coordinator.rows = rows
+
+ table.delegate = context.coordinator
+ table.dataSource = context.coordinator
+
+ configureColumns(table)
+
+ scroll.documentView = table
+ table.reloadData()
+
+ return scroll
+ }
+
+ func updateNSView(
+ _ scroll: NSScrollView,
+ context: Context
+ ) {
+ guard let table = scroll.documentView as? NSTableView else {
+ return
+ }
+
+ let oldColumns = context.coordinator.columns
+
+ context.coordinator.columns = columns
+ context.coordinator.rows = rows
+
+ if oldColumns != columns {
+ configureColumns(table)
+ }
+
+ table.reloadData()
+ }
+
+ private func configureColumns(_ table: NSTableView) {
+ for column in Array(table.tableColumns) {
+ table.removeTableColumn(column)
+ }
+
+ for (index, name) in columns.enumerated() {
+ let identifier = NSUserInterfaceItemIdentifier(
+ "column_\(index)"
+ )
+
+ let column = NSTableColumn(identifier: identifier)
+ column.title = name
+ column.width = 170
+ column.minWidth = 70
+ column.maxWidth = 700
+ column.resizingMask = .userResizingMask
+
+ table.addTableColumn(column)
+ }
+ }
+
+ final class Coordinator: NSObject,
+ NSTableViewDataSource,
+ NSTableViewDelegate {
+
+ var columns: [String] = []
+ var rows: [[String]] = []
+
+ func numberOfRows(in tableView: NSTableView) -> Int {
+ rows.count
+ }
+
+ func tableView(
+ _ tableView: NSTableView,
+ viewFor tableColumn: NSTableColumn?,
+ row: Int
+ ) -> NSView? {
+
+ guard let tableColumn,
+ row >= 0,
+ row < rows.count else {
+ return nil
+ }
+
+ let identifier = tableColumn.identifier.rawValue
+
+ guard identifier.hasPrefix("column_"),
+ let columnIndex = Int(identifier.dropFirst(7)),
+ columnIndex < columns.count else {
+ return nil
+ }
+
+ let cellIdentifier =
+ NSUserInterfaceItemIdentifier("ResultCell")
+
+ let cell: NSTableCellView
+
+ if let reused = tableView.makeView(
+ withIdentifier: cellIdentifier,
+ owner: nil
+ ) as? NSTableCellView {
+ cell = reused
+ } else {
+ cell = NSTableCellView()
+ cell.identifier = cellIdentifier
+
+ let field = NSTextField(labelWithString: "")
+ field.font = NSFont.monospacedSystemFont(
+ ofSize: 12,
+ weight: .regular
+ )
+ field.lineBreakMode = .byTruncatingTail
+ field.translatesAutoresizingMaskIntoConstraints = false
+
+ cell.addSubview(field)
+ cell.textField = field
+
+ NSLayoutConstraint.activate([
+ field.leadingAnchor.constraint(
+ equalTo: cell.leadingAnchor,
+ constant: 7
+ ),
+ field.trailingAnchor.constraint(
+ equalTo: cell.trailingAnchor,
+ constant: -7
+ ),
+ field.centerYAnchor.constraint(
+ equalTo: cell.centerYAnchor
+ )
+ ])
+ }
+
+ let values = rows[row]
+
+ cell.textField?.stringValue =
+ columnIndex < values.count
+ ? values[columnIndex]
+ : ""
+
+ return cell
+ }
+ }
+}
diff --git a/build/Build/Intermediates.noindex/WorkbenchReplacement.build/Debug/WorkbenchReplacement.build/Objects-normal/arm64/App.o b/build/Build/Intermediates.noindex/WorkbenchReplacement.build/Debug/WorkbenchReplacement.build/Objects-normal/arm64/App.o
index 965b586..d116e6e 100644
Binary files a/build/Build/Intermediates.noindex/WorkbenchReplacement.build/Debug/WorkbenchReplacement.build/Objects-normal/arm64/App.o and b/build/Build/Intermediates.noindex/WorkbenchReplacement.build/Debug/WorkbenchReplacement.build/Objects-normal/arm64/App.o differ
diff --git a/build/Build/Intermediates.noindex/WorkbenchReplacement.build/Debug/WorkbenchReplacement.build/Objects-normal/arm64/App.stringsdata b/build/Build/Intermediates.noindex/WorkbenchReplacement.build/Debug/WorkbenchReplacement.build/Objects-normal/arm64/App.stringsdata
index 2e02fcb..0724fec 100644
--- a/build/Build/Intermediates.noindex/WorkbenchReplacement.build/Debug/WorkbenchReplacement.build/Objects-normal/arm64/App.stringsdata
+++ b/build/Build/Intermediates.noindex/WorkbenchReplacement.build/Debug/WorkbenchReplacement.build/Objects-normal/arm64/App.stringsdata
@@ -9,6 +9,158 @@
"startingColumn": 21,
"startingLine": 8
}
+ },
+ {
+ "comment": "",
+ "key": "SQL Editor",
+ "location": {
+ "startingColumn": 25,
+ "startingLine": 97
+ }
+ },
+ {
+ "comment": "",
+ "key": "Query Execution",
+ "location": {
+ "startingColumn": 25,
+ "startingLine": 105
+ }
+ },
+ {
+ "comment": "",
+ "key": "Result Grid",
+ "location": {
+ "startingColumn": 25,
+ "startingLine": 113
+ }
+ },
+ {
+ "comment": "",
+ "key": "General",
+ "location": {
+ "startingColumn": 25,
+ "startingLine": 121
+ }
+ },
+ {
+ "comment": "",
+ "key": "%lld pt",
+ "location": {
+ "startingColumn": 21,
+ "startingLine": 135
+ }
+ },
+ {
+ "comment": "",
+ "key": "",
+ "location": {
+ "startingColumn": 24,
+ "startingLine": 144
+ }
+ },
+ {
+ "comment": "",
+ "key": "2 spaces",
+ "location": {
+ "startingColumn": 26,
+ "startingLine": 145
+ }
+ },
+ {
+ "comment": "",
+ "key": "4 spaces",
+ "location": {
+ "startingColumn": 26,
+ "startingLine": 146
+ }
+ },
+ {
+ "comment": "",
+ "key": "8 spaces",
+ "location": {
+ "startingColumn": 26,
+ "startingLine": 147
+ }
+ },
+ {
+ "comment": "",
+ "key": "Automatically uppercase SQL keywords",
+ "location": {
+ "startingColumn": 21,
+ "startingLine": 155
+ }
+ },
+ {
+ "comment": "",
+ "key": "Show line numbers",
+ "location": {
+ "startingColumn": 21,
+ "startingLine": 162
+ }
+ },
+ {
+ "comment": "",
+ "key": "Controls the number of rows retrieved when opening a table from the schema navigator.",
+ "location": {
+ "startingColumn": 17,
+ "startingLine": 193
+ }
+ },
+ {
+ "comment": "",
+ "key": "Examples",
+ "location": {
+ "startingColumn": 18,
+ "startingLine": 203
+ }
+ },
+ {
+ "comment": "",
+ "key": "",
+ "location": {
+ "startingColumn": 25,
+ "startingLine": 175
+ }
+ },
+ {
+ "comment": "",
+ "key": "",
+ "location": {
+ "startingColumn": 25,
+ "startingLine": 183
+ }
+ },
+ {
+ "comment": "",
+ "key": "%lld px",
+ "location": {
+ "startingColumn": 21,
+ "startingLine": 222
+ }
+ },
+ {
+ "comment": "",
+ "key": "%lld px",
+ "location": {
+ "startingColumn": 21,
+ "startingLine": 232
+ }
+ },
+ {
+ "comment": "",
+ "key": "Alternating row colours",
+ "location": {
+ "startingColumn": 21,
+ "startingLine": 242
+ }
+ },
+ {
+ "comment": "",
+ "key": "Automatically expand table folders",
+ "location": {
+ "startingColumn": 21,
+ "startingLine": 254
+ }
}
]
},
diff --git a/build/Build/Intermediates.noindex/WorkbenchReplacement.build/Debug/WorkbenchReplacement.build/Objects-normal/arm64/App.swiftdeps b/build/Build/Intermediates.noindex/WorkbenchReplacement.build/Debug/WorkbenchReplacement.build/Objects-normal/arm64/App.swiftdeps
index 028fb6e..c94be9d 100644
Binary files a/build/Build/Intermediates.noindex/WorkbenchReplacement.build/Debug/WorkbenchReplacement.build/Objects-normal/arm64/App.swiftdeps and b/build/Build/Intermediates.noindex/WorkbenchReplacement.build/Debug/WorkbenchReplacement.build/Objects-normal/arm64/App.swiftdeps differ
diff --git a/build/Build/Intermediates.noindex/WorkbenchReplacement.build/Debug/WorkbenchReplacement.build/Objects-normal/arm64/DatabaseModel.dia b/build/Build/Intermediates.noindex/WorkbenchReplacement.build/Debug/WorkbenchReplacement.build/Objects-normal/arm64/DatabaseModel.dia
index d338a45..093d351 100644
Binary files a/build/Build/Intermediates.noindex/WorkbenchReplacement.build/Debug/WorkbenchReplacement.build/Objects-normal/arm64/DatabaseModel.dia and b/build/Build/Intermediates.noindex/WorkbenchReplacement.build/Debug/WorkbenchReplacement.build/Objects-normal/arm64/DatabaseModel.dia differ
diff --git a/build/Build/Intermediates.noindex/WorkbenchReplacement.build/Debug/WorkbenchReplacement.build/Objects-normal/arm64/DatabaseModel.o b/build/Build/Intermediates.noindex/WorkbenchReplacement.build/Debug/WorkbenchReplacement.build/Objects-normal/arm64/DatabaseModel.o
index e049e9d..aaf4c61 100644
Binary files a/build/Build/Intermediates.noindex/WorkbenchReplacement.build/Debug/WorkbenchReplacement.build/Objects-normal/arm64/DatabaseModel.o and b/build/Build/Intermediates.noindex/WorkbenchReplacement.build/Debug/WorkbenchReplacement.build/Objects-normal/arm64/DatabaseModel.o differ
diff --git a/build/Build/Intermediates.noindex/WorkbenchReplacement.build/Debug/WorkbenchReplacement.build/Objects-normal/arm64/DatabaseModel.swiftdeps b/build/Build/Intermediates.noindex/WorkbenchReplacement.build/Debug/WorkbenchReplacement.build/Objects-normal/arm64/DatabaseModel.swiftdeps
index 2c62cd1..4965eb7 100644
Binary files a/build/Build/Intermediates.noindex/WorkbenchReplacement.build/Debug/WorkbenchReplacement.build/Objects-normal/arm64/DatabaseModel.swiftdeps and b/build/Build/Intermediates.noindex/WorkbenchReplacement.build/Debug/WorkbenchReplacement.build/Objects-normal/arm64/DatabaseModel.swiftdeps differ
diff --git a/build/Build/Intermediates.noindex/WorkbenchReplacement.build/Debug/WorkbenchReplacement.build/Objects-normal/arm64/MainView.o b/build/Build/Intermediates.noindex/WorkbenchReplacement.build/Debug/WorkbenchReplacement.build/Objects-normal/arm64/MainView.o
index de5b814..22df5ce 100644
Binary files a/build/Build/Intermediates.noindex/WorkbenchReplacement.build/Debug/WorkbenchReplacement.build/Objects-normal/arm64/MainView.o and b/build/Build/Intermediates.noindex/WorkbenchReplacement.build/Debug/WorkbenchReplacement.build/Objects-normal/arm64/MainView.o differ
diff --git a/build/Build/Intermediates.noindex/WorkbenchReplacement.build/Debug/WorkbenchReplacement.build/Objects-normal/arm64/MainView.stringsdata b/build/Build/Intermediates.noindex/WorkbenchReplacement.build/Debug/WorkbenchReplacement.build/Objects-normal/arm64/MainView.stringsdata
index 983ef73..2d4d09c 100644
--- a/build/Build/Intermediates.noindex/WorkbenchReplacement.build/Debug/WorkbenchReplacement.build/Objects-normal/arm64/MainView.stringsdata
+++ b/build/Build/Intermediates.noindex/WorkbenchReplacement.build/Debug/WorkbenchReplacement.build/Objects-normal/arm64/MainView.stringsdata
@@ -4,42 +4,18 @@
"Localizable": [
{
"comment": "",
- "key": "WorkbenchReplacement",
+ "key": "MySQL Workbench",
"location": {
- "startingColumn": 26,
- "startingLine": 187
- }
- },
- {
- "comment": "",
- "key": "NAVIGATOR",
- "location": {
- "startingColumn": 22,
- "startingLine": 41
- }
- },
- {
- "comment": "",
- "key": "Refresh schemas",
- "location": {
- "startingColumn": 23,
- "startingLine": 53
- }
- },
- {
- "comment": "",
- "key": "MYSQL CONNECTIONS",
- "location": {
- "startingColumn": 25,
- "startingLine": 61
+ "startingColumn": 18,
+ "startingLine": 84
}
},
{
"comment": "",
"key": "SCHEMAS",
"location": {
- "startingColumn": 25,
- "startingLine": 76
+ "startingColumn": 22,
+ "startingLine": 169
}
},
{
@@ -47,87 +23,47 @@
"key": "Edit Connection",
"location": {
"startingColumn": 36,
- "startingLine": 69
- }
- },
- {
- "comment": "",
- "key": "No tables",
- "location": {
- "startingColumn": 22,
- "startingLine": 137
- }
- },
- {
- "comment": "",
- "key": "Load Tables",
- "location": {
- "startingColumn": 20,
- "startingLine": 160
+ "startingLine": 223
}
},
{
"comment": "",
"key": "Set as Default Schema",
"location": {
- "startingColumn": 40,
- "startingLine": 90
+ "startingColumn": 24,
+ "startingLine": 280
}
},
{
"comment": "",
"key": "Refresh Tables",
"location": {
- "startingColumn": 40,
- "startingLine": 94
+ "startingColumn": 24,
+ "startingLine": 284
}
},
{
"comment": "",
- "key": "New Connection",
+ "key": "Load Tables",
"location": {
- "startingColumn": 25,
- "startingLine": 121
+ "startingColumn": 28,
+ "startingLine": 354
+ }
+ },
+ {
+ "comment": "",
+ "key": "Tables",
+ "location": {
+ "startingColumn": 22,
+ "startingLine": 327
}
},
{
"comment": "",
"key": "Select First 100 Rows",
"location": {
- "startingColumn": 36,
- "startingLine": 150
- }
- },
- {
- "comment": "",
- "key": "SQL Editor",
- "location": {
- "startingColumn": 17,
- "startingLine": 194
- }
- },
- {
- "comment": "",
- "key": "Refresh schemas",
- "location": {
- "startingColumn": 19,
- "startingLine": 241
- }
- },
- {
- "comment": "",
- "key": "Execute",
- "location": {
- "startingColumn": 21,
- "startingLine": 207
- }
- },
- {
- "comment": "",
- "key": "Format SQL",
- "location": {
- "startingColumn": 21,
- "startingLine": 221
+ "startingColumn": 20,
+ "startingLine": 410
}
},
{
@@ -135,39 +71,79 @@
"key": "Query 1",
"location": {
"startingColumn": 22,
- "startingLine": 251
+ "startingLine": 472
}
},
{
"comment": "",
- "key": "MySQL",
+ "key": "Default Schema:",
"location": {
- "startingColumn": 22,
- "startingLine": 256
+ "startingColumn": 18,
+ "startingLine": 508
}
},
{
"comment": "",
- "key": "Execute a SQL query to display results.",
+ "key": "SQL Editor",
"location": {
- "startingColumn": 25,
- "startingLine": 307
+ "startingColumn": 18,
+ "startingLine": 518
}
},
{
"comment": "",
- "key": "No Results",
+ "key": "Execute SQL",
"location": {
- "startingColumn": 21,
- "startingLine": 304
+ "startingColumn": 19,
+ "startingLine": 496
+ }
+ },
+ {
+ "comment": "",
+ "key": "Format SQL",
+ "location": {
+ "startingColumn": 19,
+ "startingLine": 503
+ }
+ },
+ {
+ "comment": "",
+ "key": "%lld",
+ "location": {
+ "startingColumn": 26,
+ "startingLine": 558
}
},
{
"comment": "",
"key": "Result Grid",
"location": {
- "startingColumn": 21,
- "startingLine": 287
+ "startingColumn": 22,
+ "startingLine": 590
+ }
+ },
+ {
+ "comment": "",
+ "key": "No query results",
+ "location": {
+ "startingColumn": 26,
+ "startingLine": 608
+ }
+ },
+ {
+ "comment": "",
+ "key": "Action Output",
+ "location": {
+ "startingColumn": 22,
+ "startingLine": 630
+ }
+ },
+ {
+ "comment": "",
+ "key": "UTF-8",
+ "location": {
+ "startingColumn": 18,
+ "startingLine": 705
}
},
{
@@ -175,15 +151,7 @@
"key": "Connection Settings",
"location": {
"startingColumn": 18,
- "startingLine": 431
- }
- },
- {
- "comment": "",
- "key": "Native MySQL connection. Local TCP connections and SSH tunnels are supported.",
- "location": {
- "startingColumn": 17,
- "startingLine": 468
+ "startingLine": 880
}
},
{
@@ -191,7 +159,7 @@
"key": "Connection Name",
"location": {
"startingColumn": 21,
- "startingLine": 436
+ "startingLine": 885
}
},
{
@@ -199,7 +167,7 @@
"key": "Hostname",
"location": {
"startingColumn": 21,
- "startingLine": 441
+ "startingLine": 890
}
},
{
@@ -207,7 +175,7 @@
"key": "Port",
"location": {
"startingColumn": 21,
- "startingLine": 446
+ "startingLine": 895
}
},
{
@@ -215,15 +183,15 @@
"key": "Username",
"location": {
"startingColumn": 21,
- "startingLine": 452
+ "startingLine": 901
}
},
{
"comment": "",
- "key": "Password (stored in macOS Keychain)",
+ "key": "Password (macOS Keychain)",
"location": {
"startingColumn": 21,
- "startingLine": 457
+ "startingLine": 906
}
},
{
@@ -231,7 +199,7 @@
"key": "Default Schema",
"location": {
"startingColumn": 21,
- "startingLine": 462
+ "startingLine": 911
}
},
{
@@ -239,7 +207,7 @@
"key": "Cancel",
"location": {
"startingColumn": 24,
- "startingLine": 476
+ "startingLine": 919
}
},
{
@@ -247,7 +215,7 @@
"key": "Save",
"location": {
"startingColumn": 24,
- "startingLine": 480
+ "startingLine": 923
}
}
]
diff --git a/build/Build/Intermediates.noindex/WorkbenchReplacement.build/Debug/WorkbenchReplacement.build/Objects-normal/arm64/MainView.swiftdeps b/build/Build/Intermediates.noindex/WorkbenchReplacement.build/Debug/WorkbenchReplacement.build/Objects-normal/arm64/MainView.swiftdeps
index 35b3385..be3b94d 100644
Binary files a/build/Build/Intermediates.noindex/WorkbenchReplacement.build/Debug/WorkbenchReplacement.build/Objects-normal/arm64/MainView.swiftdeps and b/build/Build/Intermediates.noindex/WorkbenchReplacement.build/Debug/WorkbenchReplacement.build/Objects-normal/arm64/MainView.swiftdeps differ
diff --git a/build/Build/Intermediates.noindex/WorkbenchReplacement.build/Debug/WorkbenchReplacement.build/Objects-normal/arm64/WorkbenchReplacement-primary.priors b/build/Build/Intermediates.noindex/WorkbenchReplacement.build/Debug/WorkbenchReplacement.build/Objects-normal/arm64/WorkbenchReplacement-primary.priors
index 41a45cd..b68590b 100644
Binary files a/build/Build/Intermediates.noindex/WorkbenchReplacement.build/Debug/WorkbenchReplacement.build/Objects-normal/arm64/WorkbenchReplacement-primary.priors and b/build/Build/Intermediates.noindex/WorkbenchReplacement.build/Debug/WorkbenchReplacement.build/Objects-normal/arm64/WorkbenchReplacement-primary.priors differ
diff --git a/build/Build/Intermediates.noindex/WorkbenchReplacement.build/Debug/WorkbenchReplacement.build/Objects-normal/arm64/WorkbenchReplacement.swiftmodule b/build/Build/Intermediates.noindex/WorkbenchReplacement.build/Debug/WorkbenchReplacement.build/Objects-normal/arm64/WorkbenchReplacement.swiftmodule
index 4c75fe1..3718105 100644
Binary files a/build/Build/Intermediates.noindex/WorkbenchReplacement.build/Debug/WorkbenchReplacement.build/Objects-normal/arm64/WorkbenchReplacement.swiftmodule and b/build/Build/Intermediates.noindex/WorkbenchReplacement.build/Debug/WorkbenchReplacement.build/Objects-normal/arm64/WorkbenchReplacement.swiftmodule differ
diff --git a/build/Build/Intermediates.noindex/WorkbenchReplacement.build/Debug/WorkbenchReplacement.build/Objects-normal/arm64/WorkbenchReplacement.swiftsourceinfo b/build/Build/Intermediates.noindex/WorkbenchReplacement.build/Debug/WorkbenchReplacement.build/Objects-normal/arm64/WorkbenchReplacement.swiftsourceinfo
index d840a8f..7dd48c4 100644
Binary files a/build/Build/Intermediates.noindex/WorkbenchReplacement.build/Debug/WorkbenchReplacement.build/Objects-normal/arm64/WorkbenchReplacement.swiftsourceinfo and b/build/Build/Intermediates.noindex/WorkbenchReplacement.build/Debug/WorkbenchReplacement.build/Objects-normal/arm64/WorkbenchReplacement.swiftsourceinfo differ
diff --git a/build/Build/Intermediates.noindex/XCBuildData/69f67b13647b23476e4a199d44d0239c.xcbuilddata/attachments/15726fb86eddf5eb007ad1da55571215 b/build/Build/Intermediates.noindex/XCBuildData/2435d0868a8768668f46b049370ba588.xcbuilddata/attachments/15726fb86eddf5eb007ad1da55571215
similarity index 100%
rename from build/Build/Intermediates.noindex/XCBuildData/69f67b13647b23476e4a199d44d0239c.xcbuilddata/attachments/15726fb86eddf5eb007ad1da55571215
rename to build/Build/Intermediates.noindex/XCBuildData/2435d0868a8768668f46b049370ba588.xcbuilddata/attachments/15726fb86eddf5eb007ad1da55571215
diff --git a/build/Build/Intermediates.noindex/XCBuildData/69f67b13647b23476e4a199d44d0239c.xcbuilddata/attachments/2de6a04cdba79ed13580c47dfd70cc5f b/build/Build/Intermediates.noindex/XCBuildData/2435d0868a8768668f46b049370ba588.xcbuilddata/attachments/2de6a04cdba79ed13580c47dfd70cc5f
similarity index 100%
rename from build/Build/Intermediates.noindex/XCBuildData/69f67b13647b23476e4a199d44d0239c.xcbuilddata/attachments/2de6a04cdba79ed13580c47dfd70cc5f
rename to build/Build/Intermediates.noindex/XCBuildData/2435d0868a8768668f46b049370ba588.xcbuilddata/attachments/2de6a04cdba79ed13580c47dfd70cc5f
diff --git a/build/Build/Intermediates.noindex/XCBuildData/843261e86582a13fd83d1289100d0b6b.xcbuilddata/attachments/d670dbc41da7bcccf06990b0bd105269 b/build/Build/Intermediates.noindex/XCBuildData/2435d0868a8768668f46b049370ba588.xcbuilddata/attachments/38154eb18ed76721f21d9a4726168a90
similarity index 99%
rename from build/Build/Intermediates.noindex/XCBuildData/843261e86582a13fd83d1289100d0b6b.xcbuilddata/attachments/d670dbc41da7bcccf06990b0bd105269
rename to build/Build/Intermediates.noindex/XCBuildData/2435d0868a8768668f46b049370ba588.xcbuilddata/attachments/38154eb18ed76721f21d9a4726168a90
index b202b52..1ff3662 100644
Binary files a/build/Build/Intermediates.noindex/XCBuildData/843261e86582a13fd83d1289100d0b6b.xcbuilddata/attachments/d670dbc41da7bcccf06990b0bd105269 and b/build/Build/Intermediates.noindex/XCBuildData/2435d0868a8768668f46b049370ba588.xcbuilddata/attachments/38154eb18ed76721f21d9a4726168a90 differ
diff --git a/build/Build/Intermediates.noindex/XCBuildData/69f67b13647b23476e4a199d44d0239c.xcbuilddata/attachments/4f5c2426dceb78351a3ede0458739fcc b/build/Build/Intermediates.noindex/XCBuildData/2435d0868a8768668f46b049370ba588.xcbuilddata/attachments/4f5c2426dceb78351a3ede0458739fcc
similarity index 100%
rename from build/Build/Intermediates.noindex/XCBuildData/69f67b13647b23476e4a199d44d0239c.xcbuilddata/attachments/4f5c2426dceb78351a3ede0458739fcc
rename to build/Build/Intermediates.noindex/XCBuildData/2435d0868a8768668f46b049370ba588.xcbuilddata/attachments/4f5c2426dceb78351a3ede0458739fcc
diff --git a/build/Build/Intermediates.noindex/XCBuildData/69f67b13647b23476e4a199d44d0239c.xcbuilddata/attachments/8bba4233626f64a7ea772bb94a08a1a9 b/build/Build/Intermediates.noindex/XCBuildData/2435d0868a8768668f46b049370ba588.xcbuilddata/attachments/8bba4233626f64a7ea772bb94a08a1a9
similarity index 100%
rename from build/Build/Intermediates.noindex/XCBuildData/69f67b13647b23476e4a199d44d0239c.xcbuilddata/attachments/8bba4233626f64a7ea772bb94a08a1a9
rename to build/Build/Intermediates.noindex/XCBuildData/2435d0868a8768668f46b049370ba588.xcbuilddata/attachments/8bba4233626f64a7ea772bb94a08a1a9
diff --git a/build/Build/Intermediates.noindex/XCBuildData/69f67b13647b23476e4a199d44d0239c.xcbuilddata/attachments/9a2910799639cff85464f3dcb75559c1 b/build/Build/Intermediates.noindex/XCBuildData/2435d0868a8768668f46b049370ba588.xcbuilddata/attachments/9a2910799639cff85464f3dcb75559c1
similarity index 100%
rename from build/Build/Intermediates.noindex/XCBuildData/69f67b13647b23476e4a199d44d0239c.xcbuilddata/attachments/9a2910799639cff85464f3dcb75559c1
rename to build/Build/Intermediates.noindex/XCBuildData/2435d0868a8768668f46b049370ba588.xcbuilddata/attachments/9a2910799639cff85464f3dcb75559c1
diff --git a/build/Build/Intermediates.noindex/XCBuildData/69f67b13647b23476e4a199d44d0239c.xcbuilddata/attachments/b47c260a05c70c9e20ee4b0d1fc4e6ed b/build/Build/Intermediates.noindex/XCBuildData/2435d0868a8768668f46b049370ba588.xcbuilddata/attachments/b47c260a05c70c9e20ee4b0d1fc4e6ed
similarity index 100%
rename from build/Build/Intermediates.noindex/XCBuildData/69f67b13647b23476e4a199d44d0239c.xcbuilddata/attachments/b47c260a05c70c9e20ee4b0d1fc4e6ed
rename to build/Build/Intermediates.noindex/XCBuildData/2435d0868a8768668f46b049370ba588.xcbuilddata/attachments/b47c260a05c70c9e20ee4b0d1fc4e6ed
diff --git a/build/Build/Intermediates.noindex/XCBuildData/69f67b13647b23476e4a199d44d0239c.xcbuilddata/attachments/b6a63299643304380633b19a42b96379 b/build/Build/Intermediates.noindex/XCBuildData/2435d0868a8768668f46b049370ba588.xcbuilddata/attachments/b6a63299643304380633b19a42b96379
similarity index 100%
rename from build/Build/Intermediates.noindex/XCBuildData/69f67b13647b23476e4a199d44d0239c.xcbuilddata/attachments/b6a63299643304380633b19a42b96379
rename to build/Build/Intermediates.noindex/XCBuildData/2435d0868a8768668f46b049370ba588.xcbuilddata/attachments/b6a63299643304380633b19a42b96379
diff --git a/build/Build/Intermediates.noindex/XCBuildData/69f67b13647b23476e4a199d44d0239c.xcbuilddata/attachments/d41d8cd98f00b204e9800998ecf8427e b/build/Build/Intermediates.noindex/XCBuildData/2435d0868a8768668f46b049370ba588.xcbuilddata/attachments/d41d8cd98f00b204e9800998ecf8427e
similarity index 100%
rename from build/Build/Intermediates.noindex/XCBuildData/69f67b13647b23476e4a199d44d0239c.xcbuilddata/attachments/d41d8cd98f00b204e9800998ecf8427e
rename to build/Build/Intermediates.noindex/XCBuildData/2435d0868a8768668f46b049370ba588.xcbuilddata/attachments/d41d8cd98f00b204e9800998ecf8427e
diff --git a/build/Build/Intermediates.noindex/XCBuildData/69f67b13647b23476e4a199d44d0239c.xcbuilddata/attachments/d88acc6e91fc370129743fcdc3e34ab1 b/build/Build/Intermediates.noindex/XCBuildData/2435d0868a8768668f46b049370ba588.xcbuilddata/attachments/d88acc6e91fc370129743fcdc3e34ab1
similarity index 100%
rename from build/Build/Intermediates.noindex/XCBuildData/69f67b13647b23476e4a199d44d0239c.xcbuilddata/attachments/d88acc6e91fc370129743fcdc3e34ab1
rename to build/Build/Intermediates.noindex/XCBuildData/2435d0868a8768668f46b049370ba588.xcbuilddata/attachments/d88acc6e91fc370129743fcdc3e34ab1
diff --git a/build/Build/Intermediates.noindex/XCBuildData/69f67b13647b23476e4a199d44d0239c.xcbuilddata/attachments/dccfce55fa07ed9ec6f5920d7bed9910 b/build/Build/Intermediates.noindex/XCBuildData/2435d0868a8768668f46b049370ba588.xcbuilddata/attachments/dccfce55fa07ed9ec6f5920d7bed9910
similarity index 100%
rename from build/Build/Intermediates.noindex/XCBuildData/69f67b13647b23476e4a199d44d0239c.xcbuilddata/attachments/dccfce55fa07ed9ec6f5920d7bed9910
rename to build/Build/Intermediates.noindex/XCBuildData/2435d0868a8768668f46b049370ba588.xcbuilddata/attachments/dccfce55fa07ed9ec6f5920d7bed9910
diff --git a/build/Build/Intermediates.noindex/XCBuildData/69f67b13647b23476e4a199d44d0239c.xcbuilddata/attachments/f86cbb10517a3bd51f252887359b04af b/build/Build/Intermediates.noindex/XCBuildData/2435d0868a8768668f46b049370ba588.xcbuilddata/attachments/f86cbb10517a3bd51f252887359b04af
similarity index 100%
rename from build/Build/Intermediates.noindex/XCBuildData/69f67b13647b23476e4a199d44d0239c.xcbuilddata/attachments/f86cbb10517a3bd51f252887359b04af
rename to build/Build/Intermediates.noindex/XCBuildData/2435d0868a8768668f46b049370ba588.xcbuilddata/attachments/f86cbb10517a3bd51f252887359b04af
diff --git a/build/Build/Intermediates.noindex/XCBuildData/69f67b13647b23476e4a199d44d0239c.xcbuilddata/attachments/fd8314defc70a8778956f026c0ddfd19 b/build/Build/Intermediates.noindex/XCBuildData/2435d0868a8768668f46b049370ba588.xcbuilddata/attachments/fd8314defc70a8778956f026c0ddfd19
similarity index 100%
rename from build/Build/Intermediates.noindex/XCBuildData/69f67b13647b23476e4a199d44d0239c.xcbuilddata/attachments/fd8314defc70a8778956f026c0ddfd19
rename to build/Build/Intermediates.noindex/XCBuildData/2435d0868a8768668f46b049370ba588.xcbuilddata/attachments/fd8314defc70a8778956f026c0ddfd19
diff --git a/build/Build/Intermediates.noindex/XCBuildData/843261e86582a13fd83d1289100d0b6b.xcbuilddata/build-request.json b/build/Build/Intermediates.noindex/XCBuildData/2435d0868a8768668f46b049370ba588.xcbuilddata/build-request.json
similarity index 100%
rename from build/Build/Intermediates.noindex/XCBuildData/843261e86582a13fd83d1289100d0b6b.xcbuilddata/build-request.json
rename to build/Build/Intermediates.noindex/XCBuildData/2435d0868a8768668f46b049370ba588.xcbuilddata/build-request.json
diff --git a/build/Build/Intermediates.noindex/XCBuildData/843261e86582a13fd83d1289100d0b6b.xcbuilddata/description.msgpack b/build/Build/Intermediates.noindex/XCBuildData/2435d0868a8768668f46b049370ba588.xcbuilddata/description.msgpack
similarity index 99%
rename from build/Build/Intermediates.noindex/XCBuildData/843261e86582a13fd83d1289100d0b6b.xcbuilddata/description.msgpack
rename to build/Build/Intermediates.noindex/XCBuildData/2435d0868a8768668f46b049370ba588.xcbuilddata/description.msgpack
index cf34665..7f396cd 100644
Binary files a/build/Build/Intermediates.noindex/XCBuildData/843261e86582a13fd83d1289100d0b6b.xcbuilddata/description.msgpack and b/build/Build/Intermediates.noindex/XCBuildData/2435d0868a8768668f46b049370ba588.xcbuilddata/description.msgpack differ
diff --git a/build/Build/Intermediates.noindex/XCBuildData/69f67b13647b23476e4a199d44d0239c.xcbuilddata/manifest.json b/build/Build/Intermediates.noindex/XCBuildData/2435d0868a8768668f46b049370ba588.xcbuilddata/manifest.json
similarity index 100%
rename from build/Build/Intermediates.noindex/XCBuildData/69f67b13647b23476e4a199d44d0239c.xcbuilddata/manifest.json
rename to build/Build/Intermediates.noindex/XCBuildData/2435d0868a8768668f46b049370ba588.xcbuilddata/manifest.json
diff --git a/build/Build/Intermediates.noindex/XCBuildData/69f67b13647b23476e4a199d44d0239c.xcbuilddata/target-graph.txt b/build/Build/Intermediates.noindex/XCBuildData/2435d0868a8768668f46b049370ba588.xcbuilddata/target-graph.txt
similarity index 100%
rename from build/Build/Intermediates.noindex/XCBuildData/69f67b13647b23476e4a199d44d0239c.xcbuilddata/target-graph.txt
rename to build/Build/Intermediates.noindex/XCBuildData/2435d0868a8768668f46b049370ba588.xcbuilddata/target-graph.txt
diff --git a/build/Build/Intermediates.noindex/XCBuildData/2435d0868a8768668f46b049370ba588.xcbuilddata/task-store.msgpack b/build/Build/Intermediates.noindex/XCBuildData/2435d0868a8768668f46b049370ba588.xcbuilddata/task-store.msgpack
new file mode 100644
index 0000000..2206931
Binary files /dev/null and b/build/Build/Intermediates.noindex/XCBuildData/2435d0868a8768668f46b049370ba588.xcbuilddata/task-store.msgpack differ
diff --git a/build/Build/Intermediates.noindex/XCBuildData/69f67b13647b23476e4a199d44d0239c.xcbuilddata/attachments/756a932d82ce86043544a83aeeb1de78 b/build/Build/Intermediates.noindex/XCBuildData/69f67b13647b23476e4a199d44d0239c.xcbuilddata/attachments/756a932d82ce86043544a83aeeb1de78
deleted file mode 100644
index 6aed236..0000000
Binary files a/build/Build/Intermediates.noindex/XCBuildData/69f67b13647b23476e4a199d44d0239c.xcbuilddata/attachments/756a932d82ce86043544a83aeeb1de78 and /dev/null differ
diff --git a/build/Build/Intermediates.noindex/XCBuildData/69f67b13647b23476e4a199d44d0239c.xcbuilddata/task-store.msgpack b/build/Build/Intermediates.noindex/XCBuildData/69f67b13647b23476e4a199d44d0239c.xcbuilddata/task-store.msgpack
deleted file mode 100644
index 64c0c37..0000000
Binary files a/build/Build/Intermediates.noindex/XCBuildData/69f67b13647b23476e4a199d44d0239c.xcbuilddata/task-store.msgpack and /dev/null differ
diff --git a/build/Build/Intermediates.noindex/XCBuildData/843261e86582a13fd83d1289100d0b6b.xcbuilddata/task-store.msgpack b/build/Build/Intermediates.noindex/XCBuildData/843261e86582a13fd83d1289100d0b6b.xcbuilddata/task-store.msgpack
deleted file mode 100644
index df604ee..0000000
Binary files a/build/Build/Intermediates.noindex/XCBuildData/843261e86582a13fd83d1289100d0b6b.xcbuilddata/task-store.msgpack and /dev/null differ
diff --git a/build/Build/Intermediates.noindex/XCBuildData/843261e86582a13fd83d1289100d0b6b.xcbuilddata/attachments/15726fb86eddf5eb007ad1da55571215 b/build/Build/Intermediates.noindex/XCBuildData/a246cfa007f5f0bb067024e4c1eac266.xcbuilddata/attachments/15726fb86eddf5eb007ad1da55571215
similarity index 100%
rename from build/Build/Intermediates.noindex/XCBuildData/843261e86582a13fd83d1289100d0b6b.xcbuilddata/attachments/15726fb86eddf5eb007ad1da55571215
rename to build/Build/Intermediates.noindex/XCBuildData/a246cfa007f5f0bb067024e4c1eac266.xcbuilddata/attachments/15726fb86eddf5eb007ad1da55571215
diff --git a/build/Build/Intermediates.noindex/XCBuildData/843261e86582a13fd83d1289100d0b6b.xcbuilddata/attachments/2de6a04cdba79ed13580c47dfd70cc5f b/build/Build/Intermediates.noindex/XCBuildData/a246cfa007f5f0bb067024e4c1eac266.xcbuilddata/attachments/2de6a04cdba79ed13580c47dfd70cc5f
similarity index 100%
rename from build/Build/Intermediates.noindex/XCBuildData/843261e86582a13fd83d1289100d0b6b.xcbuilddata/attachments/2de6a04cdba79ed13580c47dfd70cc5f
rename to build/Build/Intermediates.noindex/XCBuildData/a246cfa007f5f0bb067024e4c1eac266.xcbuilddata/attachments/2de6a04cdba79ed13580c47dfd70cc5f
diff --git a/build/Build/Intermediates.noindex/XCBuildData/843261e86582a13fd83d1289100d0b6b.xcbuilddata/attachments/4f5c2426dceb78351a3ede0458739fcc b/build/Build/Intermediates.noindex/XCBuildData/a246cfa007f5f0bb067024e4c1eac266.xcbuilddata/attachments/4f5c2426dceb78351a3ede0458739fcc
similarity index 100%
rename from build/Build/Intermediates.noindex/XCBuildData/843261e86582a13fd83d1289100d0b6b.xcbuilddata/attachments/4f5c2426dceb78351a3ede0458739fcc
rename to build/Build/Intermediates.noindex/XCBuildData/a246cfa007f5f0bb067024e4c1eac266.xcbuilddata/attachments/4f5c2426dceb78351a3ede0458739fcc
diff --git a/build/Build/Intermediates.noindex/XCBuildData/a246cfa007f5f0bb067024e4c1eac266.xcbuilddata/attachments/844dd827368ca9dedb2b19baf3d6c714 b/build/Build/Intermediates.noindex/XCBuildData/a246cfa007f5f0bb067024e4c1eac266.xcbuilddata/attachments/844dd827368ca9dedb2b19baf3d6c714
new file mode 100644
index 0000000..34f8a64
Binary files /dev/null and b/build/Build/Intermediates.noindex/XCBuildData/a246cfa007f5f0bb067024e4c1eac266.xcbuilddata/attachments/844dd827368ca9dedb2b19baf3d6c714 differ
diff --git a/build/Build/Intermediates.noindex/XCBuildData/843261e86582a13fd83d1289100d0b6b.xcbuilddata/attachments/8bba4233626f64a7ea772bb94a08a1a9 b/build/Build/Intermediates.noindex/XCBuildData/a246cfa007f5f0bb067024e4c1eac266.xcbuilddata/attachments/8bba4233626f64a7ea772bb94a08a1a9
similarity index 100%
rename from build/Build/Intermediates.noindex/XCBuildData/843261e86582a13fd83d1289100d0b6b.xcbuilddata/attachments/8bba4233626f64a7ea772bb94a08a1a9
rename to build/Build/Intermediates.noindex/XCBuildData/a246cfa007f5f0bb067024e4c1eac266.xcbuilddata/attachments/8bba4233626f64a7ea772bb94a08a1a9
diff --git a/build/Build/Intermediates.noindex/XCBuildData/843261e86582a13fd83d1289100d0b6b.xcbuilddata/attachments/9a2910799639cff85464f3dcb75559c1 b/build/Build/Intermediates.noindex/XCBuildData/a246cfa007f5f0bb067024e4c1eac266.xcbuilddata/attachments/9a2910799639cff85464f3dcb75559c1
similarity index 100%
rename from build/Build/Intermediates.noindex/XCBuildData/843261e86582a13fd83d1289100d0b6b.xcbuilddata/attachments/9a2910799639cff85464f3dcb75559c1
rename to build/Build/Intermediates.noindex/XCBuildData/a246cfa007f5f0bb067024e4c1eac266.xcbuilddata/attachments/9a2910799639cff85464f3dcb75559c1
diff --git a/build/Build/Intermediates.noindex/XCBuildData/843261e86582a13fd83d1289100d0b6b.xcbuilddata/attachments/b47c260a05c70c9e20ee4b0d1fc4e6ed b/build/Build/Intermediates.noindex/XCBuildData/a246cfa007f5f0bb067024e4c1eac266.xcbuilddata/attachments/b47c260a05c70c9e20ee4b0d1fc4e6ed
similarity index 100%
rename from build/Build/Intermediates.noindex/XCBuildData/843261e86582a13fd83d1289100d0b6b.xcbuilddata/attachments/b47c260a05c70c9e20ee4b0d1fc4e6ed
rename to build/Build/Intermediates.noindex/XCBuildData/a246cfa007f5f0bb067024e4c1eac266.xcbuilddata/attachments/b47c260a05c70c9e20ee4b0d1fc4e6ed
diff --git a/build/Build/Intermediates.noindex/XCBuildData/843261e86582a13fd83d1289100d0b6b.xcbuilddata/attachments/b6a63299643304380633b19a42b96379 b/build/Build/Intermediates.noindex/XCBuildData/a246cfa007f5f0bb067024e4c1eac266.xcbuilddata/attachments/b6a63299643304380633b19a42b96379
similarity index 100%
rename from build/Build/Intermediates.noindex/XCBuildData/843261e86582a13fd83d1289100d0b6b.xcbuilddata/attachments/b6a63299643304380633b19a42b96379
rename to build/Build/Intermediates.noindex/XCBuildData/a246cfa007f5f0bb067024e4c1eac266.xcbuilddata/attachments/b6a63299643304380633b19a42b96379
diff --git a/build/Build/Intermediates.noindex/XCBuildData/843261e86582a13fd83d1289100d0b6b.xcbuilddata/attachments/d41d8cd98f00b204e9800998ecf8427e b/build/Build/Intermediates.noindex/XCBuildData/a246cfa007f5f0bb067024e4c1eac266.xcbuilddata/attachments/d41d8cd98f00b204e9800998ecf8427e
similarity index 100%
rename from build/Build/Intermediates.noindex/XCBuildData/843261e86582a13fd83d1289100d0b6b.xcbuilddata/attachments/d41d8cd98f00b204e9800998ecf8427e
rename to build/Build/Intermediates.noindex/XCBuildData/a246cfa007f5f0bb067024e4c1eac266.xcbuilddata/attachments/d41d8cd98f00b204e9800998ecf8427e
diff --git a/build/Build/Intermediates.noindex/XCBuildData/843261e86582a13fd83d1289100d0b6b.xcbuilddata/attachments/d88acc6e91fc370129743fcdc3e34ab1 b/build/Build/Intermediates.noindex/XCBuildData/a246cfa007f5f0bb067024e4c1eac266.xcbuilddata/attachments/d88acc6e91fc370129743fcdc3e34ab1
similarity index 100%
rename from build/Build/Intermediates.noindex/XCBuildData/843261e86582a13fd83d1289100d0b6b.xcbuilddata/attachments/d88acc6e91fc370129743fcdc3e34ab1
rename to build/Build/Intermediates.noindex/XCBuildData/a246cfa007f5f0bb067024e4c1eac266.xcbuilddata/attachments/d88acc6e91fc370129743fcdc3e34ab1
diff --git a/build/Build/Intermediates.noindex/XCBuildData/843261e86582a13fd83d1289100d0b6b.xcbuilddata/attachments/dccfce55fa07ed9ec6f5920d7bed9910 b/build/Build/Intermediates.noindex/XCBuildData/a246cfa007f5f0bb067024e4c1eac266.xcbuilddata/attachments/dccfce55fa07ed9ec6f5920d7bed9910
similarity index 100%
rename from build/Build/Intermediates.noindex/XCBuildData/843261e86582a13fd83d1289100d0b6b.xcbuilddata/attachments/dccfce55fa07ed9ec6f5920d7bed9910
rename to build/Build/Intermediates.noindex/XCBuildData/a246cfa007f5f0bb067024e4c1eac266.xcbuilddata/attachments/dccfce55fa07ed9ec6f5920d7bed9910
diff --git a/build/Build/Intermediates.noindex/XCBuildData/843261e86582a13fd83d1289100d0b6b.xcbuilddata/attachments/f86cbb10517a3bd51f252887359b04af b/build/Build/Intermediates.noindex/XCBuildData/a246cfa007f5f0bb067024e4c1eac266.xcbuilddata/attachments/f86cbb10517a3bd51f252887359b04af
similarity index 100%
rename from build/Build/Intermediates.noindex/XCBuildData/843261e86582a13fd83d1289100d0b6b.xcbuilddata/attachments/f86cbb10517a3bd51f252887359b04af
rename to build/Build/Intermediates.noindex/XCBuildData/a246cfa007f5f0bb067024e4c1eac266.xcbuilddata/attachments/f86cbb10517a3bd51f252887359b04af
diff --git a/build/Build/Intermediates.noindex/XCBuildData/843261e86582a13fd83d1289100d0b6b.xcbuilddata/attachments/fd8314defc70a8778956f026c0ddfd19 b/build/Build/Intermediates.noindex/XCBuildData/a246cfa007f5f0bb067024e4c1eac266.xcbuilddata/attachments/fd8314defc70a8778956f026c0ddfd19
similarity index 100%
rename from build/Build/Intermediates.noindex/XCBuildData/843261e86582a13fd83d1289100d0b6b.xcbuilddata/attachments/fd8314defc70a8778956f026c0ddfd19
rename to build/Build/Intermediates.noindex/XCBuildData/a246cfa007f5f0bb067024e4c1eac266.xcbuilddata/attachments/fd8314defc70a8778956f026c0ddfd19
diff --git a/build/Build/Intermediates.noindex/XCBuildData/a246cfa007f5f0bb067024e4c1eac266.xcbuilddata/build-request.json b/build/Build/Intermediates.noindex/XCBuildData/a246cfa007f5f0bb067024e4c1eac266.xcbuilddata/build-request.json
new file mode 100644
index 0000000..2f983be
--- /dev/null
+++ b/build/Build/Intermediates.noindex/XCBuildData/a246cfa007f5f0bb067024e4c1eac266.xcbuilddata/build-request.json
@@ -0,0 +1,66 @@
+{
+ "buildCommand" : {
+ "command" : "build",
+ "skipDependencies" : false,
+ "style" : "buildOnly"
+ },
+ "configuredTargets" : [
+ {
+ "guid" : "34f2007b1d0798da0712fb82baec90fb2878cdfbaa5fcb5fa30c8485a83dfb14"
+ }
+ ],
+ "containerPath" : "/Volumes/Developer/Applications/WorkbenchReplacement/WorkbenchReplacement.xcodeproj",
+ "continueBuildingAfterErrors" : false,
+ "dependencyScope" : "workspace",
+ "enableIndexBuildArena" : false,
+ "hideShellScriptEnvironment" : false,
+ "parameters" : {
+ "action" : "build",
+ "activeArchitecture" : "arm64",
+ "activeRunDestination" : {
+ "disableOnlyActiveArch" : false,
+ "hostTargetedPlatform" : null,
+ "platform" : "macosx",
+ "sdk" : "macosx27.0",
+ "sdkVariant" : "macos",
+ "supportedArchitectures" : [
+ "arm64e",
+ "arm64",
+ "x86_64"
+ ],
+ "targetArchitecture" : "arm64"
+ },
+ "arenaInfo" : {
+ "buildIntermediatesPath" : "/Volumes/Developer/Applications/WorkbenchReplacement/build/Build/Intermediates.noindex",
+ "buildProductsPath" : "/Volumes/Developer/Applications/WorkbenchReplacement/build/Build/Products",
+ "derivedDataPath" : "/Volumes/Developer/Applications/WorkbenchReplacement/build",
+ "indexDataStoreFolderPath" : "/Volumes/Developer/Applications/WorkbenchReplacement/build/Index.noindex/DataStore",
+ "indexEnableDataStore" : true,
+ "indexPCHPath" : "/Volumes/Developer/Applications/WorkbenchReplacement/build/Index.noindex/PrecompiledHeaders",
+ "pchPath" : "/Volumes/Developer/Applications/WorkbenchReplacement/build/Build/Intermediates.noindex/PrecompiledHeaders"
+ },
+ "configurationName" : "Debug",
+ "overrides" : {
+ "commandLine" : {
+ "table" : {
+
+ }
+ },
+ "synthesized" : {
+ "table" : {
+ "ACTION" : "build",
+ "ENABLE_PREVIEWS" : "NO",
+ "ENABLE_XOJIT_PREVIEWS" : "YES",
+ "ONLY_ACTIVE_ARCH" : "YES",
+ "TOOLCHAINS" : "com.apple.dt.toolchain.Metal.32023.921.5 $(inherited)"
+ }
+ }
+ }
+ },
+ "schemeCommand" : "launch",
+ "showNonLoggedProgress" : true,
+ "useDryRun" : false,
+ "useImplicitDependencies" : true,
+ "useLegacyBuildLocations" : false,
+ "useParallelTargets" : true
+}
\ No newline at end of file
diff --git a/build/Build/Intermediates.noindex/XCBuildData/69f67b13647b23476e4a199d44d0239c.xcbuilddata/description.msgpack b/build/Build/Intermediates.noindex/XCBuildData/a246cfa007f5f0bb067024e4c1eac266.xcbuilddata/description.msgpack
similarity index 99%
rename from build/Build/Intermediates.noindex/XCBuildData/69f67b13647b23476e4a199d44d0239c.xcbuilddata/description.msgpack
rename to build/Build/Intermediates.noindex/XCBuildData/a246cfa007f5f0bb067024e4c1eac266.xcbuilddata/description.msgpack
index edd352a..32ec59a 100644
Binary files a/build/Build/Intermediates.noindex/XCBuildData/69f67b13647b23476e4a199d44d0239c.xcbuilddata/description.msgpack and b/build/Build/Intermediates.noindex/XCBuildData/a246cfa007f5f0bb067024e4c1eac266.xcbuilddata/description.msgpack differ
diff --git a/build/Build/Intermediates.noindex/XCBuildData/843261e86582a13fd83d1289100d0b6b.xcbuilddata/manifest.json b/build/Build/Intermediates.noindex/XCBuildData/a246cfa007f5f0bb067024e4c1eac266.xcbuilddata/manifest.json
similarity index 100%
rename from build/Build/Intermediates.noindex/XCBuildData/843261e86582a13fd83d1289100d0b6b.xcbuilddata/manifest.json
rename to build/Build/Intermediates.noindex/XCBuildData/a246cfa007f5f0bb067024e4c1eac266.xcbuilddata/manifest.json
diff --git a/build/Build/Intermediates.noindex/XCBuildData/843261e86582a13fd83d1289100d0b6b.xcbuilddata/target-graph.txt b/build/Build/Intermediates.noindex/XCBuildData/a246cfa007f5f0bb067024e4c1eac266.xcbuilddata/target-graph.txt
similarity index 100%
rename from build/Build/Intermediates.noindex/XCBuildData/843261e86582a13fd83d1289100d0b6b.xcbuilddata/target-graph.txt
rename to build/Build/Intermediates.noindex/XCBuildData/a246cfa007f5f0bb067024e4c1eac266.xcbuilddata/target-graph.txt
diff --git a/build/Build/Intermediates.noindex/XCBuildData/a246cfa007f5f0bb067024e4c1eac266.xcbuilddata/task-store.msgpack b/build/Build/Intermediates.noindex/XCBuildData/a246cfa007f5f0bb067024e4c1eac266.xcbuilddata/task-store.msgpack
new file mode 100644
index 0000000..a8ab76a
Binary files /dev/null and b/build/Build/Intermediates.noindex/XCBuildData/a246cfa007f5f0bb067024e4c1eac266.xcbuilddata/task-store.msgpack differ
diff --git a/build/Build/Intermediates.noindex/XCBuildData/b4f9670003ddbc00bd0e34134fd05c3c.xcbuilddata/attachments/15726fb86eddf5eb007ad1da55571215 b/build/Build/Intermediates.noindex/XCBuildData/b4f9670003ddbc00bd0e34134fd05c3c.xcbuilddata/attachments/15726fb86eddf5eb007ad1da55571215
new file mode 100644
index 0000000..b41c2b4
--- /dev/null
+++ b/build/Build/Intermediates.noindex/XCBuildData/b4f9670003ddbc00bd0e34134fd05c3c.xcbuilddata/attachments/15726fb86eddf5eb007ad1da55571215
@@ -0,0 +1 @@
+@rpath/WorkbenchReplacement.debug.dylib
\ No newline at end of file
diff --git a/build/Build/Intermediates.noindex/XCBuildData/b4f9670003ddbc00bd0e34134fd05c3c.xcbuilddata/attachments/2de6a04cdba79ed13580c47dfd70cc5f b/build/Build/Intermediates.noindex/XCBuildData/b4f9670003ddbc00bd0e34134fd05c3c.xcbuilddata/attachments/2de6a04cdba79ed13580c47dfd70cc5f
new file mode 100644
index 0000000..0c67376
--- /dev/null
+++ b/build/Build/Intermediates.noindex/XCBuildData/b4f9670003ddbc00bd0e34134fd05c3c.xcbuilddata/attachments/2de6a04cdba79ed13580c47dfd70cc5f
@@ -0,0 +1,5 @@
+
+
+
+
+
diff --git a/build/Build/Intermediates.noindex/XCBuildData/b4f9670003ddbc00bd0e34134fd05c3c.xcbuilddata/attachments/4f5c2426dceb78351a3ede0458739fcc b/build/Build/Intermediates.noindex/XCBuildData/b4f9670003ddbc00bd0e34134fd05c3c.xcbuilddata/attachments/4f5c2426dceb78351a3ede0458739fcc
new file mode 100644
index 0000000..38c3fe3
--- /dev/null
+++ b/build/Build/Intermediates.noindex/XCBuildData/b4f9670003ddbc00bd0e34134fd05c3c.xcbuilddata/attachments/4f5c2426dceb78351a3ede0458739fcc
@@ -0,0 +1,49 @@
+{
+ "" : {
+ "diagnostics" : "/Volumes/Developer/Applications/WorkbenchReplacement/build/Build/Intermediates.noindex/WorkbenchReplacement.build/Debug/WorkbenchReplacement.build/Objects-normal/arm64/WorkbenchReplacement-primary.dia",
+ "emit-module-dependencies" : "/Volumes/Developer/Applications/WorkbenchReplacement/build/Build/Intermediates.noindex/WorkbenchReplacement.build/Debug/WorkbenchReplacement.build/Objects-normal/arm64/WorkbenchReplacement-primary-emit-module.d",
+ "emit-module-diagnostics" : "/Volumes/Developer/Applications/WorkbenchReplacement/build/Build/Intermediates.noindex/WorkbenchReplacement.build/Debug/WorkbenchReplacement.build/Objects-normal/arm64/WorkbenchReplacement-primary-emit-module.dia",
+ "pch" : "/Volumes/Developer/Applications/WorkbenchReplacement/build/Build/Intermediates.noindex/WorkbenchReplacement.build/Debug/WorkbenchReplacement.build/Objects-normal/arm64/WorkbenchReplacement-primary-Bridging-header.pch",
+ "swift-dependencies" : "/Volumes/Developer/Applications/WorkbenchReplacement/build/Build/Intermediates.noindex/WorkbenchReplacement.build/Debug/WorkbenchReplacement.build/Objects-normal/arm64/WorkbenchReplacement-primary.swiftdeps"
+ },
+ "/Volumes/Developer/Applications/WorkbenchReplacement/WorkbenchReplacement/App.swift" : {
+ "const-values" : "/Volumes/Developer/Applications/WorkbenchReplacement/build/Build/Intermediates.noindex/WorkbenchReplacement.build/Debug/WorkbenchReplacement.build/Objects-normal/arm64/App.swiftconstvalues",
+ "dependencies" : "/Volumes/Developer/Applications/WorkbenchReplacement/build/Build/Intermediates.noindex/WorkbenchReplacement.build/Debug/WorkbenchReplacement.build/Objects-normal/arm64/App.d",
+ "diagnostics" : "/Volumes/Developer/Applications/WorkbenchReplacement/build/Build/Intermediates.noindex/WorkbenchReplacement.build/Debug/WorkbenchReplacement.build/Objects-normal/arm64/App.dia",
+ "index-unit-output-path" : "/WorkbenchReplacement.build/Debug/WorkbenchReplacement.build/Objects-normal/arm64/App.o",
+ "llvm-bc" : "/Volumes/Developer/Applications/WorkbenchReplacement/build/Build/Intermediates.noindex/WorkbenchReplacement.build/Debug/WorkbenchReplacement.build/Objects-normal/arm64/App.bc",
+ "object" : "/Volumes/Developer/Applications/WorkbenchReplacement/build/Build/Intermediates.noindex/WorkbenchReplacement.build/Debug/WorkbenchReplacement.build/Objects-normal/arm64/App.o",
+ "swift-dependencies" : "/Volumes/Developer/Applications/WorkbenchReplacement/build/Build/Intermediates.noindex/WorkbenchReplacement.build/Debug/WorkbenchReplacement.build/Objects-normal/arm64/App.swiftdeps",
+ "swiftmodule" : "/Volumes/Developer/Applications/WorkbenchReplacement/build/Build/Intermediates.noindex/WorkbenchReplacement.build/Debug/WorkbenchReplacement.build/Objects-normal/arm64/App~partial.swiftmodule"
+ },
+ "/Volumes/Developer/Applications/WorkbenchReplacement/WorkbenchReplacement/DatabaseModel.swift" : {
+ "const-values" : "/Volumes/Developer/Applications/WorkbenchReplacement/build/Build/Intermediates.noindex/WorkbenchReplacement.build/Debug/WorkbenchReplacement.build/Objects-normal/arm64/DatabaseModel.swiftconstvalues",
+ "dependencies" : "/Volumes/Developer/Applications/WorkbenchReplacement/build/Build/Intermediates.noindex/WorkbenchReplacement.build/Debug/WorkbenchReplacement.build/Objects-normal/arm64/DatabaseModel.d",
+ "diagnostics" : "/Volumes/Developer/Applications/WorkbenchReplacement/build/Build/Intermediates.noindex/WorkbenchReplacement.build/Debug/WorkbenchReplacement.build/Objects-normal/arm64/DatabaseModel.dia",
+ "index-unit-output-path" : "/WorkbenchReplacement.build/Debug/WorkbenchReplacement.build/Objects-normal/arm64/DatabaseModel.o",
+ "llvm-bc" : "/Volumes/Developer/Applications/WorkbenchReplacement/build/Build/Intermediates.noindex/WorkbenchReplacement.build/Debug/WorkbenchReplacement.build/Objects-normal/arm64/DatabaseModel.bc",
+ "object" : "/Volumes/Developer/Applications/WorkbenchReplacement/build/Build/Intermediates.noindex/WorkbenchReplacement.build/Debug/WorkbenchReplacement.build/Objects-normal/arm64/DatabaseModel.o",
+ "swift-dependencies" : "/Volumes/Developer/Applications/WorkbenchReplacement/build/Build/Intermediates.noindex/WorkbenchReplacement.build/Debug/WorkbenchReplacement.build/Objects-normal/arm64/DatabaseModel.swiftdeps",
+ "swiftmodule" : "/Volumes/Developer/Applications/WorkbenchReplacement/build/Build/Intermediates.noindex/WorkbenchReplacement.build/Debug/WorkbenchReplacement.build/Objects-normal/arm64/DatabaseModel~partial.swiftmodule"
+ },
+ "/Volumes/Developer/Applications/WorkbenchReplacement/WorkbenchReplacement/MainView.swift" : {
+ "const-values" : "/Volumes/Developer/Applications/WorkbenchReplacement/build/Build/Intermediates.noindex/WorkbenchReplacement.build/Debug/WorkbenchReplacement.build/Objects-normal/arm64/MainView.swiftconstvalues",
+ "dependencies" : "/Volumes/Developer/Applications/WorkbenchReplacement/build/Build/Intermediates.noindex/WorkbenchReplacement.build/Debug/WorkbenchReplacement.build/Objects-normal/arm64/MainView.d",
+ "diagnostics" : "/Volumes/Developer/Applications/WorkbenchReplacement/build/Build/Intermediates.noindex/WorkbenchReplacement.build/Debug/WorkbenchReplacement.build/Objects-normal/arm64/MainView.dia",
+ "index-unit-output-path" : "/WorkbenchReplacement.build/Debug/WorkbenchReplacement.build/Objects-normal/arm64/MainView.o",
+ "llvm-bc" : "/Volumes/Developer/Applications/WorkbenchReplacement/build/Build/Intermediates.noindex/WorkbenchReplacement.build/Debug/WorkbenchReplacement.build/Objects-normal/arm64/MainView.bc",
+ "object" : "/Volumes/Developer/Applications/WorkbenchReplacement/build/Build/Intermediates.noindex/WorkbenchReplacement.build/Debug/WorkbenchReplacement.build/Objects-normal/arm64/MainView.o",
+ "swift-dependencies" : "/Volumes/Developer/Applications/WorkbenchReplacement/build/Build/Intermediates.noindex/WorkbenchReplacement.build/Debug/WorkbenchReplacement.build/Objects-normal/arm64/MainView.swiftdeps",
+ "swiftmodule" : "/Volumes/Developer/Applications/WorkbenchReplacement/build/Build/Intermediates.noindex/WorkbenchReplacement.build/Debug/WorkbenchReplacement.build/Objects-normal/arm64/MainView~partial.swiftmodule"
+ },
+ "/Volumes/Developer/Applications/WorkbenchReplacement/WorkbenchReplacement/MySQLProtocol.swift" : {
+ "const-values" : "/Volumes/Developer/Applications/WorkbenchReplacement/build/Build/Intermediates.noindex/WorkbenchReplacement.build/Debug/WorkbenchReplacement.build/Objects-normal/arm64/MySQLProtocol.swiftconstvalues",
+ "dependencies" : "/Volumes/Developer/Applications/WorkbenchReplacement/build/Build/Intermediates.noindex/WorkbenchReplacement.build/Debug/WorkbenchReplacement.build/Objects-normal/arm64/MySQLProtocol.d",
+ "diagnostics" : "/Volumes/Developer/Applications/WorkbenchReplacement/build/Build/Intermediates.noindex/WorkbenchReplacement.build/Debug/WorkbenchReplacement.build/Objects-normal/arm64/MySQLProtocol.dia",
+ "index-unit-output-path" : "/WorkbenchReplacement.build/Debug/WorkbenchReplacement.build/Objects-normal/arm64/MySQLProtocol.o",
+ "llvm-bc" : "/Volumes/Developer/Applications/WorkbenchReplacement/build/Build/Intermediates.noindex/WorkbenchReplacement.build/Debug/WorkbenchReplacement.build/Objects-normal/arm64/MySQLProtocol.bc",
+ "object" : "/Volumes/Developer/Applications/WorkbenchReplacement/build/Build/Intermediates.noindex/WorkbenchReplacement.build/Debug/WorkbenchReplacement.build/Objects-normal/arm64/MySQLProtocol.o",
+ "swift-dependencies" : "/Volumes/Developer/Applications/WorkbenchReplacement/build/Build/Intermediates.noindex/WorkbenchReplacement.build/Debug/WorkbenchReplacement.build/Objects-normal/arm64/MySQLProtocol.swiftdeps",
+ "swiftmodule" : "/Volumes/Developer/Applications/WorkbenchReplacement/build/Build/Intermediates.noindex/WorkbenchReplacement.build/Debug/WorkbenchReplacement.build/Objects-normal/arm64/MySQLProtocol~partial.swiftmodule"
+ }
+}
\ No newline at end of file
diff --git a/build/Build/Intermediates.noindex/XCBuildData/b4f9670003ddbc00bd0e34134fd05c3c.xcbuilddata/attachments/8bba4233626f64a7ea772bb94a08a1a9 b/build/Build/Intermediates.noindex/XCBuildData/b4f9670003ddbc00bd0e34134fd05c3c.xcbuilddata/attachments/8bba4233626f64a7ea772bb94a08a1a9
new file mode 100644
index 0000000..ee59dbc
--- /dev/null
+++ b/build/Build/Intermediates.noindex/XCBuildData/b4f9670003ddbc00bd0e34134fd05c3c.xcbuilddata/attachments/8bba4233626f64a7ea772bb94a08a1a9
@@ -0,0 +1 @@
+{"case-sensitive":"false","roots":[],"version":0}
\ No newline at end of file
diff --git a/build/Build/Intermediates.noindex/XCBuildData/b4f9670003ddbc00bd0e34134fd05c3c.xcbuilddata/attachments/9a2910799639cff85464f3dcb75559c1 b/build/Build/Intermediates.noindex/XCBuildData/b4f9670003ddbc00bd0e34134fd05c3c.xcbuilddata/attachments/9a2910799639cff85464f3dcb75559c1
new file mode 100644
index 0000000..d78c86b
--- /dev/null
+++ b/build/Build/Intermediates.noindex/XCBuildData/b4f9670003ddbc00bd0e34134fd05c3c.xcbuilddata/attachments/9a2910799639cff85464f3dcb75559c1
@@ -0,0 +1 @@
+["AnyResolverProviding","AppEntity","AppEnum","AppExtension","AppIntent","AppIntentsPackage","AppShortcutProviding","AppShortcutsProvider","AppUnionValue","AppUnionValueCasesProviding","DynamicOptionsProvider","EntityQuery","ExtensionPointDefining","IntentValueQuery","Resolver","TransientEntity","_AssistantIntentsProvider","_GenerativeFunctionExtractable","_IntentValueRepresentable"]
\ No newline at end of file
diff --git a/build/Build/Intermediates.noindex/XCBuildData/b4f9670003ddbc00bd0e34134fd05c3c.xcbuilddata/attachments/b47c260a05c70c9e20ee4b0d1fc4e6ed b/build/Build/Intermediates.noindex/XCBuildData/b4f9670003ddbc00bd0e34134fd05c3c.xcbuilddata/attachments/b47c260a05c70c9e20ee4b0d1fc4e6ed
new file mode 100644
index 0000000..f9bf4c6
--- /dev/null
+++ b/build/Build/Intermediates.noindex/XCBuildData/b4f9670003ddbc00bd0e34134fd05c3c.xcbuilddata/attachments/b47c260a05c70c9e20ee4b0d1fc4e6ed
@@ -0,0 +1,4 @@
+/Volumes/Developer/Applications/WorkbenchReplacement/build/Build/Intermediates.noindex/WorkbenchReplacement.build/Debug/WorkbenchReplacement.build/Objects-normal/arm64/App.o
+/Volumes/Developer/Applications/WorkbenchReplacement/build/Build/Intermediates.noindex/WorkbenchReplacement.build/Debug/WorkbenchReplacement.build/Objects-normal/arm64/MainView.o
+/Volumes/Developer/Applications/WorkbenchReplacement/build/Build/Intermediates.noindex/WorkbenchReplacement.build/Debug/WorkbenchReplacement.build/Objects-normal/arm64/DatabaseModel.o
+/Volumes/Developer/Applications/WorkbenchReplacement/build/Build/Intermediates.noindex/WorkbenchReplacement.build/Debug/WorkbenchReplacement.build/Objects-normal/arm64/MySQLProtocol.o
diff --git a/build/Build/Intermediates.noindex/XCBuildData/b4f9670003ddbc00bd0e34134fd05c3c.xcbuilddata/attachments/b6a63299643304380633b19a42b96379 b/build/Build/Intermediates.noindex/XCBuildData/b4f9670003ddbc00bd0e34134fd05c3c.xcbuilddata/attachments/b6a63299643304380633b19a42b96379
new file mode 100644
index 0000000..b2f4ea6
--- /dev/null
+++ b/build/Build/Intermediates.noindex/XCBuildData/b4f9670003ddbc00bd0e34134fd05c3c.xcbuilddata/attachments/b6a63299643304380633b19a42b96379
@@ -0,0 +1,4 @@
+/Volumes/Developer/Applications/WorkbenchReplacement/build/Build/Intermediates.noindex/WorkbenchReplacement.build/Debug/WorkbenchReplacement.build/Objects-normal/arm64/App.swiftconstvalues
+/Volumes/Developer/Applications/WorkbenchReplacement/build/Build/Intermediates.noindex/WorkbenchReplacement.build/Debug/WorkbenchReplacement.build/Objects-normal/arm64/MainView.swiftconstvalues
+/Volumes/Developer/Applications/WorkbenchReplacement/build/Build/Intermediates.noindex/WorkbenchReplacement.build/Debug/WorkbenchReplacement.build/Objects-normal/arm64/DatabaseModel.swiftconstvalues
+/Volumes/Developer/Applications/WorkbenchReplacement/build/Build/Intermediates.noindex/WorkbenchReplacement.build/Debug/WorkbenchReplacement.build/Objects-normal/arm64/MySQLProtocol.swiftconstvalues
diff --git a/build/Build/Intermediates.noindex/XCBuildData/b4f9670003ddbc00bd0e34134fd05c3c.xcbuilddata/attachments/d3a090ccbccc4a2bbd58a3018530b3af b/build/Build/Intermediates.noindex/XCBuildData/b4f9670003ddbc00bd0e34134fd05c3c.xcbuilddata/attachments/d3a090ccbccc4a2bbd58a3018530b3af
new file mode 100644
index 0000000..09ea535
Binary files /dev/null and b/build/Build/Intermediates.noindex/XCBuildData/b4f9670003ddbc00bd0e34134fd05c3c.xcbuilddata/attachments/d3a090ccbccc4a2bbd58a3018530b3af differ
diff --git a/build/Build/Intermediates.noindex/XCBuildData/b4f9670003ddbc00bd0e34134fd05c3c.xcbuilddata/attachments/d41d8cd98f00b204e9800998ecf8427e b/build/Build/Intermediates.noindex/XCBuildData/b4f9670003ddbc00bd0e34134fd05c3c.xcbuilddata/attachments/d41d8cd98f00b204e9800998ecf8427e
new file mode 100644
index 0000000..e69de29
diff --git a/build/Build/Intermediates.noindex/XCBuildData/b4f9670003ddbc00bd0e34134fd05c3c.xcbuilddata/attachments/d88acc6e91fc370129743fcdc3e34ab1 b/build/Build/Intermediates.noindex/XCBuildData/b4f9670003ddbc00bd0e34134fd05c3c.xcbuilddata/attachments/d88acc6e91fc370129743fcdc3e34ab1
new file mode 100644
index 0000000..5920ce0
--- /dev/null
+++ b/build/Build/Intermediates.noindex/XCBuildData/b4f9670003ddbc00bd0e34134fd05c3c.xcbuilddata/attachments/d88acc6e91fc370129743fcdc3e34ab1
@@ -0,0 +1,4 @@
+/Volumes/Developer/Applications/WorkbenchReplacement/WorkbenchReplacement/App.swift
+/Volumes/Developer/Applications/WorkbenchReplacement/WorkbenchReplacement/MainView.swift
+/Volumes/Developer/Applications/WorkbenchReplacement/WorkbenchReplacement/DatabaseModel.swift
+/Volumes/Developer/Applications/WorkbenchReplacement/WorkbenchReplacement/MySQLProtocol.swift
diff --git a/build/Build/Intermediates.noindex/XCBuildData/b4f9670003ddbc00bd0e34134fd05c3c.xcbuilddata/attachments/dccfce55fa07ed9ec6f5920d7bed9910 b/build/Build/Intermediates.noindex/XCBuildData/b4f9670003ddbc00bd0e34134fd05c3c.xcbuilddata/attachments/dccfce55fa07ed9ec6f5920d7bed9910
new file mode 100644
index 0000000..81feccc
--- /dev/null
+++ b/build/Build/Intermediates.noindex/XCBuildData/b4f9670003ddbc00bd0e34134fd05c3c.xcbuilddata/attachments/dccfce55fa07ed9ec6f5920d7bed9910
@@ -0,0 +1 @@
+WorkbenchReplacement.debug.dylib
\ No newline at end of file
diff --git a/build/Build/Intermediates.noindex/XCBuildData/b4f9670003ddbc00bd0e34134fd05c3c.xcbuilddata/attachments/f86cbb10517a3bd51f252887359b04af b/build/Build/Intermediates.noindex/XCBuildData/b4f9670003ddbc00bd0e34134fd05c3c.xcbuilddata/attachments/f86cbb10517a3bd51f252887359b04af
new file mode 100644
index 0000000..3842541
--- /dev/null
+++ b/build/Build/Intermediates.noindex/XCBuildData/b4f9670003ddbc00bd0e34134fd05c3c.xcbuilddata/attachments/f86cbb10517a3bd51f252887359b04af
@@ -0,0 +1,8 @@
+
+
+
+
+ com.apple.security.get-task-allow
+
+
+
diff --git a/build/Build/Intermediates.noindex/XCBuildData/b4f9670003ddbc00bd0e34134fd05c3c.xcbuilddata/attachments/fd8314defc70a8778956f026c0ddfd19 b/build/Build/Intermediates.noindex/XCBuildData/b4f9670003ddbc00bd0e34134fd05c3c.xcbuilddata/attachments/fd8314defc70a8778956f026c0ddfd19
new file mode 100644
index 0000000..dd8b535
Binary files /dev/null and b/build/Build/Intermediates.noindex/XCBuildData/b4f9670003ddbc00bd0e34134fd05c3c.xcbuilddata/attachments/fd8314defc70a8778956f026c0ddfd19 differ
diff --git a/build/Build/Intermediates.noindex/XCBuildData/69f67b13647b23476e4a199d44d0239c.xcbuilddata/build-request.json b/build/Build/Intermediates.noindex/XCBuildData/b4f9670003ddbc00bd0e34134fd05c3c.xcbuilddata/build-request.json
similarity index 97%
rename from build/Build/Intermediates.noindex/XCBuildData/69f67b13647b23476e4a199d44d0239c.xcbuilddata/build-request.json
rename to build/Build/Intermediates.noindex/XCBuildData/b4f9670003ddbc00bd0e34134fd05c3c.xcbuilddata/build-request.json
index ed952da..cb3dcea 100644
--- a/build/Build/Intermediates.noindex/XCBuildData/69f67b13647b23476e4a199d44d0239c.xcbuilddata/build-request.json
+++ b/build/Build/Intermediates.noindex/XCBuildData/b4f9670003ddbc00bd0e34134fd05c3c.xcbuilddata/build-request.json
@@ -50,7 +50,7 @@
"table" : {
"ACTION" : "build",
"COLOR_DIAGNOSTICS" : "YES",
- "diagnostic_message_length" : "167",
+ "diagnostic_message_length" : "162",
"ENABLE_PREVIEWS" : "NO",
"ENABLE_XOJIT_PREVIEWS" : "YES",
"ONLY_ACTIVE_ARCH" : "YES",
diff --git a/build/Build/Intermediates.noindex/XCBuildData/b4f9670003ddbc00bd0e34134fd05c3c.xcbuilddata/description.msgpack b/build/Build/Intermediates.noindex/XCBuildData/b4f9670003ddbc00bd0e34134fd05c3c.xcbuilddata/description.msgpack
new file mode 100644
index 0000000..d826de2
Binary files /dev/null and b/build/Build/Intermediates.noindex/XCBuildData/b4f9670003ddbc00bd0e34134fd05c3c.xcbuilddata/description.msgpack differ
diff --git a/build/Build/Intermediates.noindex/XCBuildData/b4f9670003ddbc00bd0e34134fd05c3c.xcbuilddata/manifest.json b/build/Build/Intermediates.noindex/XCBuildData/b4f9670003ddbc00bd0e34134fd05c3c.xcbuilddata/manifest.json
new file mode 100644
index 0000000..1fb7dbf
--- /dev/null
+++ b/build/Build/Intermediates.noindex/XCBuildData/b4f9670003ddbc00bd0e34134fd05c3c.xcbuilddata/manifest.json
@@ -0,0 +1 @@
+{"client":{"name":"basic","version":0,"file-system":"device-agnostic","perform-ownership-analysis":"no"},"targets":{"":[""]},"nodes":{"/Volumes/Developer/Applications/WorkbenchReplacement/build/Build/Intermediates.noindex":{"is-mutated":true},"/Volumes/Developer/Applications/WorkbenchReplacement/build/Build/Intermediates.noindex/EagerLinkingTBDs/Debug":{"is-mutated":true},"/Volumes/Developer/Applications/WorkbenchReplacement/build/Build/Intermediates.noindex/ExplicitPrecompiledModules":{"is-mutated":true},"/Volumes/Developer/Applications/WorkbenchReplacement/build/Build/Intermediates.noindex/PrecompiledHeaders":{"is-mutated":true},"/Volumes/Developer/Applications/WorkbenchReplacement/build/Build/Intermediates.noindex/SwiftExplicitPrecompiledModules":{"is-mutated":true},"/Volumes/Developer/Applications/WorkbenchReplacement/build/Build/Products":{"is-mutated":true},"/Volumes/Developer/Applications/WorkbenchReplacement/build/Build/Products/Debug":{"is-mutated":true},"/Volumes/Developer/Applications/WorkbenchReplacement/build/Build/Products/Debug/WorkbenchReplacement.app":{"is-mutated":true},"/Volumes/Developer/Applications/WorkbenchReplacement/build/Build/Products/Debug/WorkbenchReplacement.app/Contents/MacOS/WorkbenchReplacement":{"is-mutated":true},"/Volumes/Developer/Applications/WorkbenchReplacement/build/Build/Products/Debug/WorkbenchReplacement.app/Contents/MacOS/WorkbenchReplacement.debug.dylib":{"is-mutated":true},"/Volumes/Developer/Applications/WorkbenchReplacement/build/Build/Products/Debug/WorkbenchReplacement.app/Contents/MacOS/__preview.dylib":{"is-mutated":true},"/Volumes/Developer/Applications/WorkbenchReplacement/build/CompilationCache.noindex":{"is-mutated":true},"/Volumes/Developer/Applications/WorkbenchReplacement/build/ModuleCache.noindex":{"is-mutated":true},"/Volumes/Developer/Applications/WorkbenchReplacement/build/SDKExplicitPrecompiledModules":{"is-mutated":true},"":{"is-command-timestamp":true},"":{"is-command-timestamp":true},"":{"is-command-timestamp":true},"":{"is-command-timestamp":true},"":{"is-command-timestamp":true},"":{"is-command-timestamp":true}},"commands":{"":{"tool":"phony","inputs":["/Volumes/Developer/Applications/WorkbenchReplacement/build/Build/Intermediates.noindex/EagerLinkingTBDs/Debug","/Volumes/Developer/Applications/WorkbenchReplacement/build/Build/Intermediates.noindex/ExplicitPrecompiledModules","/Volumes/Developer/Applications/WorkbenchReplacement/build/Build/Intermediates.noindex/PrecompiledHeaders","/Volumes/Developer/Applications/WorkbenchReplacement/build/Build/Intermediates.noindex/SwiftExplicitPrecompiledModules","/Volumes/Developer/Applications/WorkbenchReplacement/build/Build/Intermediates.noindex/WorkbenchReplacement.build/Debug/WorkbenchReplacement.build/Objects-normal/arm64/ExtractedAppShortcutsMetadata.stringsdata","/Volumes/Developer/Applications/WorkbenchReplacement/build/Build/Products/Debug/WorkbenchReplacement.app/Contents","/Volumes/Developer/Applications/WorkbenchReplacement/build/Build/Products/Debug/WorkbenchReplacement.app/Contents/MacOS","/Volumes/Developer/Applications/WorkbenchReplacement/build/Build/Products/Debug/WorkbenchReplacement.app/Contents/MacOS/__preview.dylib","/Volumes/Developer/Applications/WorkbenchReplacement/build/Build/Products/Debug/WorkbenchReplacement.app/_CodeSignature","/Volumes/Developer/Applications/WorkbenchReplacement/build/CompilationCache.noindex","/Volumes/Developer/Applications/WorkbenchReplacement/build/ModuleCache.noindex","/Volumes/Developer/Applications/WorkbenchReplacement/build/SDKExplicitPrecompiledModules","/Volumes/Developer/Applications/WorkbenchReplacement/build/SDKStatCaches.noindex/macosx27.0-26A425-e829b21a6f8c9b70876ba0858053c0447231f6df4def33e022ae2eb8f782df1a.sdkstatcache","","","","","","","","",""],"outputs":[""]},"":{"tool":"stale-file-removal","expectedOutputs":["/Volumes/Developer/Applications/WorkbenchReplacement/build/Build/Products/Debug/WorkbenchReplacement.app","/Volumes/Developer/Applications/WorkbenchReplacement/build/Build/Products/Debug/WorkbenchReplacement.app/Contents/MacOS/WorkbenchReplacement","/Volumes/Developer/Applications/WorkbenchReplacement/build/Build/Products/Debug/WorkbenchReplacement.app/_CodeSignature","/Volumes/Developer/Applications/WorkbenchReplacement/build/Build/Products/Debug/WorkbenchReplacement.app/Contents/MacOS/WorkbenchReplacement.debug.dylib","/Volumes/Developer/Applications/WorkbenchReplacement/build/Build/Products/Debug/WorkbenchReplacement.app/Contents/MacOS/__preview.dylib","/Volumes/Developer/Applications/WorkbenchReplacement/build/Build/Intermediates.noindex/WorkbenchReplacement.build/Debug/WorkbenchReplacement.build/Objects-normal/arm64/ExtractedAppShortcutsMetadata.stringsdata","/Volumes/Developer/Applications/WorkbenchReplacement/build/Build/Products/Debug/WorkbenchReplacement.app","/Volumes/Developer/Applications/WorkbenchReplacement/build/Build/Products/Debug/WorkbenchReplacement.app/Contents","/Volumes/Developer/Applications/WorkbenchReplacement/build/Build/Products/Debug/WorkbenchReplacement.app/Contents/MacOS","/Volumes/Developer/Applications/WorkbenchReplacement/build/Build/Products/Debug/WorkbenchReplacement.app/Contents/Info.plist","/Volumes/Developer/Applications/WorkbenchReplacement/build/Build/Products/Debug/WorkbenchReplacement.app/Contents/PkgInfo","/Volumes/Developer/Applications/WorkbenchReplacement/build/Build/Intermediates.noindex/WorkbenchReplacement.build/Debug/WorkbenchReplacement.build/WorkbenchReplacement.app.xcent","/Volumes/Developer/Applications/WorkbenchReplacement/build/Build/Intermediates.noindex/WorkbenchReplacement.build/Debug/WorkbenchReplacement.build/WorkbenchReplacement.app.xcent.der","/Volumes/Developer/Applications/WorkbenchReplacement/build/Build/Products/Debug/WorkbenchReplacement.app","/Volumes/Developer/Applications/WorkbenchReplacement/build/Build/Intermediates.noindex/WorkbenchReplacement.build/Debug/WorkbenchReplacement.build/Objects-normal/arm64/WorkbenchReplacement Swift Compilation Finished","/Volumes/Developer/Applications/WorkbenchReplacement/build/Build/Intermediates.noindex/WorkbenchReplacement.build/Debug/WorkbenchReplacement.build/Objects-normal/arm64/App.o","/Volumes/Developer/Applications/WorkbenchReplacement/build/Build/Intermediates.noindex/WorkbenchReplacement.build/Debug/WorkbenchReplacement.build/Objects-normal/arm64/MainView.o","/Volumes/Developer/Applications/WorkbenchReplacement/build/Build/Intermediates.noindex/WorkbenchReplacement.build/Debug/WorkbenchReplacement.build/Objects-normal/arm64/DatabaseModel.o","/Volumes/Developer/Applications/WorkbenchReplacement/build/Build/Intermediates.noindex/WorkbenchReplacement.build/Debug/WorkbenchReplacement.build/Objects-normal/arm64/MySQLProtocol.o","/Volumes/Developer/Applications/WorkbenchReplacement/build/Build/Intermediates.noindex/WorkbenchReplacement.build/Debug/WorkbenchReplacement.build/Objects-normal/arm64/App.stringsdata","/Volumes/Developer/Applications/WorkbenchReplacement/build/Build/Intermediates.noindex/WorkbenchReplacement.build/Debug/WorkbenchReplacement.build/Objects-normal/arm64/MainView.stringsdata","/Volumes/Developer/Applications/WorkbenchReplacement/build/Build/Intermediates.noindex/WorkbenchReplacement.build/Debug/WorkbenchReplacement.build/Objects-normal/arm64/DatabaseModel.stringsdata","/Volumes/Developer/Applications/WorkbenchReplacement/build/Build/Intermediates.noindex/WorkbenchReplacement.build/Debug/WorkbenchReplacement.build/Objects-normal/arm64/MySQLProtocol.stringsdata","/Volumes/Developer/Applications/WorkbenchReplacement/build/Build/Intermediates.noindex/WorkbenchReplacement.build/Debug/WorkbenchReplacement.build/Objects-normal/arm64/App.swiftconstvalues","/Volumes/Developer/Applications/WorkbenchReplacement/build/Build/Intermediates.noindex/WorkbenchReplacement.build/Debug/WorkbenchReplacement.build/Objects-normal/arm64/MainView.swiftconstvalues","/Volumes/Developer/Applications/WorkbenchReplacement/build/Build/Intermediates.noindex/WorkbenchReplacement.build/Debug/WorkbenchReplacement.build/Objects-normal/arm64/DatabaseModel.swiftconstvalues","/Volumes/Developer/Applications/WorkbenchReplacement/build/Build/Intermediates.noindex/WorkbenchReplacement.build/Debug/WorkbenchReplacement.build/Objects-normal/arm64/MySQLProtocol.swiftconstvalues","/Volumes/Developer/Applications/WorkbenchReplacement/build/Build/Products/Debug/WorkbenchReplacement.app","/Volumes/Developer/Applications/WorkbenchReplacement/build/Build/Intermediates.noindex/WorkbenchReplacement.build/Debug/WorkbenchReplacement.build/WorkbenchReplacement-ExecutorLinkFileList-normal-arm64.txt","/Volumes/Developer/Applications/WorkbenchReplacement/build/Build/Products/Debug/WorkbenchReplacement.swiftmodule/Project/arm64-apple-macos.swiftsourceinfo","/Volumes/Developer/Applications/WorkbenchReplacement/build/Build/Products/Debug/WorkbenchReplacement.swiftmodule/arm64-apple-macos.abi.json","/Volumes/Developer/Applications/WorkbenchReplacement/build/Build/Products/Debug/WorkbenchReplacement.swiftmodule/arm64-apple-macos.swiftdoc","/Volumes/Developer/Applications/WorkbenchReplacement/build/Build/Products/Debug/WorkbenchReplacement.swiftmodule/arm64-apple-macos.swiftmodule","/Volumes/Developer/Applications/WorkbenchReplacement/build/Build/Products/Debug/WorkbenchReplacement.app/Contents/MacOS/WorkbenchReplacement","/Volumes/Developer/Applications/WorkbenchReplacement/build/Build/Products/Debug/WorkbenchReplacement.app/Contents/MacOS/WorkbenchReplacement.debug.dylib","/Volumes/Developer/Applications/WorkbenchReplacement/build/Build/Intermediates.noindex/WorkbenchReplacement.build/Debug/WorkbenchReplacement.build/Objects-normal/arm64/WorkbenchReplacement_lto.o","/Volumes/Developer/Applications/WorkbenchReplacement/build/Build/Intermediates.noindex/WorkbenchReplacement.build/Debug/WorkbenchReplacement.build/Objects-normal/arm64/WorkbenchReplacement_dependency_info.dat","/Volumes/Developer/Applications/WorkbenchReplacement/build/Build/Products/Debug/WorkbenchReplacement.app/Contents/MacOS/__preview.dylib","/Volumes/Developer/Applications/WorkbenchReplacement/build/Build/Intermediates.noindex/WorkbenchReplacement.build/Debug/WorkbenchReplacement.build/Objects-normal/arm64/WorkbenchReplacement Swift Compilation Requirements Finished","/Volumes/Developer/Applications/WorkbenchReplacement/build/Build/Intermediates.noindex/WorkbenchReplacement.build/Debug/WorkbenchReplacement.build/Objects-normal/arm64/WorkbenchReplacement.swiftmodule","/Volumes/Developer/Applications/WorkbenchReplacement/build/Build/Intermediates.noindex/WorkbenchReplacement.build/Debug/WorkbenchReplacement.build/Objects-normal/arm64/WorkbenchReplacement-dependency-scan.dia","/Volumes/Developer/Applications/WorkbenchReplacement/build/Build/Intermediates.noindex/WorkbenchReplacement.build/Debug/WorkbenchReplacement.build/Objects-normal/arm64/WorkbenchReplacement.swiftsourceinfo","/Volumes/Developer/Applications/WorkbenchReplacement/build/Build/Intermediates.noindex/WorkbenchReplacement.build/Debug/WorkbenchReplacement.build/Objects-normal/arm64/WorkbenchReplacement.abi.json","/Volumes/Developer/Applications/WorkbenchReplacement/build/Build/Intermediates.noindex/WorkbenchReplacement.build/Debug/WorkbenchReplacement.build/Objects-normal/arm64/WorkbenchReplacement-Swift.h","/Volumes/Developer/Applications/WorkbenchReplacement/build/Build/Intermediates.noindex/WorkbenchReplacement.build/Debug/WorkbenchReplacement.build/Objects-normal/arm64/WorkbenchReplacement.swiftdoc","/Volumes/Developer/Applications/WorkbenchReplacement/build/Build/Intermediates.noindex/WorkbenchReplacement.build/Debug/WorkbenchReplacement.build/DerivedSources/WorkbenchReplacement-Swift.h","/Volumes/Developer/Applications/WorkbenchReplacement/build/Build/Intermediates.noindex/WorkbenchReplacement.build/Debug/WorkbenchReplacement.build/DerivedSources/Entitlements.plist","/Volumes/Developer/Applications/WorkbenchReplacement/build/Build/Intermediates.noindex/WorkbenchReplacement.build/Debug/WorkbenchReplacement.build/Objects-normal/arm64/WorkbenchReplacement-OutputFileMap.json","/Volumes/Developer/Applications/WorkbenchReplacement/build/Build/Intermediates.noindex/WorkbenchReplacement.build/Debug/WorkbenchReplacement.build/Objects-normal/arm64/WorkbenchReplacement.LinkFileList","/Volumes/Developer/Applications/WorkbenchReplacement/build/Build/Intermediates.noindex/WorkbenchReplacement.build/Debug/WorkbenchReplacement.build/Objects-normal/arm64/WorkbenchReplacement.SwiftConstValuesFileList","/Volumes/Developer/Applications/WorkbenchReplacement/build/Build/Intermediates.noindex/WorkbenchReplacement.build/Debug/WorkbenchReplacement.build/Objects-normal/arm64/WorkbenchReplacement.SwiftFileList","/Volumes/Developer/Applications/WorkbenchReplacement/build/Build/Intermediates.noindex/WorkbenchReplacement.build/Debug/WorkbenchReplacement.build/Objects-normal/arm64/WorkbenchReplacement_const_extract_protocols.json","/Volumes/Developer/Applications/WorkbenchReplacement/build/Build/Intermediates.noindex/WorkbenchReplacement.build/Debug/WorkbenchReplacement.build/WorkbenchReplacement-DebugDylibInstallName-normal-arm64.txt","/Volumes/Developer/Applications/WorkbenchReplacement/build/Build/Intermediates.noindex/WorkbenchReplacement.build/Debug/WorkbenchReplacement.build/WorkbenchReplacement-DebugDylibPath-normal-arm64.txt","/Volumes/Developer/Applications/WorkbenchReplacement/build/Build/Intermediates.noindex/WorkbenchReplacement.build/Debug/WorkbenchReplacement.build/WorkbenchReplacement-all-non-framework-target-headers.hmap","/Volumes/Developer/Applications/WorkbenchReplacement/build/Build/Intermediates.noindex/WorkbenchReplacement.build/Debug/WorkbenchReplacement.build/WorkbenchReplacement-all-target-headers.hmap","/Volumes/Developer/Applications/WorkbenchReplacement/build/Build/Intermediates.noindex/WorkbenchReplacement.build/Debug/WorkbenchReplacement.build/WorkbenchReplacement-generated-files.hmap","/Volumes/Developer/Applications/WorkbenchReplacement/build/Build/Intermediates.noindex/WorkbenchReplacement.build/Debug/WorkbenchReplacement.build/WorkbenchReplacement-own-target-headers.hmap","/Volumes/Developer/Applications/WorkbenchReplacement/build/Build/Intermediates.noindex/WorkbenchReplacement.build/Debug/WorkbenchReplacement.build/WorkbenchReplacement-project-headers.hmap","/Volumes/Developer/Applications/WorkbenchReplacement/build/Build/Intermediates.noindex/WorkbenchReplacement.build/Debug/WorkbenchReplacement.build/WorkbenchReplacement.DependencyMetadataFileList","/Volumes/Developer/Applications/WorkbenchReplacement/build/Build/Intermediates.noindex/WorkbenchReplacement.build/Debug/WorkbenchReplacement.build/WorkbenchReplacement.DependencyStaticMetadataFileList","/Volumes/Developer/Applications/WorkbenchReplacement/build/Build/Intermediates.noindex/WorkbenchReplacement.build/Debug/WorkbenchReplacement.build/WorkbenchReplacement.hmap","/Volumes/Developer/Applications/WorkbenchReplacement/build/Build/Intermediates.noindex/WorkbenchReplacement.build/Debug/WorkbenchReplacement.build/empty-WorkbenchReplacement.plist"],"roots":["/tmp/WorkbenchReplacement.dst","/Volumes/Developer/Applications/WorkbenchReplacement/build/Build/Intermediates.noindex","/Volumes/Developer/Applications/WorkbenchReplacement/build/Build/Products"],"outputs":[""]},"":{"tool":"stale-file-removal","expectedOutputs":["/Volumes/Developer/Applications/WorkbenchReplacement/build/Build/Intermediates.noindex/WorkbenchReplacement.build/Debug/WorkbenchReplacement-34f2007b1d0798da0712fb82baec90fb-VFS/all-product-headers.yaml"],"outputs":[""]},"P0:::ClangStatCache /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin/clang-stat-cache /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX27.0.sdk /Volumes/Developer/Applications/WorkbenchReplacement/build/SDKStatCaches.noindex/macosx27.0-26A425-e829b21a6f8c9b70876ba0858053c0447231f6df4def33e022ae2eb8f782df1a.sdkstatcache":{"tool":"shell","description":"ClangStatCache /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin/clang-stat-cache /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX27.0.sdk /Volumes/Developer/Applications/WorkbenchReplacement/build/SDKStatCaches.noindex/macosx27.0-26A425-e829b21a6f8c9b70876ba0858053c0447231f6df4def33e022ae2eb8f782df1a.sdkstatcache","inputs":[],"outputs":["/Volumes/Developer/Applications/WorkbenchReplacement/build/SDKStatCaches.noindex/macosx27.0-26A425-e829b21a6f8c9b70876ba0858053c0447231f6df4def33e022ae2eb8f782df1a.sdkstatcache",""],"args":["/Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin/clang-stat-cache","/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX27.0.sdk","-o","/Volumes/Developer/Applications/WorkbenchReplacement/build/SDKStatCaches.noindex/macosx27.0-26A425-e829b21a6f8c9b70876ba0858053c0447231f6df4def33e022ae2eb8f782df1a.sdkstatcache"],"env":{},"always-out-of-date":true,"working-directory":"/Volumes/Developer/Applications/WorkbenchReplacement/WorkbenchReplacement.xcodeproj","signature":"d4ef45907cf3236d96171349ed1b904b"},"P0:::CreateBuildDirectory /Volumes/Developer/Applications/WorkbenchReplacement/build/Build/Intermediates.noindex":{"tool":"create-build-directory","description":"CreateBuildDirectory /Volumes/Developer/Applications/WorkbenchReplacement/build/Build/Intermediates.noindex","inputs":[],"outputs":["","/Volumes/Developer/Applications/WorkbenchReplacement/build/Build/Intermediates.noindex"]},"P0:::CreateBuildDirectory /Volumes/Developer/Applications/WorkbenchReplacement/build/Build/Intermediates.noindex/EagerLinkingTBDs/Debug":{"tool":"create-build-directory","description":"CreateBuildDirectory /Volumes/Developer/Applications/WorkbenchReplacement/build/Build/Intermediates.noindex/EagerLinkingTBDs/Debug","inputs":["/Volumes/Developer/Applications/WorkbenchReplacement/build/Build/Intermediates.noindex"],"outputs":["","/Volumes/Developer/Applications/WorkbenchReplacement/build/Build/Intermediates.noindex/EagerLinkingTBDs/Debug"]},"P0:::CreateBuildDirectory /Volumes/Developer/Applications/WorkbenchReplacement/build/Build/Intermediates.noindex/ExplicitPrecompiledModules":{"tool":"create-build-directory","description":"CreateBuildDirectory /Volumes/Developer/Applications/WorkbenchReplacement/build/Build/Intermediates.noindex/ExplicitPrecompiledModules","inputs":["/Volumes/Developer/Applications/WorkbenchReplacement/build/Build/Intermediates.noindex"],"outputs":["","/Volumes/Developer/Applications/WorkbenchReplacement/build/Build/Intermediates.noindex/ExplicitPrecompiledModules"]},"P0:::CreateBuildDirectory /Volumes/Developer/Applications/WorkbenchReplacement/build/Build/Intermediates.noindex/PrecompiledHeaders":{"tool":"create-build-directory","description":"CreateBuildDirectory /Volumes/Developer/Applications/WorkbenchReplacement/build/Build/Intermediates.noindex/PrecompiledHeaders","inputs":["/Volumes/Developer/Applications/WorkbenchReplacement/build/Build/Intermediates.noindex"],"outputs":["","/Volumes/Developer/Applications/WorkbenchReplacement/build/Build/Intermediates.noindex/PrecompiledHeaders"]},"P0:::CreateBuildDirectory /Volumes/Developer/Applications/WorkbenchReplacement/build/Build/Intermediates.noindex/SwiftExplicitPrecompiledModules":{"tool":"create-build-directory","description":"CreateBuildDirectory /Volumes/Developer/Applications/WorkbenchReplacement/build/Build/Intermediates.noindex/SwiftExplicitPrecompiledModules","inputs":["/Volumes/Developer/Applications/WorkbenchReplacement/build/Build/Intermediates.noindex"],"outputs":["","/Volumes/Developer/Applications/WorkbenchReplacement/build/Build/Intermediates.noindex/SwiftExplicitPrecompiledModules"]},"P0:::CreateBuildDirectory /Volumes/Developer/Applications/WorkbenchReplacement/build/Build/Products":{"tool":"create-build-directory","description":"CreateBuildDirectory /Volumes/Developer/Applications/WorkbenchReplacement/build/Build/Products","inputs":[],"outputs":["","/Volumes/Developer/Applications/WorkbenchReplacement/build/Build/Products"]},"P0:::CreateBuildDirectory /Volumes/Developer/Applications/WorkbenchReplacement/build/Build/Products/Debug":{"tool":"create-build-directory","description":"CreateBuildDirectory /Volumes/Developer/Applications/WorkbenchReplacement/build/Build/Products/Debug","inputs":["/Volumes/Developer/Applications/WorkbenchReplacement/build/Build/Products"],"outputs":["","/Volumes/Developer/Applications/WorkbenchReplacement/build/Build/Products/Debug"]},"P0:::CreateBuildDirectory /Volumes/Developer/Applications/WorkbenchReplacement/build/CompilationCache.noindex":{"tool":"create-build-directory","description":"CreateBuildDirectory /Volumes/Developer/Applications/WorkbenchReplacement/build/CompilationCache.noindex","inputs":[],"outputs":["","/Volumes/Developer/Applications/WorkbenchReplacement/build/CompilationCache.noindex"]},"P0:::CreateBuildDirectory /Volumes/Developer/Applications/WorkbenchReplacement/build/ModuleCache.noindex":{"tool":"create-build-directory","description":"CreateBuildDirectory /Volumes/Developer/Applications/WorkbenchReplacement/build/ModuleCache.noindex","inputs":[],"outputs":["","/Volumes/Developer/Applications/WorkbenchReplacement/build/ModuleCache.noindex"]},"P0:::CreateBuildDirectory /Volumes/Developer/Applications/WorkbenchReplacement/build/SDKExplicitPrecompiledModules":{"tool":"create-build-directory","description":"CreateBuildDirectory /Volumes/Developer/Applications/WorkbenchReplacement/build/SDKExplicitPrecompiledModules","inputs":[],"outputs":["","/Volumes/Developer/Applications/WorkbenchReplacement/build/SDKExplicitPrecompiledModules"]},"P0:::Gate WorkspaceHeaderMapVFSFilesWritten":{"tool":"phony","inputs":["/Volumes/Developer/Applications/WorkbenchReplacement/build/Build/Intermediates.noindex/WorkbenchReplacement.build/Debug/WorkbenchReplacement-34f2007b1d0798da0712fb82baec90fb-VFS/all-product-headers.yaml"],"outputs":[""]},"P0:::Gate target-WorkbenchReplacement-34f2007b1d0798da0712fb82baec90fb2878cdfbaa5fcb5fa30c8485a83dfb14--AppExtensionInfoPlistGeneratorTaskProducer":{"tool":"phony","inputs":["",""],"outputs":[""]},"P0:::Gate target-WorkbenchReplacement-34f2007b1d0798da0712fb82baec90fb2878cdfbaa5fcb5fa30c8485a83dfb14--AppIntentsMetadataTaskProducer":{"tool":"phony","inputs":["","","","","/Volumes/Developer/Applications/WorkbenchReplacement/build/Build/Intermediates.noindex/WorkbenchReplacement.build/Debug/WorkbenchReplacement.build/Objects-normal/arm64/WorkbenchReplacement.SwiftConstValuesFileList","/Volumes/Developer/Applications/WorkbenchReplacement/build/Build/Intermediates.noindex/WorkbenchReplacement.build/Debug/WorkbenchReplacement.build/WorkbenchReplacement.DependencyMetadataFileList","/Volumes/Developer/Applications/WorkbenchReplacement/build/Build/Intermediates.noindex/WorkbenchReplacement.build/Debug/WorkbenchReplacement.build/WorkbenchReplacement.DependencyStaticMetadataFileList"],"outputs":[""]},"P0:::Gate target-WorkbenchReplacement-34f2007b1d0798da0712fb82baec90fb2878cdfbaa5fcb5fa30c8485a83dfb14--Barrier-ChangeAlternatePermissions":{"tool":"phony","inputs":["","",""],"outputs":[""]},"P0:::Gate target-WorkbenchReplacement-34f2007b1d0798da0712fb82baec90fb2878cdfbaa5fcb5fa30c8485a83dfb14--Barrier-ChangePermissions":{"tool":"phony","inputs":["","",""],"outputs":[""]},"P0:::Gate target-WorkbenchReplacement-34f2007b1d0798da0712fb82baec90fb2878cdfbaa5fcb5fa30c8485a83dfb14--Barrier-CodeSign":{"tool":"phony","inputs":["","","","","",""],"outputs":[""]},"P0:::Gate target-WorkbenchReplacement-34f2007b1d0798da0712fb82baec90fb2878cdfbaa5fcb5fa30c8485a83dfb14--Barrier-CopyAside":{"tool":"phony","inputs":["","",""],"outputs":[""]},"P0:::Gate target-WorkbenchReplacement-34f2007b1d0798da0712fb82baec90fb2878cdfbaa5fcb5fa30c8485a83dfb14--Barrier-GenerateStubAPI":{"tool":"phony","inputs":["",""],"outputs":[""]},"P0:::Gate target-WorkbenchReplacement-34f2007b1d0798da0712fb82baec90fb2878cdfbaa5fcb5fa30c8485a83dfb14--Barrier-RegisterExecutionPolicyException":{"tool":"phony","inputs":["","","",""],"outputs":[""]},"P0:::Gate target-WorkbenchReplacement-34f2007b1d0798da0712fb82baec90fb2878cdfbaa5fcb5fa30c8485a83dfb14--Barrier-RegisterProduct":{"tool":"phony","inputs":["","","","",""],"outputs":[""]},"P0:::Gate target-WorkbenchReplacement-34f2007b1d0798da0712fb82baec90fb2878cdfbaa5fcb5fa30c8485a83dfb14--Barrier-StripSymbols":{"tool":"phony","inputs":["","",""],"outputs":[""]},"P0:::Gate target-WorkbenchReplacement-34f2007b1d0798da0712fb82baec90fb2878cdfbaa5fcb5fa30c8485a83dfb14--Barrier-Validate":{"tool":"phony","inputs":["","","",""],"outputs":[""]},"P0:::Gate target-WorkbenchReplacement-34f2007b1d0798da0712fb82baec90fb2878cdfbaa5fcb5fa30c8485a83dfb14--CopySwiftPackageResourcesTaskProducer":{"tool":"phony","inputs":["",""],"outputs":[""]},"P0:::Gate target-WorkbenchReplacement-34f2007b1d0798da0712fb82baec90fb2878cdfbaa5fcb5fa30c8485a83dfb14--CustomTaskProducer":{"tool":"phony","inputs":["",""],"outputs":[""]},"P0:::Gate target-WorkbenchReplacement-34f2007b1d0798da0712fb82baec90fb2878cdfbaa5fcb5fa30c8485a83dfb14--DocumentationTaskProducer":{"tool":"phony","inputs":["",""],"outputs":[""]},"P0:::Gate target-WorkbenchReplacement-34f2007b1d0798da0712fb82baec90fb2878cdfbaa5fcb5fa30c8485a83dfb14--ExtensionPointExtractorTaskProducer":{"tool":"phony","inputs":["",""],"outputs":[""]},"P0:::Gate target-WorkbenchReplacement-34f2007b1d0798da0712fb82baec90fb2878cdfbaa5fcb5fa30c8485a83dfb14--GenerateAppPlaygroundAssetCatalogTaskProducer":{"tool":"phony","inputs":["",""],"outputs":[""]},"P0:::Gate target-WorkbenchReplacement-34f2007b1d0798da0712fb82baec90fb2878cdfbaa5fcb5fa30c8485a83dfb14--GeneratedFilesTaskProducer":{"tool":"phony","inputs":["","","/Volumes/Developer/Applications/WorkbenchReplacement/build/Build/Intermediates.noindex/WorkbenchReplacement.build/Debug/WorkbenchReplacement.build/WorkbenchReplacement.app.xcent","/Volumes/Developer/Applications/WorkbenchReplacement/build/Build/Intermediates.noindex/WorkbenchReplacement.build/Debug/WorkbenchReplacement.build/WorkbenchReplacement.app.xcent.der","/Volumes/Developer/Applications/WorkbenchReplacement/build/Build/Intermediates.noindex/WorkbenchReplacement.build/Debug/WorkbenchReplacement.build/DerivedSources/Entitlements.plist"],"outputs":[""]},"P0:::Gate target-WorkbenchReplacement-34f2007b1d0798da0712fb82baec90fb2878cdfbaa5fcb5fa30c8485a83dfb14--HeadermapTaskProducer":{"tool":"phony","inputs":["","","/Volumes/Developer/Applications/WorkbenchReplacement/build/Build/Intermediates.noindex/WorkbenchReplacement.build/Debug/WorkbenchReplacement.build/WorkbenchReplacement-all-non-framework-target-headers.hmap","/Volumes/Developer/Applications/WorkbenchReplacement/build/Build/Intermediates.noindex/WorkbenchReplacement.build/Debug/WorkbenchReplacement.build/WorkbenchReplacement-all-target-headers.hmap","/Volumes/Developer/Applications/WorkbenchReplacement/build/Build/Intermediates.noindex/WorkbenchReplacement.build/Debug/WorkbenchReplacement.build/WorkbenchReplacement-generated-files.hmap","/Volumes/Developer/Applications/WorkbenchReplacement/build/Build/Intermediates.noindex/WorkbenchReplacement.build/Debug/WorkbenchReplacement.build/WorkbenchReplacement-own-target-headers.hmap","/Volumes/Developer/Applications/WorkbenchReplacement/build/Build/Intermediates.noindex/WorkbenchReplacement.build/Debug/WorkbenchReplacement.build/WorkbenchReplacement-project-headers.hmap","/Volumes/Developer/Applications/WorkbenchReplacement/build/Build/Intermediates.noindex/WorkbenchReplacement.build/Debug/WorkbenchReplacement.build/WorkbenchReplacement.hmap"],"outputs":[""]},"P0:::Gate target-WorkbenchReplacement-34f2007b1d0798da0712fb82baec90fb2878cdfbaa5fcb5fa30c8485a83dfb14--InfoPlistTaskProducer":{"tool":"phony","inputs":["","","/Volumes/Developer/Applications/WorkbenchReplacement/build/Build/Products/Debug/WorkbenchReplacement.app/Contents/Info.plist","/Volumes/Developer/Applications/WorkbenchReplacement/build/Build/Products/Debug/WorkbenchReplacement.app/Contents/PkgInfo","/Volumes/Developer/Applications/WorkbenchReplacement/build/Build/Intermediates.noindex/WorkbenchReplacement.build/Debug/WorkbenchReplacement.build/empty-WorkbenchReplacement.plist"],"outputs":[""]},"P0:::Gate target-WorkbenchReplacement-34f2007b1d0798da0712fb82baec90fb2878cdfbaa5fcb5fa30c8485a83dfb14--ModuleMapTaskProducer":{"tool":"phony","inputs":["",""],"outputs":[""]},"P0:::Gate target-WorkbenchReplacement-34f2007b1d0798da0712fb82baec90fb2878cdfbaa5fcb5fa30c8485a83dfb14--ModuleVerifierTaskProducer":{"tool":"phony","inputs":["",""],"outputs":[""]},"P0:::Gate target-WorkbenchReplacement-34f2007b1d0798da0712fb82baec90fb2878cdfbaa5fcb5fa30c8485a83dfb14--ProductPostprocessingTaskProducer":{"tool":"phony","inputs":["","","","","","","","","","","","","","","","","","","",""],"outputs":[""]},"P0:::Gate target-WorkbenchReplacement-34f2007b1d0798da0712fb82baec90fb2878cdfbaa5fcb5fa30c8485a83dfb14--ProductStructureTaskProducer":{"tool":"phony","inputs":["","","","",""],"outputs":[""]},"P0:::Gate target-WorkbenchReplacement-34f2007b1d0798da0712fb82baec90fb2878cdfbaa5fcb5fa30c8485a83dfb14--RealityAssetsTaskProducer":{"tool":"phony","inputs":["",""],"outputs":[""]},"P0:::Gate target-WorkbenchReplacement-34f2007b1d0798da0712fb82baec90fb2878cdfbaa5fcb5fa30c8485a83dfb14--SanitizerTaskProducer":{"tool":"phony","inputs":["",""],"outputs":[""]},"P0:::Gate target-WorkbenchReplacement-34f2007b1d0798da0712fb82baec90fb2878cdfbaa5fcb5fa30c8485a83dfb14--StubBinaryTaskProducer":{"tool":"phony","inputs":["",""],"outputs":[""]},"P0:::Gate target-WorkbenchReplacement-34f2007b1d0798da0712fb82baec90fb2878cdfbaa5fcb5fa30c8485a83dfb14--SwiftABIBaselineGenerationTaskProducer":{"tool":"phony","inputs":["","",""],"outputs":[""]},"P0:::Gate target-WorkbenchReplacement-34f2007b1d0798da0712fb82baec90fb2878cdfbaa5fcb5fa30c8485a83dfb14--SwiftFrameworkABICheckerTaskProducer":{"tool":"phony","inputs":["","",""],"outputs":[""]},"P0:::Gate target-WorkbenchReplacement-34f2007b1d0798da0712fb82baec90fb2878cdfbaa5fcb5fa30c8485a83dfb14--SwiftPackageCopyFilesTaskProducer":{"tool":"phony","inputs":["",""],"outputs":[""]},"P0:::Gate target-WorkbenchReplacement-34f2007b1d0798da0712fb82baec90fb2878cdfbaa5fcb5fa30c8485a83dfb14--SwiftStandardLibrariesTaskProducer":{"tool":"phony","inputs":["","","",""],"outputs":[""]},"P0:::Gate target-WorkbenchReplacement-34f2007b1d0798da0712fb82baec90fb2878cdfbaa5fcb5fa30c8485a83dfb14--TAPISymbolExtractorTaskProducer":{"tool":"phony","inputs":["",""],"outputs":[""]},"P0:::Gate target-WorkbenchReplacement-34f2007b1d0798da0712fb82baec90fb2878cdfbaa5fcb5fa30c8485a83dfb14--TestEntryPointTaskProducerFactory":{"tool":"phony","inputs":["",""],"outputs":[""]},"P0:::Gate target-WorkbenchReplacement-34f2007b1d0798da0712fb82baec90fb2878cdfbaa5fcb5fa30c8485a83dfb14--TestHostTaskProducer":{"tool":"phony","inputs":["",""],"outputs":[""]},"P0:::Gate target-WorkbenchReplacement-34f2007b1d0798da0712fb82baec90fb2878cdfbaa5fcb5fa30c8485a83dfb14--TestTargetPostprocessingTaskProducer":{"tool":"phony","inputs":["",""],"outputs":[""]},"P0:::Gate target-WorkbenchReplacement-34f2007b1d0798da0712fb82baec90fb2878cdfbaa5fcb5fa30c8485a83dfb14--TestTargetTaskProducer":{"tool":"phony","inputs":["",""],"outputs":[""]},"P0:::Gate target-WorkbenchReplacement-34f2007b1d0798da0712fb82baec90fb2878cdfbaa5fcb5fa30c8485a83dfb14--copy-headers-completion":{"tool":"phony","inputs":[""],"outputs":[""]},"P0:::Gate target-WorkbenchReplacement-34f2007b1d0798da0712fb82baec90fb2878cdfbaa5fcb5fa30c8485a83dfb14--fused-phase0-compile-sources&link-binary©-bundle-resources":{"tool":"phony","inputs":["","","/Volumes/Developer/Applications/WorkbenchReplacement/build/Build/Intermediates.noindex/WorkbenchReplacement.build/Debug/WorkbenchReplacement.build/Objects-normal/arm64/WorkbenchReplacement Swift Compilation Finished","/Volumes/Developer/Applications/WorkbenchReplacement/build/Build/Intermediates.noindex/WorkbenchReplacement.build/Debug/WorkbenchReplacement.build/Objects-normal/arm64/App.o","/Volumes/Developer/Applications/WorkbenchReplacement/build/Build/Intermediates.noindex/WorkbenchReplacement.build/Debug/WorkbenchReplacement.build/Objects-normal/arm64/MainView.o","/Volumes/Developer/Applications/WorkbenchReplacement/build/Build/Intermediates.noindex/WorkbenchReplacement.build/Debug/WorkbenchReplacement.build/Objects-normal/arm64/DatabaseModel.o","/Volumes/Developer/Applications/WorkbenchReplacement/build/Build/Intermediates.noindex/WorkbenchReplacement.build/Debug/WorkbenchReplacement.build/Objects-normal/arm64/MySQLProtocol.o","/Volumes/Developer/Applications/WorkbenchReplacement/build/Build/Intermediates.noindex/WorkbenchReplacement.build/Debug/WorkbenchReplacement.build/Objects-normal/arm64/App.stringsdata","/Volumes/Developer/Applications/WorkbenchReplacement/build/Build/Intermediates.noindex/WorkbenchReplacement.build/Debug/WorkbenchReplacement.build/Objects-normal/arm64/MainView.stringsdata","/Volumes/Developer/Applications/WorkbenchReplacement/build/Build/Intermediates.noindex/WorkbenchReplacement.build/Debug/WorkbenchReplacement.build/Objects-normal/arm64/DatabaseModel.stringsdata","/Volumes/Developer/Applications/WorkbenchReplacement/build/Build/Intermediates.noindex/WorkbenchReplacement.build/Debug/WorkbenchReplacement.build/Objects-normal/arm64/MySQLProtocol.stringsdata","/Volumes/Developer/Applications/WorkbenchReplacement/build/Build/Intermediates.noindex/WorkbenchReplacement.build/Debug/WorkbenchReplacement.build/Objects-normal/arm64/App.swiftconstvalues","/Volumes/Developer/Applications/WorkbenchReplacement/build/Build/Intermediates.noindex/WorkbenchReplacement.build/Debug/WorkbenchReplacement.build/Objects-normal/arm64/MainView.swiftconstvalues","/Volumes/Developer/Applications/WorkbenchReplacement/build/Build/Intermediates.noindex/WorkbenchReplacement.build/Debug/WorkbenchReplacement.build/Objects-normal/arm64/DatabaseModel.swiftconstvalues","/Volumes/Developer/Applications/WorkbenchReplacement/build/Build/Intermediates.noindex/WorkbenchReplacement.build/Debug/WorkbenchReplacement.build/Objects-normal/arm64/MySQLProtocol.swiftconstvalues","/Volumes/Developer/Applications/WorkbenchReplacement/build/Build/Intermediates.noindex/WorkbenchReplacement.build/Debug/WorkbenchReplacement.build/WorkbenchReplacement-ExecutorLinkFileList-normal-arm64.txt","/Volumes/Developer/Applications/WorkbenchReplacement/build/Build/Products/Debug/WorkbenchReplacement.swiftmodule/Project/arm64-apple-macos.swiftsourceinfo","/Volumes/Developer/Applications/WorkbenchReplacement/build/Build/Products/Debug/WorkbenchReplacement.swiftmodule/arm64-apple-macos.abi.json","/Volumes/Developer/Applications/WorkbenchReplacement/build/Build/Products/Debug/WorkbenchReplacement.swiftmodule/arm64-apple-macos.swiftdoc","/Volumes/Developer/Applications/WorkbenchReplacement/build/Build/Products/Debug/WorkbenchReplacement.swiftmodule/arm64-apple-macos.swiftmodule","","/Volumes/Developer/Applications/WorkbenchReplacement/build/Build/Intermediates.noindex/WorkbenchReplacement.build/Debug/WorkbenchReplacement.build/Objects-normal/arm64/WorkbenchReplacement_lto.o","/Volumes/Developer/Applications/WorkbenchReplacement/build/Build/Intermediates.noindex/WorkbenchReplacement.build/Debug/WorkbenchReplacement.build/Objects-normal/arm64/WorkbenchReplacement_dependency_info.dat","","/Volumes/Developer/Applications/WorkbenchReplacement/build/Build/Intermediates.noindex/WorkbenchReplacement.build/Debug/WorkbenchReplacement.build/Objects-normal/arm64/WorkbenchReplacement Swift Compilation Requirements Finished","/Volumes/Developer/Applications/WorkbenchReplacement/build/Build/Intermediates.noindex/WorkbenchReplacement.build/Debug/WorkbenchReplacement.build/Objects-normal/arm64/WorkbenchReplacement.swiftmodule","/Volumes/Developer/Applications/WorkbenchReplacement/build/Build/Intermediates.noindex/WorkbenchReplacement.build/Debug/WorkbenchReplacement.build/Objects-normal/arm64/WorkbenchReplacement-dependency-scan.dia","/Volumes/Developer/Applications/WorkbenchReplacement/build/Build/Intermediates.noindex/WorkbenchReplacement.build/Debug/WorkbenchReplacement.build/Objects-normal/arm64/WorkbenchReplacement.swiftsourceinfo","/Volumes/Developer/Applications/WorkbenchReplacement/build/Build/Intermediates.noindex/WorkbenchReplacement.build/Debug/WorkbenchReplacement.build/Objects-normal/arm64/WorkbenchReplacement.abi.json","/Volumes/Developer/Applications/WorkbenchReplacement/build/Build/Intermediates.noindex/WorkbenchReplacement.build/Debug/WorkbenchReplacement.build/Objects-normal/arm64/WorkbenchReplacement-Swift.h","/Volumes/Developer/Applications/WorkbenchReplacement/build/Build/Intermediates.noindex/WorkbenchReplacement.build/Debug/WorkbenchReplacement.build/Objects-normal/arm64/WorkbenchReplacement.swiftdoc","/Volumes/Developer/Applications/WorkbenchReplacement/build/Build/Intermediates.noindex/WorkbenchReplacement.build/Debug/WorkbenchReplacement.build/Objects-normal/arm64/WorkbenchReplacement-OutputFileMap.json","/Volumes/Developer/Applications/WorkbenchReplacement/build/Build/Intermediates.noindex/WorkbenchReplacement.build/Debug/WorkbenchReplacement.build/Objects-normal/arm64/WorkbenchReplacement.LinkFileList","/Volumes/Developer/Applications/WorkbenchReplacement/build/Build/Intermediates.noindex/WorkbenchReplacement.build/Debug/WorkbenchReplacement.build/Objects-normal/arm64/WorkbenchReplacement.SwiftFileList","/Volumes/Developer/Applications/WorkbenchReplacement/build/Build/Intermediates.noindex/WorkbenchReplacement.build/Debug/WorkbenchReplacement.build/Objects-normal/arm64/WorkbenchReplacement_const_extract_protocols.json","/Volumes/Developer/Applications/WorkbenchReplacement/build/Build/Intermediates.noindex/WorkbenchReplacement.build/Debug/WorkbenchReplacement.build/WorkbenchReplacement-DebugDylibInstallName-normal-arm64.txt","/Volumes/Developer/Applications/WorkbenchReplacement/build/Build/Intermediates.noindex/WorkbenchReplacement.build/Debug/WorkbenchReplacement.build/WorkbenchReplacement-DebugDylibPath-normal-arm64.txt"],"outputs":[""]},"P0:::Gate target-WorkbenchReplacement-34f2007b1d0798da0712fb82baec90fb2878cdfbaa5fcb5fa30c8485a83dfb14--generated-headers":{"tool":"phony","inputs":[""],"outputs":[""]},"P0:::Gate target-WorkbenchReplacement-34f2007b1d0798da0712fb82baec90fb2878cdfbaa5fcb5fa30c8485a83dfb14--swift-generated-headers":{"tool":"phony","inputs":["","/Volumes/Developer/Applications/WorkbenchReplacement/build/Build/Intermediates.noindex/WorkbenchReplacement.build/Debug/WorkbenchReplacement.build/Objects-normal/arm64/WorkbenchReplacement Swift Compilation Requirements Finished","/Volumes/Developer/Applications/WorkbenchReplacement/build/Build/Intermediates.noindex/WorkbenchReplacement.build/Debug/WorkbenchReplacement.build/Objects-normal/arm64/WorkbenchReplacement.swiftmodule","/Volumes/Developer/Applications/WorkbenchReplacement/build/Build/Intermediates.noindex/WorkbenchReplacement.build/Debug/WorkbenchReplacement.build/Objects-normal/arm64/WorkbenchReplacement-dependency-scan.dia","/Volumes/Developer/Applications/WorkbenchReplacement/build/Build/Intermediates.noindex/WorkbenchReplacement.build/Debug/WorkbenchReplacement.build/Objects-normal/arm64/WorkbenchReplacement.swiftsourceinfo","/Volumes/Developer/Applications/WorkbenchReplacement/build/Build/Intermediates.noindex/WorkbenchReplacement.build/Debug/WorkbenchReplacement.build/Objects-normal/arm64/WorkbenchReplacement.abi.json","/Volumes/Developer/Applications/WorkbenchReplacement/build/Build/Intermediates.noindex/WorkbenchReplacement.build/Debug/WorkbenchReplacement.build/Objects-normal/arm64/WorkbenchReplacement-Swift.h","/Volumes/Developer/Applications/WorkbenchReplacement/build/Build/Intermediates.noindex/WorkbenchReplacement.build/Debug/WorkbenchReplacement.build/Objects-normal/arm64/WorkbenchReplacement.swiftdoc","/Volumes/Developer/Applications/WorkbenchReplacement/build/Build/Intermediates.noindex/WorkbenchReplacement.build/Debug/WorkbenchReplacement.build/DerivedSources/WorkbenchReplacement-Swift.h"],"outputs":[""]},"P0:target-WorkbenchReplacement-34f2007b1d0798da0712fb82baec90fb2878cdfbaa5fcb5fa30c8485a83dfb14-:Debug:CodeSign /Volumes/Developer/Applications/WorkbenchReplacement/build/Build/Products/Debug/WorkbenchReplacement.app":{"tool":"code-sign-task","description":"CodeSign /Volumes/Developer/Applications/WorkbenchReplacement/build/Build/Products/Debug/WorkbenchReplacement.app","inputs":["/Volumes/Developer/Applications/WorkbenchReplacement/WorkbenchReplacement/App.swift/","/Volumes/Developer/Applications/WorkbenchReplacement/WorkbenchReplacement/DatabaseModel.swift/","/Volumes/Developer/Applications/WorkbenchReplacement/WorkbenchReplacement/MainView.swift/","/Volumes/Developer/Applications/WorkbenchReplacement/WorkbenchReplacement/MySQLProtocol.swift/","/Volumes/Developer/Applications/WorkbenchReplacement/build/Build/Intermediates.noindex/WorkbenchReplacement.build/Debug/WorkbenchReplacement.build/WorkbenchReplacement.app.xcent/","/Volumes/Developer/Applications/WorkbenchReplacement/build/Build/Products/Debug/WorkbenchReplacement.app/Contents/Info.plist/","","","","","","",""],"outputs":["/Volumes/Developer/Applications/WorkbenchReplacement/build/Build/Products/Debug/WorkbenchReplacement.app/_CodeSignature","",""]},"P0:target-WorkbenchReplacement-34f2007b1d0798da0712fb82baec90fb2878cdfbaa5fcb5fa30c8485a83dfb14-:Debug:CodeSign /Volumes/Developer/Applications/WorkbenchReplacement/build/Build/Products/Debug/WorkbenchReplacement.app/Contents/MacOS/WorkbenchReplacement.debug.dylib":{"tool":"code-sign-task","description":"CodeSign /Volumes/Developer/Applications/WorkbenchReplacement/build/Build/Products/Debug/WorkbenchReplacement.app/Contents/MacOS/WorkbenchReplacement.debug.dylib","inputs":["/Volumes/Developer/Applications/WorkbenchReplacement/WorkbenchReplacement/App.swift/","/Volumes/Developer/Applications/WorkbenchReplacement/WorkbenchReplacement/DatabaseModel.swift/","/Volumes/Developer/Applications/WorkbenchReplacement/WorkbenchReplacement/MainView.swift/","/Volumes/Developer/Applications/WorkbenchReplacement/WorkbenchReplacement/MySQLProtocol.swift/","/Volumes/Developer/Applications/WorkbenchReplacement/build/Build/Products/Debug/WorkbenchReplacement.app/Contents/Info.plist/","","","",""],"outputs":[""]},"P0:target-WorkbenchReplacement-34f2007b1d0798da0712fb82baec90fb2878cdfbaa5fcb5fa30c8485a83dfb14-:Debug:CodeSign /Volumes/Developer/Applications/WorkbenchReplacement/build/Build/Products/Debug/WorkbenchReplacement.app/Contents/MacOS/__preview.dylib":{"tool":"code-sign-task","description":"CodeSign /Volumes/Developer/Applications/WorkbenchReplacement/build/Build/Products/Debug/WorkbenchReplacement.app/Contents/MacOS/__preview.dylib","inputs":["/Volumes/Developer/Applications/WorkbenchReplacement/WorkbenchReplacement/App.swift/","/Volumes/Developer/Applications/WorkbenchReplacement/WorkbenchReplacement/DatabaseModel.swift/","/Volumes/Developer/Applications/WorkbenchReplacement/WorkbenchReplacement/MainView.swift/","/Volumes/Developer/Applications/WorkbenchReplacement/WorkbenchReplacement/MySQLProtocol.swift/","/Volumes/Developer/Applications/WorkbenchReplacement/build/Build/Products/Debug/WorkbenchReplacement.app/Contents/Info.plist/","","","","",""],"outputs":[""]},"P0:target-WorkbenchReplacement-34f2007b1d0798da0712fb82baec90fb2878cdfbaa5fcb5fa30c8485a83dfb14-:Debug:CopySwiftLibs /Volumes/Developer/Applications/WorkbenchReplacement/build/Build/Products/Debug/WorkbenchReplacement.app":{"tool":"embed-swift-stdlib","description":"CopySwiftLibs /Volumes/Developer/Applications/WorkbenchReplacement/build/Build/Products/Debug/WorkbenchReplacement.app","inputs":["/Volumes/Developer/Applications/WorkbenchReplacement/build/Build/Products/Debug/WorkbenchReplacement.app/Contents/MacOS/WorkbenchReplacement.debug.dylib","","",""],"outputs":[""],"deps":"/Volumes/Developer/Applications/WorkbenchReplacement/build/Build/Intermediates.noindex/WorkbenchReplacement.build/Debug/WorkbenchReplacement.build/SwiftStdLibToolInputDependencies.dep"},"P0:target-WorkbenchReplacement-34f2007b1d0798da0712fb82baec90fb2878cdfbaa5fcb5fa30c8485a83dfb14-:Debug:ExtractAppIntentsMetadata":{"tool":"shell","description":"ExtractAppIntentsMetadata","inputs":["/Volumes/Developer/Applications/WorkbenchReplacement/WorkbenchReplacement/App.swift","/Volumes/Developer/Applications/WorkbenchReplacement/WorkbenchReplacement/MainView.swift","/Volumes/Developer/Applications/WorkbenchReplacement/WorkbenchReplacement/DatabaseModel.swift","/Volumes/Developer/Applications/WorkbenchReplacement/WorkbenchReplacement/MySQLProtocol.swift","/Volumes/Developer/Applications/WorkbenchReplacement/build/Build/Intermediates.noindex/WorkbenchReplacement.build/Debug/WorkbenchReplacement.build/Objects-normal/arm64/App.swiftconstvalues","/Volumes/Developer/Applications/WorkbenchReplacement/build/Build/Intermediates.noindex/WorkbenchReplacement.build/Debug/WorkbenchReplacement.build/Objects-normal/arm64/MainView.swiftconstvalues","/Volumes/Developer/Applications/WorkbenchReplacement/build/Build/Intermediates.noindex/WorkbenchReplacement.build/Debug/WorkbenchReplacement.build/Objects-normal/arm64/DatabaseModel.swiftconstvalues","/Volumes/Developer/Applications/WorkbenchReplacement/build/Build/Intermediates.noindex/WorkbenchReplacement.build/Debug/WorkbenchReplacement.build/Objects-normal/arm64/MySQLProtocol.swiftconstvalues","/Volumes/Developer/Applications/WorkbenchReplacement/build/Build/Products/Debug/WorkbenchReplacement.app/Contents/MacOS/WorkbenchReplacement","/Volumes/Developer/Applications/WorkbenchReplacement/build/Build/Intermediates.noindex/WorkbenchReplacement.build/Debug/WorkbenchReplacement.build/WorkbenchReplacement.DependencyMetadataFileList","/Volumes/Developer/Applications/WorkbenchReplacement/build/Build/Intermediates.noindex/WorkbenchReplacement.build/Debug/WorkbenchReplacement.build/WorkbenchReplacement.DependencyStaticMetadataFileList","/Volumes/Developer/Applications/WorkbenchReplacement/build/Build/Intermediates.noindex/WorkbenchReplacement.build/Debug/WorkbenchReplacement.build/Objects-normal/arm64/WorkbenchReplacement_dependency_info.dat","/Volumes/Developer/Applications/WorkbenchReplacement/build/Build/Intermediates.noindex/WorkbenchReplacement.build/Debug/WorkbenchReplacement.build/Objects-normal/arm64/WorkbenchReplacement.SwiftFileList","/Volumes/Developer/Applications/WorkbenchReplacement/build/Build/Intermediates.noindex/WorkbenchReplacement.build/Debug/WorkbenchReplacement.build/Objects-normal/arm64/WorkbenchReplacement.SwiftConstValuesFileList","","",""],"outputs":["/Volumes/Developer/Applications/WorkbenchReplacement/build/Build/Intermediates.noindex/WorkbenchReplacement.build/Debug/WorkbenchReplacement.build/Objects-normal/arm64/ExtractedAppShortcutsMetadata.stringsdata",""],"args":["/Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin/appintentsmetadataprocessor","--toolchain-dir","/var/run/com.apple.security.cryptexd/mnt/com.apple.MobileAsset.MetalToolchain-v27.1.266.1.3TPee2/Metal.xctoolchain","--module-name","WorkbenchReplacement","--sdk-root","/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX27.0.sdk","--xcode-version","27A9275","--platform-family","macOS","--deployment-target","15.0","--bundle-identifier","net.absdev.WorkbenchReplacement","--output","/Volumes/Developer/Applications/WorkbenchReplacement/build/Build/Products/Debug/WorkbenchReplacement.app/Contents/Resources","--target-triple","arm64-apple-macos15.0","--binary-file","/Volumes/Developer/Applications/WorkbenchReplacement/build/Build/Products/Debug/WorkbenchReplacement.app/Contents/MacOS/WorkbenchReplacement","--dependency-file","/Volumes/Developer/Applications/WorkbenchReplacement/build/Build/Intermediates.noindex/WorkbenchReplacement.build/Debug/WorkbenchReplacement.build/Objects-normal/arm64/WorkbenchReplacement_dependency_info.dat","--stringsdata-file","/Volumes/Developer/Applications/WorkbenchReplacement/build/Build/Intermediates.noindex/WorkbenchReplacement.build/Debug/WorkbenchReplacement.build/Objects-normal/arm64/ExtractedAppShortcutsMetadata.stringsdata","--source-file-list","/Volumes/Developer/Applications/WorkbenchReplacement/build/Build/Intermediates.noindex/WorkbenchReplacement.build/Debug/WorkbenchReplacement.build/Objects-normal/arm64/WorkbenchReplacement.SwiftFileList","--metadata-file-list","/Volumes/Developer/Applications/WorkbenchReplacement/build/Build/Intermediates.noindex/WorkbenchReplacement.build/Debug/WorkbenchReplacement.build/WorkbenchReplacement.DependencyMetadataFileList","--static-metadata-file-list","/Volumes/Developer/Applications/WorkbenchReplacement/build/Build/Intermediates.noindex/WorkbenchReplacement.build/Debug/WorkbenchReplacement.build/WorkbenchReplacement.DependencyStaticMetadataFileList","--swift-const-vals-list","/Volumes/Developer/Applications/WorkbenchReplacement/build/Build/Intermediates.noindex/WorkbenchReplacement.build/Debug/WorkbenchReplacement.build/Objects-normal/arm64/WorkbenchReplacement.SwiftConstValuesFileList","--compile-time-extraction","--deployment-aware-processing","--no-app-shortcuts-localization"],"env":{},"working-directory":"/Volumes/Developer/Applications/WorkbenchReplacement","signature":"e943b3d3a6f115dd17caa8739c15d434"},"P0:target-WorkbenchReplacement-34f2007b1d0798da0712fb82baec90fb2878cdfbaa5fcb5fa30c8485a83dfb14-:Debug:Gate target-WorkbenchReplacement-34f2007b1d0798da0712fb82baec90fb2878cdfbaa5fcb5fa30c8485a83dfb14--begin-compiling":{"tool":"phony","inputs":["","","","","","","","