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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions Pika.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -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 */; };
Expand All @@ -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 */; };
Expand Down Expand Up @@ -194,6 +197,7 @@
CC2000000000000000000700 /* WCAGComplianceTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WCAGComplianceTests.swift; sourceTree = "<group>"; };
CC2000000000000000000800 /* APCAComplianceTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = APCAComplianceTests.swift; sourceTree = "<group>"; };
CC2000000000000000000900 /* NotificationNamesTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationNamesTests.swift; sourceTree = "<group>"; };
CC2000000000000000000A00 /* CGFloatFormatTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CGFloatFormatTests.swift; sourceTree = "<group>"; };

220D5E9328DB154300B6285E /* AppModeButtons.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppModeButtons.swift; sourceTree = "<group>"; };
220D5E9728DB158400B6285E /* AppModeToggleGroup.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppModeToggleGroup.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -223,6 +227,7 @@
CC000001000000000000AAE0 /* NSColor+Luminance.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NSColor+Luminance.swift"; sourceTree = "<group>"; };
CC000001000000000000AAF0 /* NSColor+Hex.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NSColor+Hex.swift"; sourceTree = "<group>"; };
CC000001000000000000AB00 /* NSColor+RGB.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NSColor+RGB.swift"; sourceTree = "<group>"; };
CC000001000000000000AB10 /* CGFloat+Format.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CGFloat+Format.swift"; sourceTree = "<group>"; };
EA0C524F25AA729300AFF716 /* Visualisation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Visualisation.swift; sourceTree = "<group>"; };
EA0C525F25AB5A2B00AFF716 /* NavigationMenuItems.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigationMenuItems.swift; sourceTree = "<group>"; };
EA0C526325AB5D1700AFF716 /* PikaWindow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PikaWindow.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -422,6 +427,7 @@
CC000001000000000000AAE0 /* NSColor+Luminance.swift */,
CC000001000000000000AAF0 /* NSColor+Hex.swift */,
CC000001000000000000AB00 /* NSColor+RGB.swift */,
CC000001000000000000AB10 /* CGFloat+Format.swift */,
);
path = Extensions;
sourceTree = "<group>";
Expand Down Expand Up @@ -480,6 +486,7 @@
CC2000000000000000000700 /* WCAGComplianceTests.swift */,
CC2000000000000000000800 /* APCAComplianceTests.swift */,
CC2000000000000000000900 /* NotificationNamesTests.swift */,
CC2000000000000000000A00 /* CGFloatFormatTests.swift */,
);
path = PikaTests;
sourceTree = "<group>";
Expand Down Expand Up @@ -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 */,
Expand Down Expand Up @@ -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 */,
Expand Down Expand Up @@ -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;
};
Expand Down Expand Up @@ -1143,6 +1153,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";
Expand Down Expand Up @@ -1174,6 +1185,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";
Expand Down Expand Up @@ -1205,6 +1217,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";
Expand Down Expand Up @@ -1237,6 +1250,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";
Expand Down
2 changes: 1 addition & 1 deletion Pika/AppDelegate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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:)),
Expand Down
14 changes: 14 additions & 0 deletions Pika/Extensions/CGFloat+Format.swift
Original file line number Diff line number Diff line change
@@ -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 }
Comment thread
superhighfives marked this conversation as resolved.
var result = formatted
while result.hasSuffix("0") { result.removeLast() }
if result.hasSuffix(".") { result.removeLast() }
return result
}
}
24 changes: 12 additions & 12 deletions Pika/Extensions/NSColor+Lab.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Comment thread
superhighfives marked this conversation as resolved.
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)"
}
}

Expand Down Expand Up @@ -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 * 10000) / 10000
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%% %.4f %.2f)", l_val, c_val, h_val)
return "oklch(\(l_str)% \(c_str) \(h_str))"
case .design, .swiftUI:
return String(format: "oklch(%.2f, %.4f, %.2f)", l_val, c_val, h_val)
return "oklch(\(l_str), \(c_str), \(h_str))"
case .unformatted:
return String(format: "%.2f, %.4f, %.2f", l_val, c_val, h_val)
return "\(l_str), \(c_str), \(h_str)"
}
}
}
Expand Down
88 changes: 60 additions & 28 deletions Pika/Services/StatusBarController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -34,40 +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())
menu.addItem(
withTitle: PikaText.textMenuQuit,
action: #selector(AppDelegate.terminatePika),
keyEquivalent: ""
)

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
}
Expand Down
31 changes: 31 additions & 0 deletions PikaTests/CGFloatFormatTests.swift
Original file line number Diff line number Diff line change
@@ -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")
}
}
36 changes: 36 additions & 0 deletions PikaTests/NSColorLabTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Comment thread
superhighfives marked this conversation as resolved.
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")
}
}
}
Loading