From e2a8b2e1ed37ca15bf7a4d80a570a7e3d1b035f1 Mon Sep 17 00:00:00 2001 From: Charlie Gleason Date: Tue, 10 Mar 2026 13:52:47 -0700 Subject: [PATCH 01/11] Fix menu warnings, OKLCH precision, Xcode settings, and context menu shortcut MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - AppDelegate: use computed activation policy instead of .prohibited in applicationWillFinishLaunching to prevent NSMenu hierarchy warnings - NSColor+Lab: reduce OKLCH chroma precision from 4 to 3 decimal places - Xcode: enable ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS - StatusBarController: add ⌘Q keyboard shortcut to Quit menu item Co-Authored-By: Claude Sonnet 4.6 --- Pika.xcodeproj/project.pbxproj | 4 ++++ Pika/AppDelegate.swift | 2 +- Pika/Extensions/NSColor+Lab.swift | 8 ++++---- Pika/Services/StatusBarController.swift | 8 +++++--- 4 files changed, 14 insertions(+), 8 deletions(-) diff --git a/Pika.xcodeproj/project.pbxproj b/Pika.xcodeproj/project.pbxproj index c42a39a7..7f7896fa 100644 --- a/Pika.xcodeproj/project.pbxproj +++ b/Pika.xcodeproj/project.pbxproj @@ -1143,6 +1143,7 @@ isa = XCBuildConfiguration; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; CODE_SIGN_IDENTITY = "Apple Development"; "CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development"; @@ -1174,6 +1175,7 @@ isa = XCBuildConfiguration; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; CODE_SIGN_IDENTITY = "Apple Development"; "CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development"; @@ -1205,6 +1207,7 @@ isa = XCBuildConfiguration; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; CODE_SIGN_ENTITLEMENTS = Pika/Pika.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; @@ -1237,6 +1240,7 @@ isa = XCBuildConfiguration; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; CODE_SIGN_ENTITLEMENTS = Pika/Pika.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; diff --git a/Pika/AppDelegate.swift b/Pika/AppDelegate.swift index 6d440362..55875870 100644 --- a/Pika/AppDelegate.swift +++ b/Pika/AppDelegate.swift @@ -45,7 +45,7 @@ class AppDelegate: NSObject, NSApplicationDelegate { } func applicationWillFinishLaunching(_: Notification) { - NSApp.setActivationPolicy(.prohibited) + NSApp.setActivationPolicy(Defaults[.appMode] == .regular ? .regular : .accessory) NSAppleEventManager.shared().setEventHandler( URLSchemeHandler.shared, andSelector: #selector(URLSchemeHandler.handle(event:withReplyEvent:)), diff --git a/Pika/Extensions/NSColor+Lab.swift b/Pika/Extensions/NSColor+Lab.swift index 9a4d2d71..df64d721 100644 --- a/Pika/Extensions/NSColor+Lab.swift +++ b/Pika/Extensions/NSColor+Lab.swift @@ -121,16 +121,16 @@ extension NSColor { func toOklchString(style: CopyFormat = .css) -> String { let oklch = toOklchComponents() let l_val = round(oklch.l * 10000) / 100 - let c_val = round(oklch.c * 10000) / 10000 + let c_val = round(oklch.c * 1000) / 1000 let h_val = round(oklch.h * 100) / 100 switch style { case .css: - return String(format: "oklch(%.2f%% %.4f %.2f)", l_val, c_val, h_val) + return String(format: "oklch(%.2f%% %.3f %.2f)", l_val, c_val, h_val) case .design, .swiftUI: - return String(format: "oklch(%.2f, %.4f, %.2f)", l_val, c_val, h_val) + return String(format: "oklch(%.2f, %.3f, %.2f)", l_val, c_val, h_val) case .unformatted: - return String(format: "%.2f, %.4f, %.2f", l_val, c_val, h_val) + return String(format: "%.2f, %.3f, %.2f", l_val, c_val, h_val) } } } diff --git a/Pika/Services/StatusBarController.swift b/Pika/Services/StatusBarController.swift index 1e1c3834..c230ad77 100644 --- a/Pika/Services/StatusBarController.swift +++ b/Pika/Services/StatusBarController.swift @@ -63,11 +63,13 @@ class StatusBarController: NSObject, NSMenuDelegate { menu.addItem(preferences) menu.addItem(NSMenuItem.separator()) - menu.addItem( - withTitle: PikaText.textMenuQuit, + let quit = NSMenuItem( + title: PikaText.textMenuQuit, action: #selector(AppDelegate.terminatePika), - keyEquivalent: "" + keyEquivalent: "q" ) + quit.keyEquivalentModifierMask = .command + menu.addItem(quit) return menu } From 1539effdd79e3e3b533bc8e433fbb34ffc476c7c Mon Sep 17 00:00:00 2001 From: Charlie Gleason Date: Tue, 10 Mar 2026 14:04:41 -0700 Subject: [PATCH 02/11] Refactor OKLCH/LAB formatting, add CGFloat utility, and expand context menu MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Extract CGFloat.strippedDecimalString(maxDecimalPlaces:) to shared utility (CGFloat+Format.swift), replacing private helper in NSColor+Lab - Apply trailing-zero stripping to both toOklchString and toLabString - Add CGFloatFormatTests unit tests for the new utility - Add OKLCH-specific format tests for trailing zero behaviour - Rebuild status bar context menu with all keyboard shortcuts: Pick (⌘D/⇧⌘D), Copy (⌘C/⇧⌘C), System picker (⌘S/⇧⌘S), Swap (X), Preferences (⌘,), Quit (⌘Q) Co-Authored-By: Claude Sonnet 4.6 --- Pika.xcodeproj/project.pbxproj | 10 +++ Pika/Extensions/CGFloat+Format.swift | 14 ++++ Pika/Extensions/NSColor+Lab.swift | 24 +++---- Pika/Services/StatusBarController.swift | 90 ++++++++++++++++--------- PikaTests/CGFloatFormatTests.swift | 31 +++++++++ PikaTests/NSColorLabTests.swift | 36 ++++++++++ 6 files changed, 163 insertions(+), 42 deletions(-) create mode 100644 Pika/Extensions/CGFloat+Format.swift create mode 100644 PikaTests/CGFloatFormatTests.swift diff --git a/Pika.xcodeproj/project.pbxproj b/Pika.xcodeproj/project.pbxproj index 7f7896fa..9d76d35e 100644 --- a/Pika.xcodeproj/project.pbxproj +++ b/Pika.xcodeproj/project.pbxproj @@ -24,6 +24,7 @@ CC2000000000000000000701 /* WCAGComplianceTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC2000000000000000000700 /* WCAGComplianceTests.swift */; }; CC2000000000000000000801 /* APCAComplianceTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC2000000000000000000800 /* APCAComplianceTests.swift */; }; CC2000000000000000000901 /* NotificationNamesTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC2000000000000000000900 /* NotificationNamesTests.swift */; }; + CC2000000000000000000A01 /* CGFloatFormatTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC2000000000000000000A00 /* CGFloatFormatTests.swift */; }; 220D5E9428DB154300B6285E /* AppModeButtons.swift in Sources */ = {isa = PBXBuildFile; fileRef = 220D5E9328DB154300B6285E /* AppModeButtons.swift */; }; 220D5E9828DB158400B6285E /* AppModeToggleGroup.swift in Sources */ = {isa = PBXBuildFile; fileRef = 220D5E9728DB158400B6285E /* AppModeToggleGroup.swift */; }; @@ -50,6 +51,8 @@ CC000001000000000000AAF2 /* NSColor+Hex.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC000001000000000000AAF0 /* NSColor+Hex.swift */; }; CC000001000000000000AB01 /* NSColor+RGB.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC000001000000000000AB00 /* NSColor+RGB.swift */; }; CC000001000000000000AB02 /* NSColor+RGB.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC000001000000000000AB00 /* NSColor+RGB.swift */; }; + CC000001000000000000AB11 /* CGFloat+Format.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC000001000000000000AB10 /* CGFloat+Format.swift */; }; + CC000001000000000000AB12 /* CGFloat+Format.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC000001000000000000AB10 /* CGFloat+Format.swift */; }; EA0C525025AA729300AFF716 /* Visualisation.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA0C524F25AA729300AFF716 /* Visualisation.swift */; }; EA0C526025AB5A2B00AFF716 /* NavigationMenuItems.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA0C525F25AB5A2B00AFF716 /* NavigationMenuItems.swift */; }; EA0C526425AB5D1700AFF716 /* PikaWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA0C526325AB5D1700AFF716 /* PikaWindow.swift */; }; @@ -194,6 +197,7 @@ CC2000000000000000000700 /* WCAGComplianceTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WCAGComplianceTests.swift; sourceTree = ""; }; CC2000000000000000000800 /* APCAComplianceTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = APCAComplianceTests.swift; sourceTree = ""; }; CC2000000000000000000900 /* NotificationNamesTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationNamesTests.swift; sourceTree = ""; }; + CC2000000000000000000A00 /* CGFloatFormatTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CGFloatFormatTests.swift; sourceTree = ""; }; 220D5E9328DB154300B6285E /* AppModeButtons.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppModeButtons.swift; sourceTree = ""; }; 220D5E9728DB158400B6285E /* AppModeToggleGroup.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppModeToggleGroup.swift; sourceTree = ""; }; @@ -223,6 +227,7 @@ CC000001000000000000AAE0 /* NSColor+Luminance.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NSColor+Luminance.swift"; sourceTree = ""; }; CC000001000000000000AAF0 /* NSColor+Hex.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NSColor+Hex.swift"; sourceTree = ""; }; CC000001000000000000AB00 /* NSColor+RGB.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NSColor+RGB.swift"; sourceTree = ""; }; + CC000001000000000000AB10 /* CGFloat+Format.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CGFloat+Format.swift"; sourceTree = ""; }; EA0C524F25AA729300AFF716 /* Visualisation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Visualisation.swift; sourceTree = ""; }; EA0C525F25AB5A2B00AFF716 /* NavigationMenuItems.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigationMenuItems.swift; sourceTree = ""; }; EA0C526325AB5D1700AFF716 /* PikaWindow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PikaWindow.swift; sourceTree = ""; }; @@ -422,6 +427,7 @@ CC000001000000000000AAE0 /* NSColor+Luminance.swift */, CC000001000000000000AAF0 /* NSColor+Hex.swift */, CC000001000000000000AB00 /* NSColor+RGB.swift */, + CC000001000000000000AB10 /* CGFloat+Format.swift */, ); path = Extensions; sourceTree = ""; @@ -480,6 +486,7 @@ CC2000000000000000000700 /* WCAGComplianceTests.swift */, CC2000000000000000000800 /* APCAComplianceTests.swift */, CC2000000000000000000900 /* NotificationNamesTests.swift */, + CC2000000000000000000A00 /* CGFloatFormatTests.swift */, ); path = PikaTests; sourceTree = ""; @@ -875,6 +882,7 @@ CC000001000000000000AAE1 /* NSColor+Luminance.swift in Sources */, CC000001000000000000AAF1 /* NSColor+Hex.swift in Sources */, CC000001000000000000AB01 /* NSColor+RGB.swift in Sources */, + CC000001000000000000AB11 /* CGFloat+Format.swift in Sources */, EAA8AE1925B8EC070049299B /* KeyboardShortcutKey.swift in Sources */, EA0C526F25AB683400AFF716 /* EyedropperButton.swift in Sources */, EA72BB8425A5334B008205E7 /* MetalShader.metal in Sources */, @@ -918,6 +926,7 @@ CC000001000000000000AAE2 /* NSColor+Luminance.swift in Sources */, CC000001000000000000AAF2 /* NSColor+Hex.swift in Sources */, CC000001000000000000AB02 /* NSColor+RGB.swift in Sources */, + CC000001000000000000AB12 /* CGFloat+Format.swift in Sources */, EAE23DAE2D032A38005BB270 /* SplashTouchBar.swift in Sources */, EAE23DAF2D032A38005BB270 /* OverflowContentViewModifier.swift in Sources */, EAE23DB02D032A38005BB270 /* KeyboardShortcutGrid.swift in Sources */, @@ -981,6 +990,7 @@ CC2000000000000000000701 /* WCAGComplianceTests.swift in Sources */, CC2000000000000000000801 /* APCAComplianceTests.swift in Sources */, CC2000000000000000000901 /* NotificationNamesTests.swift in Sources */, + CC2000000000000000000A01 /* CGFloatFormatTests.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/Pika/Extensions/CGFloat+Format.swift b/Pika/Extensions/CGFloat+Format.swift new file mode 100644 index 00000000..ec1840aa --- /dev/null +++ b/Pika/Extensions/CGFloat+Format.swift @@ -0,0 +1,14 @@ +import CoreGraphics + +extension CGFloat { + /// Formats the value with up to `maxDecimalPlaces` decimal places, stripping trailing zeros. + /// e.g. 0.5000 → "0.5", 0.0000 → "0", 56.78 → "56.78" + func strippedDecimalString(maxDecimalPlaces: Int) -> String { + let formatted = String(format: "%.\(maxDecimalPlaces)f", self) + guard formatted.contains(".") else { return formatted } + var result = formatted + while result.hasSuffix("0") { result.removeLast() } + if result.hasSuffix(".") { result.removeLast() } + return result + } +} diff --git a/Pika/Extensions/NSColor+Lab.swift b/Pika/Extensions/NSColor+Lab.swift index df64d721..d3065b93 100644 --- a/Pika/Extensions/NSColor+Lab.swift +++ b/Pika/Extensions/NSColor+Lab.swift @@ -75,17 +75,17 @@ extension NSColor { func toLabString(style: CopyFormat = .css) -> String { let lab = toLabComponents() - let l_val = round(lab.l * 100) / 100 - let a_val = round(lab.a * 100) / 100 - let b_val = round(lab.b * 100) / 100 + let l_str = (round(lab.l * 100) / 100).strippedDecimalString(maxDecimalPlaces: 2) + let a_str = (round(lab.a * 100) / 100).strippedDecimalString(maxDecimalPlaces: 2) + let b_str = (round(lab.b * 100) / 100).strippedDecimalString(maxDecimalPlaces: 2) switch style { case .css: - return String(format: "lab(%.2f %.2f %.2f)", l_val, a_val, b_val) + return "lab(\(l_str) \(a_str) \(b_str))" case .design, .swiftUI: - return String(format: "lab(%.2f, %.2f, %.2f)", l_val, a_val, b_val) + return "lab(\(l_str), \(a_str), \(b_str))" case .unformatted: - return String(format: "%.2f,%.2f,%.2f", l_val, a_val, b_val) + return "\(l_str),\(a_str),\(b_str)" } } @@ -120,17 +120,17 @@ extension NSColor { func toOklchString(style: CopyFormat = .css) -> String { let oklch = toOklchComponents() - let l_val = round(oklch.l * 10000) / 100 - let c_val = round(oklch.c * 1000) / 1000 - let h_val = round(oklch.h * 100) / 100 + let l_str = (round(oklch.l * 10000) / 100).strippedDecimalString(maxDecimalPlaces: 2) + let c_str = (round(oklch.c * 10000) / 10000).strippedDecimalString(maxDecimalPlaces: 4) + let h_str = (round(oklch.h * 100) / 100).strippedDecimalString(maxDecimalPlaces: 2) switch style { case .css: - return String(format: "oklch(%.2f%% %.3f %.2f)", l_val, c_val, h_val) + return "oklch(\(l_str)% \(c_str) \(h_str))" case .design, .swiftUI: - return String(format: "oklch(%.2f, %.3f, %.2f)", l_val, c_val, h_val) + return "oklch(\(l_str), \(c_str), \(h_str))" case .unformatted: - return String(format: "%.2f, %.3f, %.2f", l_val, c_val, h_val) + return "\(l_str), \(c_str), \(h_str)" } } } diff --git a/Pika/Services/StatusBarController.swift b/Pika/Services/StatusBarController.swift index c230ad77..c043c446 100644 --- a/Pika/Services/StatusBarController.swift +++ b/Pika/Services/StatusBarController.swift @@ -34,42 +34,72 @@ class StatusBarController: NSObject, NSMenuDelegate { }.tieToLifetime(of: self) } + private func menuItem( + title: String, + action: Selector, + key: String, + modifiers: NSEvent.ModifierFlags = .command + ) -> NSMenuItem { + let item = NSMenuItem(title: title, action: action, keyEquivalent: key) + item.keyEquivalentModifierMask = modifiers + return item + } + private func buildMenu() -> NSMenu { let menu = NSMenu(title: "Status Bar Menu") menu.delegate = self - menu.addItem( - withTitle: PikaText.textMenuAbout, - action: #selector(AppDelegate.openAboutWindow), - keyEquivalent: "" - ) - menu.addItem( - withTitle: "\(PikaText.textMenuUpdates)...", - action: #selector(AppDelegate.checkForUpdates), - keyEquivalent: "" - ) - menu.addItem( - withTitle: PikaText.textMenuGitHubIssue, - action: #selector(AppDelegate.openGitHubIssue), - keyEquivalent: "" - ) - - let preferences = NSMenuItem( - title: "\(PikaText.textMenuPreferences)...", - action: #selector(AppDelegate.openPreferencesWindow), - keyEquivalent: "," - ) - preferences.keyEquivalentModifierMask = NSEvent.ModifierFlags.command - menu.addItem(preferences) + menu.addItem(menuItem(title: "\(PikaText.textPickForeground)...", + action: #selector(AppDelegate.triggerPickForeground), + key: "d")) + menu.addItem(menuItem(title: "\(PikaText.textPickBackground)...", + action: #selector(AppDelegate.triggerPickBackground), + key: "d", modifiers: [.command, .shift])) + + menu.addItem(NSMenuItem.separator()) + + menu.addItem(menuItem(title: PikaText.textCopyForeground, + action: #selector(AppDelegate.triggerCopyForeground), + key: "c")) + menu.addItem(menuItem(title: PikaText.textCopyBackground, + action: #selector(AppDelegate.triggerCopyBackground), + key: "c", modifiers: [.command, .shift])) + + menu.addItem(NSMenuItem.separator()) + + menu.addItem(menuItem(title: PikaText.textColorSystemPickerForeground, + action: #selector(AppDelegate.triggerSystemPickerForeground), + key: "s")) + menu.addItem(menuItem(title: PikaText.textColorSystemPickerBackground, + action: #selector(AppDelegate.triggerSystemPickerBackground), + key: "s", modifiers: [.command, .shift])) menu.addItem(NSMenuItem.separator()) - let quit = NSMenuItem( - title: PikaText.textMenuQuit, - action: #selector(AppDelegate.terminatePika), - keyEquivalent: "q" - ) - quit.keyEquivalentModifierMask = .command - menu.addItem(quit) + + menu.addItem(menuItem(title: PikaText.textColorSwapDetail, + action: #selector(AppDelegate.triggerSwap), + key: "x", modifiers: [])) + + menu.addItem(NSMenuItem.separator()) + + menu.addItem(menuItem(title: PikaText.textMenuAbout, + action: #selector(AppDelegate.openAboutWindow), + key: "")) + menu.addItem(menuItem(title: "\(PikaText.textMenuUpdates)...", + action: #selector(AppDelegate.checkForUpdates), + key: "")) + menu.addItem(menuItem(title: PikaText.textMenuGitHubIssue, + action: #selector(AppDelegate.openGitHubIssue), + key: "")) + menu.addItem(menuItem(title: "\(PikaText.textMenuPreferences)...", + action: #selector(AppDelegate.openPreferencesWindow), + key: ",")) + + menu.addItem(NSMenuItem.separator()) + + menu.addItem(menuItem(title: PikaText.textMenuQuit, + action: #selector(AppDelegate.terminatePika), + key: "q")) return menu } diff --git a/PikaTests/CGFloatFormatTests.swift b/PikaTests/CGFloatFormatTests.swift new file mode 100644 index 00000000..865bc659 --- /dev/null +++ b/PikaTests/CGFloatFormatTests.swift @@ -0,0 +1,31 @@ +@testable import Pika +import XCTest + +final class CGFloatFormatTests: XCTestCase { + func test_strippedDecimalString_trailingZerosRemoved() { + XCTAssertEqual(CGFloat(0.5000).strippedDecimalString(maxDecimalPlaces: 4), "0.5") + XCTAssertEqual(CGFloat(0.1000).strippedDecimalString(maxDecimalPlaces: 4), "0.1") + XCTAssertEqual(CGFloat(1.2300).strippedDecimalString(maxDecimalPlaces: 4), "1.23") + } + + func test_strippedDecimalString_allZerosAfterDecimalRemoved() { + XCTAssertEqual(CGFloat(0.0000).strippedDecimalString(maxDecimalPlaces: 4), "0") + XCTAssertEqual(CGFloat(1.0000).strippedDecimalString(maxDecimalPlaces: 4), "1") + XCTAssertEqual(CGFloat(50.00).strippedDecimalString(maxDecimalPlaces: 2), "50") + } + + func test_strippedDecimalString_significantDecimalsPreserved() { + XCTAssertEqual(CGFloat(0.1234).strippedDecimalString(maxDecimalPlaces: 4), "0.1234") + XCTAssertEqual(CGFloat(56.78).strippedDecimalString(maxDecimalPlaces: 2), "56.78") + } + + func test_strippedDecimalString_respectsMaxDecimalPlaces() { + // Should not show more than maxDecimalPlaces significant digits + XCTAssertEqual(CGFloat(0.12345).strippedDecimalString(maxDecimalPlaces: 3), "0.123") + } + + func test_strippedDecimalString_negativeValues() { + XCTAssertEqual(CGFloat(-1.5000).strippedDecimalString(maxDecimalPlaces: 4), "-1.5") + XCTAssertEqual(CGFloat(-12.3400).strippedDecimalString(maxDecimalPlaces: 4), "-12.34") + } +} diff --git a/PikaTests/NSColorLabTests.swift b/PikaTests/NSColorLabTests.swift index da542dac..09258f71 100644 --- a/PikaTests/NSColorLabTests.swift +++ b/PikaTests/NSColorLabTests.swift @@ -111,4 +111,40 @@ final class NSColorLabTests: XCTestCase { let color = NSColor(r: 50, g: 100, b: 200) XCTAssertFalse(color.toOklchString().isEmpty) } + + func test_toOklchString_achromatic_noTrailingZerosOnChroma() { + // Gray has chroma ≈ 0; the formatted chroma should not end in trailing zeros + let gray = NSColor(red: 0.5, green: 0.5, blue: 0.5, alpha: 1).usingColorSpace(.sRGB)! + let result = gray.toOklchString(style: .css) + // Extract chroma token: oklch(L% C H) + let tokens = result.dropFirst("oklch(".count).dropLast(1).components(separatedBy: " ") + XCTAssertEqual(tokens.count, 3) + let chroma = tokens[1] + XCTAssertFalse(chroma.contains(".") && chroma.hasSuffix("0"), + "Chroma '\(chroma)' has trailing zeros after decimal") + } + + func test_toOklchString_chromatic_stripsTrailingZerosWherePresent() { + // A color whose chroma rounds to a value with trailing zeros (e.g. exactly 0.1000) + // should not show them, while one with significant digits should keep them + let blue = NSColor(red: 0, green: 0, blue: 1, alpha: 1).usingColorSpace(.sRGB)! + let result = blue.toOklchString(style: .css) + XCTAssertTrue(result.hasPrefix("oklch(")) + // No token should end in a trailing zero after the decimal + let tokens = result.dropFirst("oklch(".count).dropLast(1).components(separatedBy: " ") + for token in tokens { + XCTAssertFalse(token.contains(".") && token.hasSuffix("0"), + "Token '\(token)' has trailing zeros after decimal") + } + } + + func test_toOklchString_unformatted_noTrailingZeros() { + let gray = NSColor(red: 0.5, green: 0.5, blue: 0.5, alpha: 1).usingColorSpace(.sRGB)! + let result = gray.toOklchString(style: .unformatted) + let tokens = result.components(separatedBy: ", ") + for token in tokens { + XCTAssertFalse(token.contains(".") && token.hasSuffix("0"), + "Token '\(token)' has trailing zeros after decimal") + } + } } From a81b72cf8c975ba57d8ab1844a04993aff8b9e90 Mon Sep 17 00:00:00 2001 From: Charlie Gleason Date: Tue, 10 Mar 2026 18:56:04 -0700 Subject: [PATCH 03/11] Fix menu warnings and add keyboard shortcuts to in-app gear menu - AppDelegate: remove setActivationPolicy from applicationWillFinishLaunching (setupAppMode in applicationDidFinishLaunching sets the correct policy); the early policy change was triggering NSMenu hierarchy consistency warnings - NavigationMenuItems: add .keyboardShortcut() to all action buttons so shortcuts are visible in the in-app gear menu dropdown Co-Authored-By: Claude Sonnet 4.6 --- Pika/AppDelegate.swift | 1 - Pika/Views/NavigationMenuItems.swift | 11 +++++++++++ 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/Pika/AppDelegate.swift b/Pika/AppDelegate.swift index 55875870..7b31c8f3 100644 --- a/Pika/AppDelegate.swift +++ b/Pika/AppDelegate.swift @@ -45,7 +45,6 @@ class AppDelegate: NSObject, NSApplicationDelegate { } func applicationWillFinishLaunching(_: Notification) { - NSApp.setActivationPolicy(Defaults[.appMode] == .regular ? .regular : .accessory) NSAppleEventManager.shared().setEventHandler( URLSchemeHandler.shared, andSelector: #selector(URLSchemeHandler.handle(event:withReplyEvent:)), diff --git a/Pika/Views/NavigationMenuItems.swift b/Pika/Views/NavigationMenuItems.swift index 52f4a519..d8da8426 100644 --- a/Pika/Views/NavigationMenuItems.swift +++ b/Pika/Views/NavigationMenuItems.swift @@ -23,10 +23,12 @@ struct NavigationMenuItems: View { Button("\(PikaText.textPickForeground)...", action: { NSApp.sendAction(#selector(AppDelegate.triggerPickForeground), to: nil, from: nil) }) + .keyboardShortcut("d", modifiers: .command) Button("\(PikaText.textPickBackground)...", action: { NSApp.sendAction(#selector(AppDelegate.triggerPickBackground), to: nil, from: nil) }) + .keyboardShortcut("d", modifiers: [.command, .shift]) VStack { Divider() @@ -35,10 +37,12 @@ struct NavigationMenuItems: View { Button(PikaText.textCopyForeground, action: { NSApp.sendAction(#selector(AppDelegate.triggerCopyForeground), to: nil, from: nil) }) + .keyboardShortcut("c", modifiers: .command) Button(PikaText.textCopyBackground, action: { NSApp.sendAction(#selector(AppDelegate.triggerCopyBackground), to: nil, from: nil) }) + .keyboardShortcut("c", modifiers: [.command, .shift]) VStack { Divider() @@ -47,10 +51,12 @@ struct NavigationMenuItems: View { Button(PikaText.textColorSystemPickerForeground, action: { NSApp.sendAction(#selector(AppDelegate.triggerSystemPickerForeground), to: nil, from: nil) }) + .keyboardShortcut("s", modifiers: .command) Button(PikaText.textColorSystemPickerBackground, action: { NSApp.sendAction(#selector(AppDelegate.triggerSystemPickerBackground), to: nil, from: nil) }) + .keyboardShortcut("s", modifiers: [.command, .shift]) VStack { Divider() @@ -72,14 +78,17 @@ struct NavigationMenuItems: View { Button(PikaText.textColorSwapDetail, action: { NSApp.sendAction(#selector(AppDelegate.triggerSwap), to: nil, from: nil) }) + .keyboardShortcut("x", modifiers: []) Button(PikaText.textColorUndo, action: { NSApp.sendAction(#selector(AppDelegate.triggerUndo), to: nil, from: nil) }) + .keyboardShortcut("z", modifiers: .command) Button(PikaText.textColorRedo, action: { NSApp.sendAction(#selector(AppDelegate.triggerRedo), to: nil, from: nil) }) + .keyboardShortcut("z", modifiers: [.command, .shift]) VStack { Divider() @@ -100,6 +109,7 @@ struct NavigationMenuItems: View { Button("\(PikaText.textMenuPreferences)...", action: { NSApp.sendAction(#selector(AppDelegate.openPreferencesWindow), to: nil, from: nil) }) + .keyboardShortcut(",", modifiers: .command) } VStack { @@ -109,6 +119,7 @@ struct NavigationMenuItems: View { Button(PikaText.textMenuQuit, action: { NSApplication.shared.terminate(self) }) + .keyboardShortcut("q", modifiers: .command) } } From 541d11c2578eaaec85d32cf83d8c63711d3b336a Mon Sep 17 00:00:00 2001 From: Charlie Gleason Date: Tue, 10 Mar 2026 19:00:42 -0700 Subject: [PATCH 04/11] Revert menu warning fix and in-app keyboard shortcuts MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Menu warnings: restoring .prohibited in applicationWillFinishLaunching — removing it didn't fix the warnings (macOS-internal NSMenu consistency check fires during storyboard menu setup, not fixable at this level). In-app keyboard shortcuts: reverting .keyboardShortcut() additions to NavigationMenuItems — SwiftUI registers these as global commands, which (1) requires two clicks for shortcuts to appear due to lazy evaluation, and (2) removes items from the Dock's contextual menu. Both are known SwiftUI on macOS limitations. Co-Authored-By: Claude Sonnet 4.6 --- Pika/AppDelegate.swift | 1 + Pika/Views/NavigationMenuItems.swift | 11 ----------- 2 files changed, 1 insertion(+), 11 deletions(-) diff --git a/Pika/AppDelegate.swift b/Pika/AppDelegate.swift index 7b31c8f3..6d440362 100644 --- a/Pika/AppDelegate.swift +++ b/Pika/AppDelegate.swift @@ -45,6 +45,7 @@ class AppDelegate: NSObject, NSApplicationDelegate { } func applicationWillFinishLaunching(_: Notification) { + NSApp.setActivationPolicy(.prohibited) NSAppleEventManager.shared().setEventHandler( URLSchemeHandler.shared, andSelector: #selector(URLSchemeHandler.handle(event:withReplyEvent:)), diff --git a/Pika/Views/NavigationMenuItems.swift b/Pika/Views/NavigationMenuItems.swift index d8da8426..52f4a519 100644 --- a/Pika/Views/NavigationMenuItems.swift +++ b/Pika/Views/NavigationMenuItems.swift @@ -23,12 +23,10 @@ struct NavigationMenuItems: View { Button("\(PikaText.textPickForeground)...", action: { NSApp.sendAction(#selector(AppDelegate.triggerPickForeground), to: nil, from: nil) }) - .keyboardShortcut("d", modifiers: .command) Button("\(PikaText.textPickBackground)...", action: { NSApp.sendAction(#selector(AppDelegate.triggerPickBackground), to: nil, from: nil) }) - .keyboardShortcut("d", modifiers: [.command, .shift]) VStack { Divider() @@ -37,12 +35,10 @@ struct NavigationMenuItems: View { Button(PikaText.textCopyForeground, action: { NSApp.sendAction(#selector(AppDelegate.triggerCopyForeground), to: nil, from: nil) }) - .keyboardShortcut("c", modifiers: .command) Button(PikaText.textCopyBackground, action: { NSApp.sendAction(#selector(AppDelegate.triggerCopyBackground), to: nil, from: nil) }) - .keyboardShortcut("c", modifiers: [.command, .shift]) VStack { Divider() @@ -51,12 +47,10 @@ struct NavigationMenuItems: View { Button(PikaText.textColorSystemPickerForeground, action: { NSApp.sendAction(#selector(AppDelegate.triggerSystemPickerForeground), to: nil, from: nil) }) - .keyboardShortcut("s", modifiers: .command) Button(PikaText.textColorSystemPickerBackground, action: { NSApp.sendAction(#selector(AppDelegate.triggerSystemPickerBackground), to: nil, from: nil) }) - .keyboardShortcut("s", modifiers: [.command, .shift]) VStack { Divider() @@ -78,17 +72,14 @@ struct NavigationMenuItems: View { Button(PikaText.textColorSwapDetail, action: { NSApp.sendAction(#selector(AppDelegate.triggerSwap), to: nil, from: nil) }) - .keyboardShortcut("x", modifiers: []) Button(PikaText.textColorUndo, action: { NSApp.sendAction(#selector(AppDelegate.triggerUndo), to: nil, from: nil) }) - .keyboardShortcut("z", modifiers: .command) Button(PikaText.textColorRedo, action: { NSApp.sendAction(#selector(AppDelegate.triggerRedo), to: nil, from: nil) }) - .keyboardShortcut("z", modifiers: [.command, .shift]) VStack { Divider() @@ -109,7 +100,6 @@ struct NavigationMenuItems: View { Button("\(PikaText.textMenuPreferences)...", action: { NSApp.sendAction(#selector(AppDelegate.openPreferencesWindow), to: nil, from: nil) }) - .keyboardShortcut(",", modifiers: .command) } VStack { @@ -119,7 +109,6 @@ struct NavigationMenuItems: View { Button(PikaText.textMenuQuit, action: { NSApplication.shared.terminate(self) }) - .keyboardShortcut("q", modifiers: .command) } } From e2323d89a4675120a8772074a2d5960287cbac19 Mon Sep 17 00:00:00 2001 From: Charlie Gleason Date: Tue, 10 Mar 2026 19:26:04 -0700 Subject: [PATCH 05/11] Fix PR review suggestions: Foundation import, Substring conversion, OpenGL float format - Add `import Foundation` to CGFloat+Format.swift for String(format:) - Normalize -0.0 to +0.0 in strippedDecimalString to avoid "-0" output - Fix toOpenGLString to ensure float notation (0.0 not 0) via glFloat helper - Convert Substring to String before calling components(separatedBy:) in tests Co-Authored-By: Claude Sonnet 4.6 --- Pika/Extensions/CGFloat+Format.swift | 5 ++++- Pika/Extensions/NSColor+Lab.swift | 17 +++++++++++------ PikaTests/NSColorLabTests.swift | 4 ++-- 3 files changed, 17 insertions(+), 9 deletions(-) diff --git a/Pika/Extensions/CGFloat+Format.swift b/Pika/Extensions/CGFloat+Format.swift index ec1840aa..aecc56d1 100644 --- a/Pika/Extensions/CGFloat+Format.swift +++ b/Pika/Extensions/CGFloat+Format.swift @@ -1,10 +1,13 @@ import CoreGraphics +import Foundation extension CGFloat { /// Formats the value with up to `maxDecimalPlaces` decimal places, stripping trailing zeros. /// e.g. 0.5000 → "0.5", 0.0000 → "0", 56.78 → "56.78" func strippedDecimalString(maxDecimalPlaces: Int) -> String { - let formatted = String(format: "%.\(maxDecimalPlaces)f", self) + // Normalize -0.0 to +0.0 to avoid producing "-0" in output. + let value: CGFloat = self == 0 ? 0 : self + let formatted = String(format: "%.\(maxDecimalPlaces)f", value) guard formatted.contains(".") else { return formatted } var result = formatted while result.hasSuffix("0") { result.removeLast() } diff --git a/Pika/Extensions/NSColor+Lab.swift b/Pika/Extensions/NSColor+Lab.swift index d3065b93..1f5f0817 100644 --- a/Pika/Extensions/NSColor+Lab.swift +++ b/Pika/Extensions/NSColor+Lab.swift @@ -20,16 +20,21 @@ extension NSColor { func toOpenGLString(style: CopyFormat = .css) -> String { let RGB = toRGBAComponents() - let formatString: NSString + func glFloat(_ val: CGFloat) -> String { + let str = String(format: "%.5g", val) + return str.contains(".") ? str : "\(str).0" + } + + let r_str = glFloat(RGB.r) + let g_str = glFloat(RGB.g) + let b_str = glFloat(RGB.b) + switch style { case .css, .design, .swiftUI: - formatString = "rgba(%.5g, %.5g, %.5g, 1.0)" + return "rgba(\(r_str), \(g_str), \(b_str), 1.0)" case .unformatted: - formatString = "%.5g, %.5g, %.5g, 1.0" + return "\(r_str), \(g_str), \(b_str), 1.0" } - - let openGLString = NSString(format: formatString, RGB.r, RGB.g, RGB.b) - return openGLString as String } /* diff --git a/PikaTests/NSColorLabTests.swift b/PikaTests/NSColorLabTests.swift index 09258f71..444eae2e 100644 --- a/PikaTests/NSColorLabTests.swift +++ b/PikaTests/NSColorLabTests.swift @@ -117,7 +117,7 @@ final class NSColorLabTests: XCTestCase { let gray = NSColor(red: 0.5, green: 0.5, blue: 0.5, alpha: 1).usingColorSpace(.sRGB)! let result = gray.toOklchString(style: .css) // Extract chroma token: oklch(L% C H) - let tokens = result.dropFirst("oklch(".count).dropLast(1).components(separatedBy: " ") + let tokens = String(result.dropFirst("oklch(".count).dropLast(1)).components(separatedBy: " ") XCTAssertEqual(tokens.count, 3) let chroma = tokens[1] XCTAssertFalse(chroma.contains(".") && chroma.hasSuffix("0"), @@ -131,7 +131,7 @@ final class NSColorLabTests: XCTestCase { let result = blue.toOklchString(style: .css) XCTAssertTrue(result.hasPrefix("oklch(")) // No token should end in a trailing zero after the decimal - let tokens = result.dropFirst("oklch(".count).dropLast(1).components(separatedBy: " ") + let tokens = String(result.dropFirst("oklch(".count).dropLast(1)).components(separatedBy: " ") for token in tokens { XCTAssertFalse(token.contains(".") && token.hasSuffix("0"), "Token '\(token)' has trailing zeros after decimal") From 1eb67f86611e9a7a753437352ee28189201b674d Mon Sep 17 00:00:00 2001 From: Charlie Gleason Date: Tue, 10 Mar 2026 19:27:34 -0700 Subject: [PATCH 06/11] Use red/green/blue names in toOpenGLString to match file style Co-Authored-By: Claude Sonnet 4.6 --- Pika/Extensions/NSColor+Lab.swift | 17 +++++++---------- 1 file changed, 7 insertions(+), 10 deletions(-) diff --git a/Pika/Extensions/NSColor+Lab.swift b/Pika/Extensions/NSColor+Lab.swift index 1f5f0817..811e14e1 100644 --- a/Pika/Extensions/NSColor+Lab.swift +++ b/Pika/Extensions/NSColor+Lab.swift @@ -20,20 +20,17 @@ extension NSColor { func toOpenGLString(style: CopyFormat = .css) -> String { let RGB = toRGBAComponents() - func glFloat(_ val: CGFloat) -> String { - let str = String(format: "%.5g", val) - return str.contains(".") ? str : "\(str).0" - } - - let r_str = glFloat(RGB.r) - let g_str = glFloat(RGB.g) - let b_str = glFloat(RGB.b) + // %.5g strips trailing zeros but drops the decimal entirely for whole numbers, + // so append ".0" when there is no decimal point (e.g. 0 → "0.0", 1 → "1.0"). + let red = { let s = String(format: "%.5g", RGB.r); return s.contains(".") ? s : "\(s).0" }() + let green = { let s = String(format: "%.5g", RGB.g); return s.contains(".") ? s : "\(s).0" }() + let blue = { let s = String(format: "%.5g", RGB.b); return s.contains(".") ? s : "\(s).0" }() switch style { case .css, .design, .swiftUI: - return "rgba(\(r_str), \(g_str), \(b_str), 1.0)" + return "rgba(\(red), \(green), \(blue), 1.0)" case .unformatted: - return "\(r_str), \(g_str), \(b_str), 1.0" + return "\(red), \(green), \(blue), 1.0" } } From 57265e2f09f139e57e114bb2d0340b3feb70c81c Mon Sep 17 00:00:00 2001 From: Charlie Gleason Date: Tue, 10 Mar 2026 19:41:19 -0700 Subject: [PATCH 07/11] Fix large tuple violations and clean up swiftlint comments - Replace 3- and 4-member return tuples with named structs (RGBAComponents, HSBComponents, HSLComponents, XYZComponents, LabComponents, OklchComponents) to resolve large_tuple violations without affecting any call sites - Remove redundant trailing_comma disable in Constants.swift (already globally disabled in .swiftlint.yml) - Rename loop variable i to idx in ClosestVector.swift to remove identifier_name disable/enable block - Add explanatory comments to remaining identifier_name and line_length disables that cannot be removed (mathematical single-letter names, unavoidably long strings) Co-Authored-By: Claude Sonnet 4.6 --- Pika/Constants/Constants.swift | 5 +++-- Pika/Extensions/APCACompliance.swift | 2 ++ Pika/Extensions/NSColor+HSL.swift | 21 +++++++++++++-------- Pika/Extensions/NSColor+Lab.swift | 18 ++++++++++++------ Pika/Extensions/NSColor+RGB.swift | 10 ++++++---- Pika/Services/ClosestVector.swift | 8 +++----- Pika/Services/Exporter.swift | 2 ++ 7 files changed, 41 insertions(+), 25 deletions(-) diff --git a/Pika/Constants/Constants.swift b/Pika/Constants/Constants.swift index b94053b4..0956a27d 100644 --- a/Pika/Constants/Constants.swift +++ b/Pika/Constants/Constants.swift @@ -2,8 +2,8 @@ import Defaults import KeyboardShortcuts import SwiftUI -// swiftlint:disable trailing_comma // swiftlint:disable line_length +// trailing_comma is already disabled globally in .swiftlint.yml extension KeyboardShortcuts.Name { static let togglePika = Self("togglePika") @@ -305,5 +305,6 @@ enum PikaText { static let textDuration = NSLocalizedString("preferences.overlay.duration", comment: "Duration:") } -// swiftlint:enable trailing_comma // swiftlint:enable line_length +// line_length is disabled above because NSLocalizedString comment strings on lines 100 and 102 +// exceed 120 characters and cannot be shortened without losing meaning for localizers. diff --git a/Pika/Extensions/APCACompliance.swift b/Pika/Extensions/APCACompliance.swift index 4024905c..37bdf7a7 100644 --- a/Pika/Extensions/APCACompliance.swift +++ b/Pika/Extensions/APCACompliance.swift @@ -1,6 +1,8 @@ import Cocoa // swiftlint:disable identifier_name +// identifier_name is disabled because the APCA algorithm uses conventional single-letter +// variable names (c, r, g, b, y, s) from the specification that would be misleading if renamed. extension NSColor { struct APCA { diff --git a/Pika/Extensions/NSColor+HSL.swift b/Pika/Extensions/NSColor+HSL.swift index 2ab798c0..3f13f5d9 100644 --- a/Pika/Extensions/NSColor+HSL.swift +++ b/Pika/Extensions/NSColor+HSL.swift @@ -2,13 +2,18 @@ import Cocoa import Defaults // swiftlint:disable identifier_name +// identifier_name is disabled because color science math uses conventional single-letter +// variable names (h, s, b, l, r, g) that would be misleading if renamed. + +struct HSBComponents { let h, s, b: CGFloat } +struct HSLComponents { let h, s, l: CGFloat } extension NSColor { /* * HSB */ - public final func toHSBComponents() -> (h: CGFloat, s: CGFloat, b: CGFloat) { + public final func toHSBComponents() -> HSBComponents { var h: CGFloat = 0.0 var s: CGFloat = 0.0 var b: CGFloat = 0.0 @@ -18,15 +23,15 @@ extension NSColor { } if toHexString() == NSColor.black.toHexString() { - return (0.0, 0.0, 0.0) + return HSBComponents(h: 0.0, s: 0.0, b: 0.0) } else if toHexString() == NSColor.white.toHexString() { - return (0.0, 0.0, 1.0) + return HSBComponents(h: 0.0, s: 0.0, b: 1.0) } rgbaColor.getHue(&h, saturation: &s, brightness: &b, alpha: nil) h = h.truncatingRemainder(dividingBy: 1.0) - return (h: h, s: s, b: b) + return HSBComponents(h: h, s: s, b: b) } /** @@ -58,7 +63,7 @@ extension NSColor { * HSL */ - public final func toHSLComponents() -> (h: CGFloat, s: CGFloat, l: CGFloat) { + public final func toHSLComponents() -> HSLComponents { var h: CGFloat = 0.0 var s: CGFloat = 0.0 var l: CGFloat = 0.0 @@ -69,9 +74,9 @@ extension NSColor { let b = RGB.b if toHexString() == NSColor.black.toHexString() { - return (0.0, 0.0, 0.0) + return HSLComponents(h: 0.0, s: 0.0, l: 0.0) } else if toHexString() == NSColor.white.toHexString() { - return (0.0, 0.0, 1.0) + return HSLComponents(h: 0.0, s: 0.0, l: 1.0) } let min = Swift.min(Swift.min(r, g), b) @@ -106,7 +111,7 @@ extension NSColor { s = delta / (2 - max - min) } - return (h: h, s: s, l: l) + return HSLComponents(h: h, s: s, l: l) } /** diff --git a/Pika/Extensions/NSColor+Lab.swift b/Pika/Extensions/NSColor+Lab.swift index 811e14e1..de44f5c9 100644 --- a/Pika/Extensions/NSColor+Lab.swift +++ b/Pika/Extensions/NSColor+Lab.swift @@ -1,6 +1,12 @@ import Cocoa // swiftlint:disable identifier_name +// identifier_name is disabled because color science math uses conventional single-letter +// variable names (x, y, z, l, a, b, c, h, L, C, H) that would be misleading if renamed. + +private struct XYZComponents { let x, y, z: CGFloat } +struct LabComponents { let l, a, b: CGFloat } +struct OklchComponents { let l, c, h: CGFloat } extension NSColor { // Shared linearization for sRGB components used by both LAB and OKLCH. @@ -38,7 +44,7 @@ extension NSColor { * CIE-LAB */ - private func toXYZComponents() -> (x: CGFloat, y: CGFloat, z: CGFloat) { + private func toXYZComponents() -> XYZComponents { let srgb = toRGBAComponents(in: .sRGB) let r_lin = linearizeSRGB(srgb.r) let g_lin = linearizeSRGB(srgb.g) @@ -48,10 +54,10 @@ extension NSColor { let y = r_lin * 0.2126729 + g_lin * 0.7151522 + b_lin * 0.0721750 let z = r_lin * 0.0193339 + g_lin * 0.1191920 + b_lin * 0.9503041 - return (x: x, y: y, z: z) + return XYZComponents(x: x, y: y, z: z) } - func toLabComponents() -> (l: CGFloat, a: CGFloat, b: CGFloat) { + func toLabComponents() -> LabComponents { let xyz = toXYZComponents() // D65 Reference White @@ -72,7 +78,7 @@ extension NSColor { let a_star = 500.0 * (f(xyz.x / Xn) - f(xyz.y / Yn)) let b_star = 200.0 * (f(xyz.y / Yn) - f(xyz.z / Zn)) - return (l: L_star, a: a_star, b: b_star) + return LabComponents(l: L_star, a: a_star, b: b_star) } func toLabString(style: CopyFormat = .css) -> String { @@ -95,7 +101,7 @@ extension NSColor { * OKLCH */ - func toOklchComponents() -> (l: CGFloat, c: CGFloat, h: CGFloat) { + func toOklchComponents() -> OklchComponents { let srgb = toRGBAComponents(in: .sRGB) let r_lin = linearizeSRGB(srgb.r) let g_lin = linearizeSRGB(srgb.g) @@ -117,7 +123,7 @@ extension NSColor { var H = atan2(b, a) * 180.0 / .pi if H < 0 { H += 360.0 } - return (l: L, c: C, h: H) + return OklchComponents(l: L, c: C, h: H) } func toOklchString(style: CopyFormat = .css) -> String { diff --git a/Pika/Extensions/NSColor+RGB.swift b/Pika/Extensions/NSColor+RGB.swift index a0ae5c2b..7ce88d76 100644 --- a/Pika/Extensions/NSColor+RGB.swift +++ b/Pika/Extensions/NSColor+RGB.swift @@ -3,11 +3,13 @@ import Defaults import SwiftUI // swiftlint:disable identifier_name +// identifier_name is disabled because color component names (r, g, b, a) are standard +// single-letter identifiers used throughout the color APIs. + +struct RGBAComponents { let r, g, b, a: CGFloat } extension NSColor { - final func toRGBAComponents(in colorSpace: NSColorSpace = Defaults[.colorSpace]) - -> (r: CGFloat, g: CGFloat, b: CGFloat, a: CGFloat) - { + final func toRGBAComponents(in colorSpace: NSColorSpace = Defaults[.colorSpace]) -> RGBAComponents { var r: CGFloat = 0, g: CGFloat = 0, b: CGFloat = 0, a: CGFloat = 0 guard let rgbaColor = usingColorSpace(colorSpace) else { @@ -16,7 +18,7 @@ extension NSColor { rgbaColor.getRed(&r, green: &g, blue: &b, alpha: &a) - return (r, g, b, a) + return RGBAComponents(r: r, g: g, b: b, a: a) } func toRGBString(style: CopyFormat = .css) -> String { diff --git a/Pika/Services/ClosestVector.swift b/Pika/Services/ClosestVector.swift index 6f178dda..1504c674 100644 --- a/Pika/Services/ClosestVector.swift +++ b/Pika/Services/ClosestVector.swift @@ -22,15 +22,13 @@ public class ClosestVector { var minDistance = Int.max var index = 0 - // swiftlint:disable identifier_name - for i in 0 ..< list.count { - let distance = diff(colorArr, list[i]) + for idx in 0 ..< list.count { + let distance = diff(colorArr, list[idx]) if distance < minDistance { minDistance = distance - index = i + index = idx } } - // swiftlint:enable identifier_name return index } diff --git a/Pika/Services/Exporter.swift b/Pika/Services/Exporter.swift index 14b9db57..c2024969 100644 --- a/Pika/Services/Exporter.swift +++ b/Pika/Services/Exporter.swift @@ -22,6 +22,8 @@ class Exporter { let failMessage = PikaText.textColorFail // swiftlint:disable line_length + // line_length is disabled because the export format strings are inherently long + // and cannot be broken across lines without changing the output format. return """ \(PikaText.textColorForeground): Hex \(foregroundHex) · RGB \(foregroundRgb) · HSB \(foregroundHsb) · HSL \(foregroundHsl) · OpenGL \(foregroundOpengl) · OKLCH \(foregroundOklch) \(PikaText.textColorBackground): Hex \(backgroundHex) · RGB \(backgroundRgb) · HSB \(backgroundHsb) · HSL \(backgroundHsl) · OpenGL \(backgroundOpengl) · OKLCH \(backgroundOklch) From 65e7cfe407243b8089b7eae580cf1a7577d3e5df Mon Sep 17 00:00:00 2001 From: Charlie Gleason Date: Tue, 10 Mar 2026 19:47:57 -0700 Subject: [PATCH 08/11] Fix all remaining swiftlint violations MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit identifier_name: - NSColor+Hex: rename x → value in roundToHex - NSColor+Init: rename c → char, R/G/B → red/green/blue; targeted disable for r/g/b/a params (public API labels NSColor(r:g:b:a:) that cannot be renamed) - NSColor+Luminance: rename v → val, c → component, L1/L2 → lum1/lum2 line_length: - Break long SwiftUI call sites across lines in ColorPickOverlay, ComplianceToggleGroup, ComplianceButtons, KeyboardShortcutGrid, ContentView, Eyedroppers, ColorPickOverlayWindow function_body_length: - ColorPickOverlayWindow: extract makeInfoPanel, makeCrosshairPanel, positionPanels helpers cyclomatic_complexity: - URLSchemeHandler: delegate each URL action to private handleFormat/Pick/System/Copy methods Co-Authored-By: Claude Sonnet 4.6 --- Pika/Extensions/NSColor+Hex.swift | 6 +- Pika/Extensions/NSColor+Init.swift | 14 +- Pika/Extensions/NSColor+Luminance.swift | 22 +-- Pika/Services/ColorPickOverlayWindow.swift | 159 +++++++++++---------- Pika/Services/Eyedroppers.swift | 3 +- Pika/Services/URLSchemeHandler.swift | 74 +++++----- Pika/Views/ColorPickOverlay.swift | 16 ++- Pika/Views/ComplianceButtons.swift | 5 +- Pika/Views/ComplianceToggleGroup.swift | 44 ++++-- Pika/Views/ContentView.swift | 8 +- Pika/Views/KeyboardShortcutGrid.swift | 17 ++- 11 files changed, 217 insertions(+), 151 deletions(-) diff --git a/Pika/Extensions/NSColor+Hex.swift b/Pika/Extensions/NSColor+Hex.swift index 1fc35abd..782ca2b2 100644 --- a/Pika/Extensions/NSColor+Hex.swift +++ b/Pika/Extensions/NSColor+Hex.swift @@ -1,9 +1,9 @@ import Cocoa extension NSColor { - func roundToHex(_ x: CGFloat) -> UInt32 { - guard x > 0 else { return 0 } - let rounded: CGFloat = round(x * 255.0) + func roundToHex(_ value: CGFloat) -> UInt32 { + guard value > 0 else { return 0 } + let rounded: CGFloat = round(value * 255.0) return UInt32(rounded) } diff --git a/Pika/Extensions/NSColor+Init.swift b/Pika/Extensions/NSColor+Init.swift index 52964d19..7122fade 100644 --- a/Pika/Extensions/NSColor+Init.swift +++ b/Pika/Extensions/NSColor+Init.swift @@ -1,6 +1,8 @@ import Cocoa extension NSColor { + // r, g, b, a are the public parameter labels for this init (NSColor(r:g:b:a:)) and cannot be renamed. + // swiftlint:disable:next identifier_name convenience init(r: CGFloat, g: CGFloat, b: CGFloat, a: CGFloat = 1) { if (r > 1) || (g > 1) || (b > 1) { self.init(red: r / 255, green: g / 255, blue: b / 255, alpha: a) @@ -25,8 +27,8 @@ extension NSColor { if hex.count == 3 { let tmp = hex hex = "" - for c in tmp { - hex += String([c, c]) + for char in tmp { + hex += String([char, char]) } } @@ -34,9 +36,9 @@ extension NSColor { var rgb: UInt64 = 0 scanner.scanHexInt64(&rgb) - let R = CGFloat((rgb >> 16) & 0xFF) / 255 - let G = CGFloat((rgb >> 8) & 0xFF) / 255 - let B = CGFloat(rgb & 0xFF) / 255 - self.init(red: R, green: G, blue: B, alpha: alpha) + let red = CGFloat((rgb >> 16) & 0xFF) / 255 + let green = CGFloat((rgb >> 8) & 0xFF) / 255 + let blue = CGFloat(rgb & 0xFF) / 255 + self.init(red: red, green: green, blue: blue, alpha: alpha) } } diff --git a/Pika/Extensions/NSColor+Luminance.swift b/Pika/Extensions/NSColor+Luminance.swift index 548df5d6..35e72ce4 100644 --- a/Pika/Extensions/NSColor+Luminance.swift +++ b/Pika/Extensions/NSColor+Luminance.swift @@ -1,29 +1,31 @@ import Cocoa extension NSColor { - func clip(_ v: T, _ minimum: T, _ maximum: T) -> T { - max(min(v, maximum), minimum) + func clip(_ val: T, _ minimum: T, _ maximum: T) -> T { + max(min(val, maximum), minimum) } var luminance: CGFloat { let rgba = toRGBAComponents(in: .extendedSRGB) - func lumHelper(c: CGFloat) -> CGFloat { - (c < 0.03928) ? (c / 12.92) : pow((c + 0.055) / 1.055, 2.4) + func lumHelper(component: CGFloat) -> CGFloat { + (component < 0.03928) ? (component / 12.92) : pow((component + 0.055) / 1.055, 2.4) } - let result = 0.2126 * lumHelper(c: rgba.r) + 0.7152 * lumHelper(c: rgba.g) + 0.0722 * lumHelper(c: rgba.b) + let result = 0.2126 * lumHelper(component: rgba.r) + + 0.7152 * lumHelper(component: rgba.g) + + 0.0722 * lumHelper(component: rgba.b) return max(.zero, result) } func contrastRatio(with color: NSColor) -> CGFloat { - let L1 = luminance - let L2 = color.luminance + let lum1 = luminance + let lum2 = color.luminance - if L1 < L2 { - return (L2 + 0.05) / (L1 + 0.05) + if lum1 < lum2 { + return (lum2 + 0.05) / (lum1 + 0.05) } else { - return (L1 + 0.05) / (L2 + 0.05) + return (lum1 + 0.05) / (lum2 + 0.05) } } diff --git a/Pika/Services/ColorPickOverlayWindow.swift b/Pika/Services/ColorPickOverlayWindow.swift index 5aff2ac8..f6d1c647 100644 --- a/Pika/Services/ColorPickOverlayWindow.swift +++ b/Pika/Services/ColorPickOverlayWindow.swift @@ -19,105 +19,116 @@ class ColorPickOverlayWindow { let viewModel = ColorPickOverlayViewModel() self.viewModel = viewModel - // Create info panel with color text + let (infoPanel, panelSize) = makeInfoPanel(colorText: colorText, pickedColor: pickedColor, viewModel: viewModel) + let crosshairSize: CGFloat = 20 + let crosshairPanel = makeCrosshairPanel(pickedColor: pickedColor, viewModel: viewModel, size: crosshairSize) + + positionPanels( + infoPanel: infoPanel, crosshairPanel: crosshairPanel, + near: cursorPosition, panelSize: panelSize, crosshairSize: crosshairSize) + + infoPanel.orderFrontRegardless() + crosshairPanel.orderFrontRegardless() + + self.infoPanel = infoPanel + self.crosshairPanel = crosshairPanel + + dismissTimer = Timer.scheduledTimer(withTimeInterval: duration, repeats: false) { [weak self] _ in + self?.dismiss() + } + } + + private func makeInfoPanel( + colorText: String, pickedColor: NSColor, viewModel: ColorPickOverlayViewModel + ) -> (NSPanel, CGSize) { let contentView = ColorPickOverlay(colorText: colorText, pickedColor: pickedColor, viewModel: viewModel) let hostingView = NSHostingView(rootView: contentView) hostingView.invalidateIntrinsicContentSize() let intrinsicSize = hostingView.intrinsicContentSize - let panelWidth = intrinsicSize.width - let panelHeight = intrinsicSize.height - let infoPanel = NSPanel( - contentRect: NSRect(x: 0, y: 0, width: panelWidth, height: panelHeight), + let panel = NSPanel( + contentRect: NSRect(x: 0, y: 0, width: intrinsicSize.width, height: intrinsicSize.height), styleMask: [.nonactivatingPanel, .hudWindow], backing: .buffered, defer: false ) + panel.isFloatingPanel = true + panel.level = .popUpMenu + panel.backgroundColor = .clear + panel.isOpaque = false + panel.hasShadow = true + panel.titlebarAppearsTransparent = true + panel.titleVisibility = .hidden + panel.isMovable = false + panel.collectionBehavior = [.canJoinAllSpaces, .fullScreenAuxiliary] + panel.contentView = hostingView + + return (panel, intrinsicSize) + } - infoPanel.isFloatingPanel = true - infoPanel.level = .popUpMenu - infoPanel.backgroundColor = .clear - infoPanel.isOpaque = false - infoPanel.hasShadow = true - infoPanel.titlebarAppearsTransparent = true - infoPanel.titleVisibility = .hidden - infoPanel.isMovable = false - infoPanel.collectionBehavior = [.canJoinAllSpaces, .fullScreenAuxiliary] - - infoPanel.contentView = hostingView - - // Create crosshair panel at cursor position + private func makeCrosshairPanel( + pickedColor: NSColor, viewModel: ColorPickOverlayViewModel, size: CGFloat + ) -> NSPanel { let crosshairView = ColorPickCrosshair(pickedColor: pickedColor, viewModel: viewModel) - let crosshairHostingView = NSHostingView(rootView: crosshairView) + let hostingView = NSHostingView(rootView: crosshairView) - let crosshairSize: CGFloat = 20 - let crosshairPanel = NSPanel( - contentRect: NSRect(x: 0, y: 0, width: crosshairSize, height: crosshairSize), + let panel = NSPanel( + contentRect: NSRect(x: 0, y: 0, width: size, height: size), styleMask: [.nonactivatingPanel, .borderless], backing: .buffered, defer: false ) + panel.isFloatingPanel = true + panel.level = .popUpMenu + panel.backgroundColor = .clear + panel.isOpaque = false + panel.hasShadow = false + panel.titlebarAppearsTransparent = true + panel.titleVisibility = .hidden + panel.isMovable = false + panel.collectionBehavior = [.canJoinAllSpaces, .fullScreenAuxiliary] + panel.contentView = hostingView + + return panel + } - crosshairPanel.isFloatingPanel = true - crosshairPanel.level = .popUpMenu - crosshairPanel.backgroundColor = .clear - crosshairPanel.isOpaque = false - crosshairPanel.hasShadow = false - crosshairPanel.titlebarAppearsTransparent = true - crosshairPanel.titleVisibility = .hidden - crosshairPanel.isMovable = false - crosshairPanel.collectionBehavior = [.canJoinAllSpaces, .fullScreenAuxiliary] - - crosshairPanel.contentView = crosshairHostingView - + private func positionPanels( + infoPanel: NSPanel, crosshairPanel: NSPanel, + near cursorPosition: NSPoint, panelSize: CGSize, crosshairSize: CGFloat + ) { let screen = NSScreen.screens.first { NSMouseInRect(cursorPosition, $0.frame, false) } ?? NSScreen.main + guard let screen = screen else { return } - if let screen = screen { - let screenFrame = screen.visibleFrame - let offset: CGFloat = 5 - - // Position info panel offset from cursor - var xPosition = cursorPosition.x + offset - var yPosition = cursorPosition.y - panelHeight - offset - - if xPosition + panelWidth > screenFrame.maxX { - xPosition = cursorPosition.x - panelWidth - offset - } - - if xPosition < screenFrame.minX { - xPosition = screenFrame.minX + 10 - } + let screenFrame = screen.visibleFrame + let offset: CGFloat = 5 - if yPosition < screenFrame.minY { - yPosition = cursorPosition.y + offset - } + var xPosition = cursorPosition.x + offset + var yPosition = cursorPosition.y - panelSize.height - offset - if yPosition + panelHeight > screenFrame.maxY { - yPosition = screenFrame.maxY - panelHeight - 10 - } - - infoPanel.setFrame( - NSRect(x: xPosition, y: yPosition, width: panelWidth, height: panelHeight), - display: true - ) - - // Position crosshair centered on cursor - crosshairPanel.setFrame( - NSRect(x: cursorPosition.x - crosshairSize / 2, y: cursorPosition.y - crosshairSize / 2, width: crosshairSize, height: crosshairSize), - display: true - ) + if xPosition + panelSize.width > screenFrame.maxX { + xPosition = cursorPosition.x - panelSize.width - offset + } + if xPosition < screenFrame.minX { + xPosition = screenFrame.minX + 10 + } + if yPosition < screenFrame.minY { + yPosition = cursorPosition.y + offset + } + if yPosition + panelSize.height > screenFrame.maxY { + yPosition = screenFrame.maxY - panelSize.height - 10 } - infoPanel.orderFrontRegardless() - crosshairPanel.orderFrontRegardless() - - self.infoPanel = infoPanel - self.crosshairPanel = crosshairPanel + infoPanel.setFrame( + NSRect(x: xPosition, y: yPosition, width: panelSize.width, height: panelSize.height), + display: true) - dismissTimer = Timer.scheduledTimer(withTimeInterval: duration, repeats: false) { [weak self] _ in - self?.dismiss() - } + crosshairPanel.setFrame( + NSRect( + x: cursorPosition.x - crosshairSize / 2, + y: cursorPosition.y - crosshairSize / 2, + width: crosshairSize, height: crosshairSize), + display: true) } func dismiss() { diff --git a/Pika/Services/Eyedroppers.swift b/Pika/Services/Eyedroppers.swift index 13b78f57..671b44d3 100644 --- a/Pika/Services/Eyedroppers.swift +++ b/Pika/Services/Eyedroppers.swift @@ -140,7 +140,8 @@ class Eyedropper: ObservableObject { if let selectedColor = selectedColor { if Defaults[.showColorOverlay] { - let colorText = selectedColor.toFormat(format: Defaults[.colorFormat], style: Defaults[.copyFormat]) + let colorText = selectedColor.toFormat( + format: Defaults[.colorFormat], style: Defaults[.copyFormat]) let cursorPosition = NSEvent.mouseLocation self.overlayWindow.show( colorText: colorText, diff --git a/Pika/Services/URLSchemeHandler.swift b/Pika/Services/URLSchemeHandler.swift index 82d19437..7985d17a 100644 --- a/Pika/Services/URLSchemeHandler.swift +++ b/Pika/Services/URLSchemeHandler.swift @@ -23,40 +23,46 @@ final class URLSchemeHandler: NSObject { } switch action { - case "format": - if let task, let format = ColorFormat.withLabel(task) { - Defaults[.colorFormat] = format - } - case "pick": - if task == "foreground" { - NSApp.sendAction(#selector(AppDelegate.triggerPickForeground), to: nil, from: nil) - } else if task == "background" { - NSApp.sendAction(#selector(AppDelegate.triggerPickBackground), to: nil, from: nil) - } - case "system": - if task == "foreground" { - NSApp.sendAction(#selector(AppDelegate.triggerSystemPickerForeground), to: nil, from: nil) - } else if task == "background" { - NSApp.sendAction(#selector(AppDelegate.triggerSystemPickerBackground), to: nil, from: nil) - } - case "copy": - if task == "foreground" { - NSApp.sendAction(#selector(AppDelegate.triggerCopyForeground), to: nil, from: nil) - } else if task == "background" { - NSApp.sendAction(#selector(AppDelegate.triggerCopyBackground), to: nil, from: nil) - } else if task == "text" { - NSApp.sendAction(#selector(AppDelegate.triggerCopyText), to: nil, from: nil) - } else if task == "json" { - NSApp.sendAction(#selector(AppDelegate.triggerCopyData), to: nil, from: nil) - } - case "swap": - NSApp.sendAction(#selector(AppDelegate.triggerSwap), to: nil, from: nil) - case "undo": - NSApp.sendAction(#selector(AppDelegate.triggerUndo), to: nil, from: nil) - case "redo": - NSApp.sendAction(#selector(AppDelegate.triggerRedo), to: nil, from: nil) - default: - break + case "format": handleFormat(task: task) + case "pick": handlePick(task: task) + case "system": handleSystem(task: task) + case "copy": handleCopy(task: task) + case "swap": NSApp.sendAction(#selector(AppDelegate.triggerSwap), to: nil, from: nil) + case "undo": NSApp.sendAction(#selector(AppDelegate.triggerUndo), to: nil, from: nil) + case "redo": NSApp.sendAction(#selector(AppDelegate.triggerRedo), to: nil, from: nil) + default: break + } + } + + private func handleFormat(task: String?) { + if let task, let format = ColorFormat.withLabel(task) { + Defaults[.colorFormat] = format + } + } + + private func handlePick(task: String?) { + if task == "foreground" { + NSApp.sendAction(#selector(AppDelegate.triggerPickForeground), to: nil, from: nil) + } else if task == "background" { + NSApp.sendAction(#selector(AppDelegate.triggerPickBackground), to: nil, from: nil) + } + } + + private func handleSystem(task: String?) { + if task == "foreground" { + NSApp.sendAction(#selector(AppDelegate.triggerSystemPickerForeground), to: nil, from: nil) + } else if task == "background" { + NSApp.sendAction(#selector(AppDelegate.triggerSystemPickerBackground), to: nil, from: nil) + } + } + + private func handleCopy(task: String?) { + switch task { + case "foreground": NSApp.sendAction(#selector(AppDelegate.triggerCopyForeground), to: nil, from: nil) + case "background": NSApp.sendAction(#selector(AppDelegate.triggerCopyBackground), to: nil, from: nil) + case "text": NSApp.sendAction(#selector(AppDelegate.triggerCopyText), to: nil, from: nil) + case "json": NSApp.sendAction(#selector(AppDelegate.triggerCopyData), to: nil, from: nil) + default: break } } } diff --git a/Pika/Views/ColorPickOverlay.swift b/Pika/Views/ColorPickOverlay.swift index aac361ea..3e268c78 100644 --- a/Pika/Views/ColorPickOverlay.swift +++ b/Pika/Views/ColorPickOverlay.swift @@ -138,10 +138,18 @@ struct ColorPickCrosshair: View { struct ColorPickOverlay_Previews: PreviewProvider { static var previews: some View { VStack(spacing: 20) { - ColorPickOverlay(colorText: "#232323", pickedColor: NSColor(hex: "#232323"), viewModel: ColorPickOverlayViewModel()) - ColorPickOverlay(colorText: "rgb(35, 35, 35)", pickedColor: NSColor(hex: "#232323"), viewModel: ColorPickOverlayViewModel()) - ColorPickOverlay(colorText: "hsl(0, 0%, 14%)", pickedColor: NSColor(hex: "#232323"), viewModel: ColorPickOverlayViewModel()) - ColorPickOverlay(colorText: "0.14 0.14 0.14", pickedColor: NSColor(hex: "#232323"), viewModel: ColorPickOverlayViewModel()) + ColorPickOverlay( + colorText: "#232323", pickedColor: NSColor(hex: "#232323"), + viewModel: ColorPickOverlayViewModel()) + ColorPickOverlay( + colorText: "rgb(35, 35, 35)", pickedColor: NSColor(hex: "#232323"), + viewModel: ColorPickOverlayViewModel()) + ColorPickOverlay( + colorText: "hsl(0, 0%, 14%)", pickedColor: NSColor(hex: "#232323"), + viewModel: ColorPickOverlayViewModel()) + ColorPickOverlay( + colorText: "0.14 0.14 0.14", pickedColor: NSColor(hex: "#232323"), + viewModel: ColorPickOverlayViewModel()) } .frame(width: 400, height: 400) } diff --git a/Pika/Views/ComplianceButtons.swift b/Pika/Views/ComplianceButtons.swift index 785e8907..7758d62a 100644 --- a/Pika/Views/ComplianceButtons.swift +++ b/Pika/Views/ComplianceButtons.swift @@ -48,7 +48,10 @@ struct CompliancePreviewAPCA: View { var body: some View { let apca = foreground.color.toAPCACompliance(with: background.color) - StyledContentView(title: PikaText.textAppearanceAPCATitle, description: PikaText.textAppearanceAPCADescription) { + StyledContentView( + title: PikaText.textAppearanceAPCATitle, + description: PikaText.textAppearanceAPCADescription + ) { ComplianceToggleGroup(complianceData: .apca(apca), theme: .weight) .padding(20.0) .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .center) diff --git a/Pika/Views/ComplianceToggleGroup.swift b/Pika/Views/ComplianceToggleGroup.swift index 68819862..d1bc0b9b 100644 --- a/Pika/Views/ComplianceToggleGroup.swift +++ b/Pika/Views/ComplianceToggleGroup.swift @@ -33,8 +33,12 @@ struct ComplianceToggleGroup: View { .foregroundColor((wcag.ratio45 && wcag.ratio70) ? .primary : .secondary) .fixedSize() } - ComplianceToggle(title: "AA", isCompliant: wcag.ratio45, tooltip: PikaText.textColorWCAG45, size: size) - ComplianceToggle(title: "AAA", isCompliant: wcag.ratio70, tooltip: PikaText.textColorWCAG70, size: size) + ComplianceToggle( + title: "AA", isCompliant: wcag.ratio45, + tooltip: PikaText.textColorWCAG45, size: size) + ComplianceToggle( + title: "AAA", isCompliant: wcag.ratio70, + tooltip: PikaText.textColorWCAG70, size: size) } HStack(alignment: .center, spacing: 8.0) { if size == .full { @@ -43,24 +47,42 @@ struct ComplianceToggleGroup: View { .foregroundColor((wcag.ratio30 && wcag.ratio45) ? .primary : .secondary) .fixedSize() } - ComplianceToggle(title: "AA", isCompliant: wcag.ratio30, tooltip: PikaText.textColorWCAG30, large: true, size: size) - ComplianceToggle(title: "AAA", isCompliant: wcag.ratio45, tooltip: PikaText.textColorWCAG45, large: true, size: size) + ComplianceToggle( + title: "AA", isCompliant: wcag.ratio30, + tooltip: PikaText.textColorWCAG30, large: true, size: size) + ComplianceToggle( + title: "AAA", isCompliant: wcag.ratio45, + tooltip: PikaText.textColorWCAG45, large: true, size: size) } } } else { HStack(spacing: size == .full ? 16.0 : 8.0) { - ComplianceToggle(title: "AA", isCompliant: wcag.ratio30, tooltip: PikaText.textColorWCAG30, large: true, combined: true, size: size) - ComplianceToggle(title: "AA/AAA", isCompliant: wcag.ratio45, tooltip: PikaText.textColorWCAG45, large: true, combined: true, size: size) - ComplianceToggle(title: "AAA", isCompliant: wcag.ratio70, tooltip: PikaText.textColorWCAG70, combined: true, size: size) + ComplianceToggle( + title: "AA", isCompliant: wcag.ratio30, + tooltip: PikaText.textColorWCAG30, large: true, combined: true, size: size) + ComplianceToggle( + title: "AA/AAA", isCompliant: wcag.ratio45, + tooltip: PikaText.textColorWCAG45, large: true, combined: true, size: size) + ComplianceToggle( + title: "AAA", isCompliant: wcag.ratio70, + tooltip: PikaText.textColorWCAG70, combined: true, size: size) } } case let .apca(apca): HStack(spacing: size == .full ? 16.0 : 8.0) { - ComplianceToggle(title: PikaText.textAPCABaseline, isCompliant: abs(apca.value) >= 30, tooltip: PikaText.textColorAPCA30, combined: true, size: size) - ComplianceToggle(title: PikaText.textAPCAHeadline, isCompliant: abs(apca.value) >= 45, tooltip: PikaText.textColorAPCA45, combined: true, size: size) - ComplianceToggle(title: PikaText.textAPCATitle, isCompliant: abs(apca.value) >= 60, tooltip: PikaText.textColorAPCA60, combined: true, size: size) - ComplianceToggle(title: PikaText.textAPCABody, isCompliant: abs(apca.value) >= 75, tooltip: PikaText.textColorAPCA75, combined: true, size: size) + ComplianceToggle( + title: PikaText.textAPCABaseline, isCompliant: abs(apca.value) >= 30, + tooltip: PikaText.textColorAPCA30, combined: true, size: size) + ComplianceToggle( + title: PikaText.textAPCAHeadline, isCompliant: abs(apca.value) >= 45, + tooltip: PikaText.textColorAPCA45, combined: true, size: size) + ComplianceToggle( + title: PikaText.textAPCATitle, isCompliant: abs(apca.value) >= 60, + tooltip: PikaText.textColorAPCA60, combined: true, size: size) + ComplianceToggle( + title: PikaText.textAPCABody, isCompliant: abs(apca.value) >= 75, + tooltip: PikaText.textColorAPCA75, combined: true, size: size) } } } diff --git a/Pika/Views/ContentView.swift b/Pika/Views/ContentView.swift index 4caebe91..56bd0762 100644 --- a/Pika/Views/ContentView.swift +++ b/Pika/Views/ContentView.swift @@ -69,12 +69,16 @@ struct ContentView: View { } .onReceive(NotificationCenter.default.publisher(for: .triggerCopyText)) { _ in pasteboard.clearContents() - let contents = "\(Exporter.toText(foreground: eyedroppers.foreground, background: eyedroppers.background, style: copyFormat))" + let contents = "\(Exporter.toText( + foreground: eyedroppers.foreground, background: eyedroppers.background, + style: copyFormat))" pasteboard.setString(contents, forType: .string) } .onReceive(NotificationCenter.default.publisher(for: .triggerCopyData)) { _ in pasteboard.clearContents() - let contents = "\(Exporter.toJSON(foreground: eyedroppers.foreground, background: eyedroppers.background, style: copyFormat))" + let contents = "\(Exporter.toJSON( + foreground: eyedroppers.foreground, background: eyedroppers.background, + style: copyFormat))" pasteboard.setString(contents, forType: .string) } } diff --git a/Pika/Views/KeyboardShortcutGrid.swift b/Pika/Views/KeyboardShortcutGrid.swift index c929a924..9336c3de 100644 --- a/Pika/Views/KeyboardShortcutGrid.swift +++ b/Pika/Views/KeyboardShortcutGrid.swift @@ -8,18 +8,25 @@ private struct ShortcutEntry { private let pickRow: [ShortcutEntry] = [ ShortcutEntry(title: PikaText.textPickForeground, notificationName: .triggerPickForeground, keys: ["⌘", "D"]), - ShortcutEntry(title: PikaText.textPickBackground, notificationName: .triggerPickBackground, keys: ["⇧", "⌘", "D"]), + ShortcutEntry( + title: PikaText.textPickBackground, notificationName: .triggerPickBackground, keys: ["⇧", "⌘", "D"]), ShortcutEntry(title: PikaText.textCopyForeground, notificationName: .triggerCopyForeground, keys: ["⌘", "C"]), - ShortcutEntry(title: PikaText.textCopyBackground, notificationName: .triggerCopyBackground, keys: ["⇧", "⌘", "C"]), - ShortcutEntry(title: PikaText.textColorSystemPickerForegroundSimple, notificationName: .triggerSystemPickerForeground, keys: ["⌘", "S"]), - ShortcutEntry(title: PikaText.textColorSystemPickerBackgroundSimple, notificationName: .triggerSystemPickerBackground, keys: ["⇧", "⌘", "S"]), + ShortcutEntry( + title: PikaText.textCopyBackground, notificationName: .triggerCopyBackground, keys: ["⇧", "⌘", "C"]), + ShortcutEntry( + title: PikaText.textColorSystemPickerForegroundSimple, + notificationName: .triggerSystemPickerForeground, keys: ["⌘", "S"]), + ShortcutEntry( + title: PikaText.textColorSystemPickerBackgroundSimple, + notificationName: .triggerSystemPickerBackground, keys: ["⇧", "⌘", "S"]), ] private let actionRow: [ShortcutEntry] = [ ShortcutEntry(title: PikaText.textColorUndo, notificationName: .triggerUndo, keys: ["⌘", "z"]), ShortcutEntry(title: PikaText.textColorRedo, notificationName: .triggerRedo, keys: ["⇧", "⌘", "Z"]), ShortcutEntry(title: PikaText.textColorSwapDetail, notificationName: .triggerSwap, keys: ["X"]), - ShortcutEntry(title: "\(PikaText.textMenuPreferences)...", notificationName: .triggerPreferences, keys: ["⌘", ","]), + ShortcutEntry( + title: "\(PikaText.textMenuPreferences)...", notificationName: .triggerPreferences, keys: ["⌘", ","]), ShortcutEntry(title: PikaText.textMenuQuit, notificationName: .triggerQuit, keys: ["⌘", "Q"]), ] From 0c1d98a0e5ae7a28f1fcf9c56f63da680acea745 Mon Sep 17 00:00:00 2001 From: Charlie Gleason Date: Tue, 10 Mar 2026 19:49:15 -0700 Subject: [PATCH 09/11] Make HSBComponents and HSLComponents public to match public method return types Co-Authored-By: Claude Sonnet 4.6 --- Pika/Extensions/NSColor+HSL.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Pika/Extensions/NSColor+HSL.swift b/Pika/Extensions/NSColor+HSL.swift index 3f13f5d9..6172c249 100644 --- a/Pika/Extensions/NSColor+HSL.swift +++ b/Pika/Extensions/NSColor+HSL.swift @@ -5,8 +5,8 @@ import Defaults // identifier_name is disabled because color science math uses conventional single-letter // variable names (h, s, b, l, r, g) that would be misleading if renamed. -struct HSBComponents { let h, s, b: CGFloat } -struct HSLComponents { let h, s, l: CGFloat } +public struct HSBComponents { let h, s, b: CGFloat } +public struct HSLComponents { let h, s, l: CGFloat } extension NSColor { /* From 157119d6fa73afec2c09c700de8ccbeb4f4970ff Mon Sep 17 00:00:00 2001 From: Charlie Gleason Date: Tue, 10 Mar 2026 20:00:53 -0700 Subject: [PATCH 10/11] Fix multiline string interpolation compile error in ContentView String interpolation cannot span multiple lines in Swift. Removed the unnecessary "\(...)" wrapper since Exporter.toText/toJSON already return String. Co-Authored-By: Claude Opus 4.6 --- Pika/Views/ContentView.swift | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Pika/Views/ContentView.swift b/Pika/Views/ContentView.swift index 56bd0762..55dc9e68 100644 --- a/Pika/Views/ContentView.swift +++ b/Pika/Views/ContentView.swift @@ -69,16 +69,16 @@ struct ContentView: View { } .onReceive(NotificationCenter.default.publisher(for: .triggerCopyText)) { _ in pasteboard.clearContents() - let contents = "\(Exporter.toText( + let contents = Exporter.toText( foreground: eyedroppers.foreground, background: eyedroppers.background, - style: copyFormat))" + style: copyFormat) pasteboard.setString(contents, forType: .string) } .onReceive(NotificationCenter.default.publisher(for: .triggerCopyData)) { _ in pasteboard.clearContents() - let contents = "\(Exporter.toJSON( + let contents = Exporter.toJSON( foreground: eyedroppers.foreground, background: eyedroppers.background, - style: copyFormat))" + style: copyFormat) pasteboard.setString(contents, forType: .string) } } From eab1c360bef0c17e79b3cef475a424ed040c3b30 Mon Sep 17 00:00:00 2001 From: Charlie Gleason Date: Tue, 10 Mar 2026 23:05:19 -0700 Subject: [PATCH 11/11] Add spaces to lab unformatted output and fix closing paren formatting Consistent comma spacing in lab unformatted style to match oklch. Move closing parentheses to their own lines for multiline function calls. Co-Authored-By: Claude Opus 4.6 --- Pika/Constants/Constants.swift | 1 - Pika/Extensions/NSColor+Lab.swift | 2 +- Pika/Services/ColorPickOverlayWindow.swift | 12 +++++--- Pika/Services/Eyedroppers.swift | 3 +- Pika/Views/ColorPickOverlay.swift | 12 +++++--- Pika/Views/ComplianceToggleGroup.swift | 33 ++++++++++++++-------- Pika/Views/ContentView.swift | 6 ++-- Pika/Views/KeyboardShortcutGrid.swift | 15 ++++++---- 8 files changed, 55 insertions(+), 29 deletions(-) diff --git a/Pika/Constants/Constants.swift b/Pika/Constants/Constants.swift index 0956a27d..96a8f3fc 100644 --- a/Pika/Constants/Constants.swift +++ b/Pika/Constants/Constants.swift @@ -3,7 +3,6 @@ import KeyboardShortcuts import SwiftUI // swiftlint:disable line_length -// trailing_comma is already disabled globally in .swiftlint.yml extension KeyboardShortcuts.Name { static let togglePika = Self("togglePika") diff --git a/Pika/Extensions/NSColor+Lab.swift b/Pika/Extensions/NSColor+Lab.swift index de44f5c9..0d8802e3 100644 --- a/Pika/Extensions/NSColor+Lab.swift +++ b/Pika/Extensions/NSColor+Lab.swift @@ -93,7 +93,7 @@ extension NSColor { case .design, .swiftUI: return "lab(\(l_str), \(a_str), \(b_str))" case .unformatted: - return "\(l_str),\(a_str),\(b_str)" + return "\(l_str), \(a_str), \(b_str)" } } diff --git a/Pika/Services/ColorPickOverlayWindow.swift b/Pika/Services/ColorPickOverlayWindow.swift index f6d1c647..a88a0c44 100644 --- a/Pika/Services/ColorPickOverlayWindow.swift +++ b/Pika/Services/ColorPickOverlayWindow.swift @@ -25,7 +25,8 @@ class ColorPickOverlayWindow { positionPanels( infoPanel: infoPanel, crosshairPanel: crosshairPanel, - near: cursorPosition, panelSize: panelSize, crosshairSize: crosshairSize) + near: cursorPosition, panelSize: panelSize, crosshairSize: crosshairSize + ) infoPanel.orderFrontRegardless() crosshairPanel.orderFrontRegardless() @@ -121,14 +122,17 @@ class ColorPickOverlayWindow { infoPanel.setFrame( NSRect(x: xPosition, y: yPosition, width: panelSize.width, height: panelSize.height), - display: true) + display: true + ) crosshairPanel.setFrame( NSRect( x: cursorPosition.x - crosshairSize / 2, y: cursorPosition.y - crosshairSize / 2, - width: crosshairSize, height: crosshairSize), - display: true) + width: crosshairSize, height: crosshairSize + ), + display: true + ) } func dismiss() { diff --git a/Pika/Services/Eyedroppers.swift b/Pika/Services/Eyedroppers.swift index 671b44d3..15cec4b4 100644 --- a/Pika/Services/Eyedroppers.swift +++ b/Pika/Services/Eyedroppers.swift @@ -141,7 +141,8 @@ class Eyedropper: ObservableObject { if let selectedColor = selectedColor { if Defaults[.showColorOverlay] { let colorText = selectedColor.toFormat( - format: Defaults[.colorFormat], style: Defaults[.copyFormat]) + format: Defaults[.colorFormat], style: Defaults[.copyFormat] + ) let cursorPosition = NSEvent.mouseLocation self.overlayWindow.show( colorText: colorText, diff --git a/Pika/Views/ColorPickOverlay.swift b/Pika/Views/ColorPickOverlay.swift index 3e268c78..857ef59b 100644 --- a/Pika/Views/ColorPickOverlay.swift +++ b/Pika/Views/ColorPickOverlay.swift @@ -140,16 +140,20 @@ struct ColorPickOverlay_Previews: PreviewProvider { VStack(spacing: 20) { ColorPickOverlay( colorText: "#232323", pickedColor: NSColor(hex: "#232323"), - viewModel: ColorPickOverlayViewModel()) + viewModel: ColorPickOverlayViewModel() + ) ColorPickOverlay( colorText: "rgb(35, 35, 35)", pickedColor: NSColor(hex: "#232323"), - viewModel: ColorPickOverlayViewModel()) + viewModel: ColorPickOverlayViewModel() + ) ColorPickOverlay( colorText: "hsl(0, 0%, 14%)", pickedColor: NSColor(hex: "#232323"), - viewModel: ColorPickOverlayViewModel()) + viewModel: ColorPickOverlayViewModel() + ) ColorPickOverlay( colorText: "0.14 0.14 0.14", pickedColor: NSColor(hex: "#232323"), - viewModel: ColorPickOverlayViewModel()) + viewModel: ColorPickOverlayViewModel() + ) } .frame(width: 400, height: 400) } diff --git a/Pika/Views/ComplianceToggleGroup.swift b/Pika/Views/ComplianceToggleGroup.swift index d1bc0b9b..8bef2077 100644 --- a/Pika/Views/ComplianceToggleGroup.swift +++ b/Pika/Views/ComplianceToggleGroup.swift @@ -35,10 +35,12 @@ struct ComplianceToggleGroup: View { } ComplianceToggle( title: "AA", isCompliant: wcag.ratio45, - tooltip: PikaText.textColorWCAG45, size: size) + tooltip: PikaText.textColorWCAG45, size: size + ) ComplianceToggle( title: "AAA", isCompliant: wcag.ratio70, - tooltip: PikaText.textColorWCAG70, size: size) + tooltip: PikaText.textColorWCAG70, size: size + ) } HStack(alignment: .center, spacing: 8.0) { if size == .full { @@ -49,23 +51,28 @@ struct ComplianceToggleGroup: View { } ComplianceToggle( title: "AA", isCompliant: wcag.ratio30, - tooltip: PikaText.textColorWCAG30, large: true, size: size) + tooltip: PikaText.textColorWCAG30, large: true, size: size + ) ComplianceToggle( title: "AAA", isCompliant: wcag.ratio45, - tooltip: PikaText.textColorWCAG45, large: true, size: size) + tooltip: PikaText.textColorWCAG45, large: true, size: size + ) } } } else { HStack(spacing: size == .full ? 16.0 : 8.0) { ComplianceToggle( title: "AA", isCompliant: wcag.ratio30, - tooltip: PikaText.textColorWCAG30, large: true, combined: true, size: size) + tooltip: PikaText.textColorWCAG30, large: true, combined: true, size: size + ) ComplianceToggle( title: "AA/AAA", isCompliant: wcag.ratio45, - tooltip: PikaText.textColorWCAG45, large: true, combined: true, size: size) + tooltip: PikaText.textColorWCAG45, large: true, combined: true, size: size + ) ComplianceToggle( title: "AAA", isCompliant: wcag.ratio70, - tooltip: PikaText.textColorWCAG70, combined: true, size: size) + tooltip: PikaText.textColorWCAG70, combined: true, size: size + ) } } @@ -73,16 +80,20 @@ struct ComplianceToggleGroup: View { HStack(spacing: size == .full ? 16.0 : 8.0) { ComplianceToggle( title: PikaText.textAPCABaseline, isCompliant: abs(apca.value) >= 30, - tooltip: PikaText.textColorAPCA30, combined: true, size: size) + tooltip: PikaText.textColorAPCA30, combined: true, size: size + ) ComplianceToggle( title: PikaText.textAPCAHeadline, isCompliant: abs(apca.value) >= 45, - tooltip: PikaText.textColorAPCA45, combined: true, size: size) + tooltip: PikaText.textColorAPCA45, combined: true, size: size + ) ComplianceToggle( title: PikaText.textAPCATitle, isCompliant: abs(apca.value) >= 60, - tooltip: PikaText.textColorAPCA60, combined: true, size: size) + tooltip: PikaText.textColorAPCA60, combined: true, size: size + ) ComplianceToggle( title: PikaText.textAPCABody, isCompliant: abs(apca.value) >= 75, - tooltip: PikaText.textColorAPCA75, combined: true, size: size) + tooltip: PikaText.textColorAPCA75, combined: true, size: size + ) } } } diff --git a/Pika/Views/ContentView.swift b/Pika/Views/ContentView.swift index 55dc9e68..dc88cd27 100644 --- a/Pika/Views/ContentView.swift +++ b/Pika/Views/ContentView.swift @@ -71,14 +71,16 @@ struct ContentView: View { pasteboard.clearContents() let contents = Exporter.toText( foreground: eyedroppers.foreground, background: eyedroppers.background, - style: copyFormat) + style: copyFormat + ) pasteboard.setString(contents, forType: .string) } .onReceive(NotificationCenter.default.publisher(for: .triggerCopyData)) { _ in pasteboard.clearContents() let contents = Exporter.toJSON( foreground: eyedroppers.foreground, background: eyedroppers.background, - style: copyFormat) + style: copyFormat + ) pasteboard.setString(contents, forType: .string) } } diff --git a/Pika/Views/KeyboardShortcutGrid.swift b/Pika/Views/KeyboardShortcutGrid.swift index 9336c3de..4c73cc4b 100644 --- a/Pika/Views/KeyboardShortcutGrid.swift +++ b/Pika/Views/KeyboardShortcutGrid.swift @@ -9,16 +9,20 @@ private struct ShortcutEntry { private let pickRow: [ShortcutEntry] = [ ShortcutEntry(title: PikaText.textPickForeground, notificationName: .triggerPickForeground, keys: ["⌘", "D"]), ShortcutEntry( - title: PikaText.textPickBackground, notificationName: .triggerPickBackground, keys: ["⇧", "⌘", "D"]), + title: PikaText.textPickBackground, notificationName: .triggerPickBackground, keys: ["⇧", "⌘", "D"] + ), ShortcutEntry(title: PikaText.textCopyForeground, notificationName: .triggerCopyForeground, keys: ["⌘", "C"]), ShortcutEntry( - title: PikaText.textCopyBackground, notificationName: .triggerCopyBackground, keys: ["⇧", "⌘", "C"]), + title: PikaText.textCopyBackground, notificationName: .triggerCopyBackground, keys: ["⇧", "⌘", "C"] + ), ShortcutEntry( title: PikaText.textColorSystemPickerForegroundSimple, - notificationName: .triggerSystemPickerForeground, keys: ["⌘", "S"]), + notificationName: .triggerSystemPickerForeground, keys: ["⌘", "S"] + ), ShortcutEntry( title: PikaText.textColorSystemPickerBackgroundSimple, - notificationName: .triggerSystemPickerBackground, keys: ["⇧", "⌘", "S"]), + notificationName: .triggerSystemPickerBackground, keys: ["⇧", "⌘", "S"] + ), ] private let actionRow: [ShortcutEntry] = [ @@ -26,7 +30,8 @@ private let actionRow: [ShortcutEntry] = [ ShortcutEntry(title: PikaText.textColorRedo, notificationName: .triggerRedo, keys: ["⇧", "⌘", "Z"]), ShortcutEntry(title: PikaText.textColorSwapDetail, notificationName: .triggerSwap, keys: ["X"]), ShortcutEntry( - title: "\(PikaText.textMenuPreferences)...", notificationName: .triggerPreferences, keys: ["⌘", ","]), + title: "\(PikaText.textMenuPreferences)...", notificationName: .triggerPreferences, keys: ["⌘", ","] + ), ShortcutEntry(title: PikaText.textMenuQuit, notificationName: .triggerQuit, keys: ["⌘", "Q"]), ]