Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
138 commits
Select commit Hold shift + click to select a range
a8cc8c0
Add custom colour picker (loupe) as opt-in alternative to system sampler
superhighfives Jul 22, 2026
425ebff
Add first-run splash and settings UI for the custom picker
superhighfives Jul 22, 2026
298c690
Source colour names from color.pizza with offline fallback
claude Jul 25, 2026
d46b281
Refresh colour names periodically and surface fetch state
claude Jul 25, 2026
b17b707
Address review feedback: decode colour names off main thread, fix ava…
superhighfives Jul 31, 2026
207f53c
Drop the build-time colour-name download (keep committed list as offl…
superhighfives Jul 31, 2026
27f4d88
Merge pull request #251 from superhighfives/claude/color-names-api-of…
superhighfives Aug 1, 2026
ad987fc
Address review feedback: window level, shortcut dispatch, colour spac…
superhighfives Aug 1, 2026
b9f0149
Address review feedback: keep cached secondary windows synced with ap…
superhighfives Aug 1, 2026
e8a4200
Refine first-run splash and Pro loupe
superhighfives Aug 1, 2026
7d0cdb3
Loupe: crisp native capture, global Escape, fix re-arm after cancel
superhighfives Aug 1, 2026
c2caa02
Loupe: capture the full display natively and crop for exact pixels
superhighfives Aug 2, 2026
29d8b96
Loupe: snap the magnifier window to the device-pixel grid
superhighfives Aug 2, 2026
556dc54
Loupe: Shift+arrow nudges ten pixels
superhighfives Aug 2, 2026
cf95678
Splash: offer optional Accessibility grant for the Pro picker
superhighfives Aug 2, 2026
dfc7697
Splash: keep the Accessibility prompt visible, add top padding
superhighfives Aug 4, 2026
1741233
Splash: side-by-side permission pills that confirm live
superhighfives Aug 4, 2026
314a1b0
Splash: auto-refresh permission pills, bottom-anchor the list
superhighfives Aug 4, 2026
776156e
Loupe: read the sampled pixel in its exact colour space
superhighfives Aug 4, 2026
65b0d5c
Loupe: fix shadow clipping, stale-colour flash, hide the cursor
superhighfives Aug 4, 2026
6bdeb7b
Loupe: engrave the readouts around the lens rim
superhighfives Aug 4, 2026
ca33ea2
Loupe: thinner, tighter engraved text
superhighfives Aug 4, 2026
ec8ab09
Loupe: two selectable themes (Lens / Badge) with a Settings picker
superhighfives Aug 4, 2026
98d7039
Loupe: tighter shadow, colour name in both, rotate badges 45°
superhighfives Aug 5, 2026
a260397
Loupe/permissions: WCAG in readouts, standard buttons, grant intro
superhighfives Aug 5, 2026
eebf55a
Permissions: flat green "granted" state, refresh on return
superhighfives Aug 5, 2026
408d5c7
Permissions: regular (bordered) grant buttons
superhighfives Aug 5, 2026
8f34305
Permissions: match granted success box height to the button
superhighfives Aug 5, 2026
a7ca710
Loupe: fix missing contrast — comparison colour was always nil
superhighfives Aug 5, 2026
2018d88
Loupe: adapt outline and badge colours to light/dark mode
superhighfives Aug 5, 2026
357202e
Loupe: fix badge text alignment, animate, add Card theme
superhighfives Aug 5, 2026
49a821e
Loupe: realtime in-app contrast, simplified readouts, reliable input …
superhighfives Aug 6, 2026
fb2f2ab
Loupe: fix pick teardown/cancel leaks; fade to 15% over Pika
superhighfives Aug 6, 2026
40c7e31
Loupe: commit the current sample on Return / keypad Enter
superhighfives Aug 6, 2026
6fb45ff
Loupe: flush rim, mono/sans type, liquid-glass dismiss disc
superhighfives Aug 7, 2026
336bed5
Re-show the splash once per onboarding version
superhighfives Aug 7, 2026
94545a7
Fix splash window reuse, popover fade-in, CI SDK gap; drop dead code
superhighfives Aug 7, 2026
ad34c79
Let Cmd+Tab reach the system app switcher during a pick
superhighfives Aug 7, 2026
64ad820
Ellipsize CircularText labels that still overrun past the font floor
superhighfives Aug 8, 2026
5d0d4aa
Wire the Badge theme's badgeArc into the same truncate-on-floor fallb…
superhighfives Aug 8, 2026
6fb7036
Loupe card: stack the colours, value + name on both
superhighfives Aug 8, 2026
c54f719
Bump MARKETING_VERSION to 2.0.0
superhighfives Aug 8, 2026
1fca344
Address review feedback: document the .card loupe theme in LoupeCircl…
superhighfives Aug 8, 2026
3c9ad14
Loupe: capture only the magnifier window, not the whole display
superhighfives Aug 8, 2026
a247579
Loupe: unify readout text colour across all three themes
superhighfives Aug 8, 2026
6aa3d51
Pick legible on-colour text by WCAG contrast, app-wide
superhighfives Aug 8, 2026
ee0cc36
Loupe: fix pair-pick foreground overwrite and system-wide key swallow
superhighfives Aug 8, 2026
8008332
Loupe: keep the magnifier crisp with a whole-point capture window
superhighfives Aug 9, 2026
f80b5e1
Loupe: test getUIColor threshold; drop orphaned doc comment
superhighfives Aug 9, 2026
d29f4eb
Loupe: document the catcher's click/scroll overrides as a fallback
superhighfives Aug 9, 2026
5460d93
Merge pull request #247 from superhighfives/claude/custom-color-picker
superhighfives Aug 9, 2026
7af9ca7
Bump KeyboardShortcuts to 3.x for Xcode 26 / Swift 6.2
superhighfives Aug 9, 2026
cfb4b7d
Address review feedback: re-level splash window and localize default …
superhighfives Aug 9, 2026
72d257e
Remove orphaned comment header in PikaText.swift
superhighfives Aug 9, 2026
f6ae8b7
Fix CI: select Xcode 26.3 to match KeyboardShortcuts 3.x Swift tools …
superhighfives Aug 9, 2026
288e47e
Fix CVE-class crash: NSColor(hex:) falls back to black on invalid len…
superhighfives Aug 9, 2026
e260a67
Give Pick pair its own Help-grid notification name
superhighfives Aug 9, 2026
d31cf38
Add string→colour inverses and safe hex parsing
superhighfives Aug 9, 2026
44a41f1
Add ColorFormat decompose/recompose component model
superhighfives Aug 9, 2026
a311b0e
Make colour readouts inline-editable
superhighfives Aug 9, 2026
fbd9285
Keep ColorPair hex parsing strict to 6 digits
superhighfives Aug 9, 2026
381d2b8
Revert "Address review feedback: re-level splash window and localize …
superhighfives Aug 9, 2026
42afb53
Address review feedback: abort in-progress edit on external eyedroppe…
superhighfives Aug 9, 2026
0384209
Address review feedback: compare eyedropper color in a common space
superhighfives Aug 9, 2026
22e3819
Address review feedback: add regression test for triggerPickPair noti…
superhighfives Aug 9, 2026
7615198
Revert "Fix CI: select Xcode 26.3 to match KeyboardShortcuts 3.x Swif…
superhighfives Aug 10, 2026
5cc9064
Revert "Bump KeyboardShortcuts to 3.x for Xcode 26 / Swift 6.2"
superhighfives Aug 10, 2026
20b7052
Pin KeyboardShortcuts to 2.4.0 and remove duplicate test
superhighfives Aug 10, 2026
d82adeb
Revert "Fix CI: select Xcode 26.3 to match KeyboardShortcuts 3.x Swif…
superhighfives Aug 10, 2026
45ea219
Revert "Bump KeyboardShortcuts to 3.x for Xcode 26 / Swift 6.2"
superhighfives Aug 10, 2026
957b1d6
Pin KeyboardShortcuts to 2.4.0 (latest 2.x)
superhighfives Aug 10, 2026
109e3f7
Fix format-switch stale values, scaffolding wrap, and focus-ring widt…
superhighfives Aug 10, 2026
7d9bf6c
fix: round query color to match named-color database 8-bit buckets
YuriNachos Aug 9, 2026
a7e95d0
fix: make the minimum-OS update gate inclusive (>=), not strict (>)
YuriNachos Aug 9, 2026
70c589b
Merge pull request #261 from superhighfives/merge/yurinachos-258-259-…
superhighfives Aug 10, 2026
9862a10
Fix drag-scrub corrupting a concurrent field-edit session
superhighfives Aug 11, 2026
70b63fc
Fix review findings: Lab inf/nan, hex trimming, localization and spla…
superhighfives Aug 12, 2026
d2b837c
Fix scrub cursor, focus-reset outline bug, and invert scroll direction
superhighfives Aug 13, 2026
7add4ee
Fix inline color-field focus, selection, and layout bugs
superhighfives Aug 13, 2026
e2653ae
Fix drag-scrub still leaving text selected on release
superhighfives Aug 13, 2026
5218c48
Fix five inline-editable-colours UI bugs
superhighfives Aug 13, 2026
1e11c3f
Address PR review feedback
superhighfives Aug 13, 2026
7b6254d
Clamp out-of-range numeric input instead of rejecting it; fix drag-sc…
superhighfives Aug 14, 2026
a1f85f4
Clamp Lab/OKLCH inputs in recompose to match the clamped display value
superhighfives Aug 14, 2026
05b1f8a
Merge pull request #260 from superhighfives/claude/inline-editable-co…
superhighfives Aug 16, 2026
566e0a1
Invalidate cached colour-value display on colorSpace change
superhighfives Aug 16, 2026
a7f2a1c
Fix 1px shift when focusing a color component field
superhighfives Aug 16, 2026
3da8efa
Bump version to 2.0.0-beta1 (build 99)
superhighfives Aug 16, 2026
2a0dec8
Suppress SwiftLint length/complexity errors blocking release archive
superhighfives Aug 16, 2026
778ceb8
Fix OKLCH/Lab value clipping by wrapping onto a second line
superhighfives Aug 16, 2026
a3ff6a3
Make value-row wrapping deterministic; tighten line spacing; normaliz…
superhighfives Aug 17, 2026
df10601
Add regression test for drag scrub sensitivity scaling
superhighfives Aug 17, 2026
aedb0b9
Cap value row at two lines; fix width measurement for real this time
superhighfives Aug 17, 2026
25e8bae
Keep Pro picker permissions intro visible; fix stale pill refresh
superhighfives Aug 17, 2026
b8174c2
Shrink further to avoid edge clipping; tighten wrapped-line spacing
superhighfives Aug 17, 2026
07fbaff
Keep trailing punctuation attached to its value when wrapping
superhighfives Aug 17, 2026
ea539b9
Fix stale cross-file doc reference after ScrubTextField split
superhighfives Aug 17, 2026
90352ea
Freeze value font size for the duration of a scrub/edit session
superhighfives Aug 17, 2026
19b5c01
Stabilize scrub display: round to 2 decimals, keep finer precision
superhighfives Aug 17, 2026
99a78b5
Add regression test for stableDecimalPlaces edge cases
superhighfives Aug 17, 2026
7a8d0cb
Size a scrub session for the format's worst case, not the live value
superhighfives Aug 17, 2026
18d4a1e
Fix stale clampValuesToRange doc references after finalizeValues rename
superhighfives Aug 17, 2026
b92190a
Show a floating pill during scrub instead of updating the field inline
superhighfives Aug 17, 2026
70ea6c3
Widen worst-case sizing budget for unranged decimal components
superhighfives Aug 17, 2026
c491823
Fix double-counted sign in worstCaseComponentString's digit budget
superhighfives Aug 17, 2026
8242d38
Fix same double-counted-sign bug in worstCaseComponentString's intege…
superhighfives Aug 17, 2026
cc171ac
Restore live eyedropper preview during click-drag/scroll scrub
superhighfives Aug 17, 2026
d64e810
Fix asymmetric digit-budget gap in worstCaseComponentString's integer…
superhighfives Aug 17, 2026
7614a92
Report the achieved value while scrubbing, not the requested one
superhighfives Aug 17, 2026
87c3ce5
Scrub vertically to change precision, horizontally to change value
superhighfives Aug 18, 2026
962e96a
Address review feedback: drop dead dragOrigin state, fix misplaced do…
superhighfives Aug 18, 2026
2ce7a78
Keep the whole readout in step with the colour while scrubbing
superhighfives Aug 18, 2026
c1a39ea
Carry the ranged-component precision floor from click-drag to two-fin…
superhighfives Aug 18, 2026
bb6435b
Clamp scroll-to-scrub precision to the same ceiling as click-drag
superhighfives Aug 18, 2026
7e5b1d5
Make a scrub reversible, cancellable with Escape, and keep the pill o…
superhighfives Aug 18, 2026
e3baaf4
Preview the whole colour in one pill; freeze every field while scrubbing
superhighfives Aug 18, 2026
d9bb755
Centre the scrub pill, unblur it, and stop the readout acting as a pi…
superhighfives Aug 18, 2026
3d2c10a
Address review feedback: clear the scrub pill on every session-ending…
superhighfives Aug 18, 2026
f3cca6b
Refine the scrub readout: pill styling, hairline, and two interaction…
superhighfives Aug 18, 2026
5fd6cce
Soften the readout hairline to 15%
superhighfives Aug 18, 2026
c3f1c49
Don't flash a text selection while a press is still resolving
superhighfives Aug 18, 2026
bbc0e23
Refuse mouse-down-driven focus so a press never flashes a selection
superhighfives Aug 18, 2026
81fe9f9
Stop hiding the type labels whenever the preview is shown
superhighfives Aug 18, 2026
2cc1ec4
Address review feedback: skip hex components in finalizeValues
superhighfives Aug 18, 2026
d9bd654
Address review feedback: dedupe scrub-precision formula, fix stale co…
superhighfives Aug 18, 2026
dd411f0
Reveal the colour name instead of snapping the layout up
superhighfives Aug 19, 2026
9375f79
Address review feedback: drop dead rowScrubPreview reset, fix stale k…
superhighfives Aug 19, 2026
64ecd39
Show the readout boundary across both swatches at one height
superhighfives Aug 19, 2026
04a7656
Fix the Settings chrome and make the picker tiles readable in light mode
superhighfives Aug 19, 2026
627ffa5
Bound frozenSize's worst-case width to only the field being typed into
superhighfives Aug 22, 2026
9521d17
Re-budget frozenSize's worst-case width for every field touched mid-s…
superhighfives Aug 22, 2026
9b086b9
Route programmatic text-field writes through the editor so undo/redo …
superhighfives Aug 22, 2026
9f985c4
Wire Cmd-Z/Cmd-Shift-Z to actually dispatch in window mode
superhighfives Aug 22, 2026
33435ae
Bump version to 2.0.0-beta2 (build 100)
superhighfives Aug 24, 2026
0a768f3
Commit the live scrub colour directly instead of recomposing from sta…
superhighfives Aug 24, 2026
78a71e8
Skip the recompose-and-set side effect when the scrub-end text write …
superhighfives Aug 24, 2026
03729b6
Bump version to 2.0.0-beta3 (build 101)
superhighfives Aug 24, 2026
04a3fa0
Drive splash permission pills via observable state, not identity churn
superhighfives Aug 24, 2026
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
70 changes: 63 additions & 7 deletions Pika.xcodeproj/project.pbxproj

Large diffs are not rendered by default.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

60 changes: 58 additions & 2 deletions Pika/AppDelegate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,11 @@ class AppDelegate: NSObject, NSApplicationDelegate {
validateColorSpace()
showPikaIfConfigured()
registerGlobalKeyMonitor()

// Refresh the colour-name list from color.pizza (catalogue + selected list) and cache
// it; the eyedroppers rebuild via `.colorNamesUpdated`. Best-effort — falls back to
// the bundled default offline.
ColorNamesManager.shared.updateOnLaunch()
}

private func removeUpdatesMenuItemIfNeeded() {
Expand Down Expand Up @@ -113,10 +118,27 @@ class AppDelegate: NSObject, NSApplicationDelegate {
NSApp.sendAction(#selector(AppDelegate.triggerPickForeground), to: nil, from: nil)
}
}
KeyboardShortcuts.onKeyUp(for: .pickPair) { [] in
if Defaults[.viewedSplash] {
NSApp.sendAction(#selector(AppDelegate.triggerPickContrast), to: nil, from: nil)
}
}
}

/// The splash shows on every launch unless the user ticked its (pre-selected) "Don't show
/// this again" checkbox — EXCEPT when a new onboarding version hasn't been seen yet, in which
/// case it shows once for everyone (see `PikaConstants.currentSplashVersion`).
private var shouldShowSplash: Bool {
!Defaults[.hideSplashOnLaunch]
|| Defaults[.lastSeenSplashVersion] < PikaConstants.currentSplashVersion
}

private func presentSplashIfNeeded() {
if !Defaults[.viewedSplash] {
// `viewedSplash` still records the first run so the pick shortcuts stay gated until
// onboarding is dismissed the first time. `lastSeenSplashVersion` is recorded on
// dismissal (`closeSplashWindow`), not here — recording it now would make
// `showPikaIfConfigured` reveal the main window behind the splash.
if shouldShowSplash {
openSplashWindow(nil)
NSApp.activate(ignoringOtherApps: true)
}
Expand All @@ -129,6 +151,13 @@ class AppDelegate: NSObject, NSApplicationDelegate {
}

private func showPikaIfConfigured() {
// Pika should only ever appear once the splash has been dismissed. When the splash
// is up, defer the main window until `closeSplashWindow` fires; otherwise show it now.
guard !shouldShowSplash else { return }
presentConfiguredPika()
}

private func presentConfiguredPika() {
if Defaults[.alwaysShowOnLaunch], !Defaults[.appMode].usesPopover {
showPika(self)
}
Expand Down Expand Up @@ -215,7 +244,15 @@ class AppDelegate: NSObject, NSApplicationDelegate {
// MARK: - Window forwarding

extension AppDelegate {
@objc func closeSplashWindow() { windowCoordinator.closeSplashWindow() }
@objc func closeSplashWindow() {
windowCoordinator.closeSplashWindow()
// Record that this onboarding version has been seen, so the version gate doesn't
// re-show it next launch (the user's "Don't show again" choice governs from here).
Defaults[.lastSeenSplashVersion] = PikaConstants.currentSplashVersion
// Now that onboarding is dismissed, show Pika if the user has it set to launch shown.
presentConfiguredPika()
}

@objc func togglePopover(_: AnyObject?) { windowCoordinator.togglePopover() }

@IBAction func openAboutWindow(_: Any?) { windowCoordinator.openAboutWindow() }
Expand All @@ -240,6 +277,7 @@ extension AppDelegate {

@IBAction func triggerPickContrast(_: Any) {
notificationCenter.post(name: .triggerPickForeground, object: self, userInfo: ["chain": true])
notificationCenter.post(name: .triggerPickPair, object: self)
}

@IBAction func triggerCopyForeground(_: Any) {
Expand Down Expand Up @@ -286,12 +324,30 @@ extension AppDelegate {
notificationCenter.post(name: .triggerSwap, object: self)
}

/// The `NSTextView` field editor of whichever `ScrubTextField` is currently being edited, if
/// any — `ScrubTextField.becomeFirstResponder` makes the field editor (not the field itself)
/// the window's first responder while editing, so this is the one place both `triggerUndo`
/// and `triggerRedo` need to check to keep Cmd-Z scoped to "whatever the user is actually
/// looking at": a value being typed, if one is focused, otherwise colour-pick history.
private var focusedFieldUndoManager: UndoManager? {
guard let editor = NSApp.keyWindow?.firstResponder as? NSTextView, editor.isFieldEditor else { return nil }
return editor.undoManager
}
Comment thread
superhighfives marked this conversation as resolved.

@IBAction func triggerUndo(_: Any) {
if let manager = focusedFieldUndoManager, manager.canUndo {
manager.undo()
return
}
notificationCenter.post(name: .triggerUndo, object: self)
eyedroppers.undo()
}

@IBAction func triggerRedo(_: Any) {
if let manager = focusedFieldUndoManager, manager.canRedo {
manager.redo()
return
}
notificationCenter.post(name: .triggerRedo, object: self)
eyedroppers.redo()
}
Expand Down
3 changes: 3 additions & 0 deletions Pika/Assets/en.lproj/Localizable.strings
Original file line number Diff line number Diff line change
Expand Up @@ -202,6 +202,9 @@
/* Background */
"color.background" = "Background";

/* Invalid input */
"color.edit.invalid" = "Invalid input";

/* WCAG 3:1 */
"color.wcag.30" = "WCAG 2 level AA requires a contrast ratio of at least 3:1 for large (18pt / ~24px) or bold (14pt / ~18.6px) text, and for non-text content like UI.";

Expand Down
14 changes: 14 additions & 0 deletions Pika/Constants/Constants.swift
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@ import SwiftUI

extension KeyboardShortcuts.Name {
static let togglePika = Self("togglePika")
// Global, user-rebindable "pick a pair" shortcut (foreground → background chain).
// Defaults to ⌥⌘D, matching the in-window menu equivalent it replaces.
static let pickPair = Self("pickPair", default: .init(.d, modifiers: [.command, .option]))
}

enum PikaConstants {
Expand All @@ -14,6 +17,11 @@ enum PikaConstants {
: "https://superhighfives.com/releases/pika"
}

// Bump when a release has onboarding worth re-showing: the splash then appears once for
// everyone (even those who ticked "Don't show again") whose `lastSeenSplashVersion` is lower.
// v1: the custom (Pro) colour picker.
static let currentSplashVersion = 1

static let pikaWebsiteURL = "https://superhighfives.com/pika"
static let gitHubRepoURL = "https://github.com/superhighfives/pika"
static let gitHubIssueURL = "https://github.com/superhighfives/pika/issues/new/choose"
Expand Down Expand Up @@ -42,6 +50,7 @@ enum PikaConstants {
static let ncTriggerCopyData = "triggerCopyData"
static let ncTriggerPickForeground = "triggerPickForeground"
static let ncTriggerPickBackground = "triggerPickBackground"
static let ncTriggerPickPair = "triggerPickPair"
static let ncTriggerSystemPickerForeground = "triggerSystemPickerForeground"
static let ncTriggerSystemPickerBackground = "triggerSystemPickerBackground"
static let ncTriggerSwap = "triggerSwap"
Expand All @@ -67,6 +76,7 @@ enum PikaConstants {
static let ncExportPalette = "exportPalette"
static let ncSystemColorChanged = "systemColorChanged"
static let ncExpandToFit = "expandToFit"
static let ncColorNamesUpdated = "colorNamesUpdated"

// Disabled formats for SwiftUI copy format
static let disabledFormats: [ColorFormat] = [.hex, .hsl, .opengl, .lab, .oklch]
Expand All @@ -75,6 +85,9 @@ enum PikaConstants {
extension Notification.Name {
static let triggerPickForeground = Notification.Name(PikaConstants.ncTriggerPickForeground)
static let triggerPickBackground = Notification.Name(PikaConstants.ncTriggerPickBackground)
/// Help-grid-only: fired alongside the chained `.triggerPickForeground` post so
/// "Pick pair"'s own row highlights instead of piggybacking on "Pick foreground"'s.
static let triggerPickPair = Notification.Name(PikaConstants.ncTriggerPickPair)
static let triggerCopyForeground = Notification.Name(PikaConstants.ncTriggerCopyForeground)
static let triggerCopyBackground = Notification.Name(PikaConstants.ncTriggerCopyBackground)
static let triggerCopyText = Notification.Name(PikaConstants.ncTriggerCopyText)
Expand Down Expand Up @@ -104,4 +117,5 @@ extension Notification.Name {
static let exportPalette = Notification.Name(PikaConstants.ncExportPalette)
static let systemColorChanged = Notification.Name(PikaConstants.ncSystemColorChanged)
static let expandToFit = Notification.Name(PikaConstants.ncExpandToFit)
static let colorNamesUpdated = Notification.Name(PikaConstants.ncColorNamesUpdated)
}
31 changes: 31 additions & 0 deletions Pika/Constants/Defaults.swift
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,25 @@ enum WindowShadow: String, Codable, CaseIterable {
var showsShadowAtRest: Bool { self != .never }
}

enum PickerStyle: String, Codable, CaseIterable, Equatable {
case system // NSColorSampler (default, today's behaviour)
case custom // Pika-native loupe
}

enum LoupeTheme: String, Codable, CaseIterable, Equatable {
case lens // colour-filled rim engraved with the format + colour name (SF Pro)
case badge // white rounded badges hugging the inside edge (monospaced)
case card // plain magnifier with a readout card beside it

var localizedName: String {
switch self {
case .lens: return PikaText.textLoupeThemeLens
case .badge: return PikaText.textLoupeThemeBadge
case .card: return PikaText.textLoupeThemeCard
}
}
}

enum AppMode: String, Codable, CaseIterable {
case menubar = "preferences.app.mode.menubar"
case regular = "preferences.app.mode.regular"
Expand All @@ -84,9 +103,21 @@ enum AppMode: String, Codable, CaseIterable {
extension Defaults.Keys {
static let colorFormat = Key<ColorFormat>("colorFormat", default: .hex)
static let viewedSplash = Key<Bool>("viewedSplash", default: false)
// The colour-name list to use, keyed to color.pizza's `/v1/lists/`. `default` is the
// list bundled at build time as the offline fallback.
static let colorNameList = Key<String>("colorNameList", default: "default")
// When false, the splash is shown on launch. The splash's pre-selected "Don't show
// this again" checkbox sets this to true on dismissal.
static let hideSplashOnLaunch = Key<Bool>("hideSplashOnLaunch", default: false)
// The `PikaConstants.currentSplashVersion` last shown to the user. Bumping that constant
// re-shows the splash once for everyone (even those who ticked "Don't show again"), so a
// release with new onboarding gets in front of existing users.
static let lastSeenSplashVersion = Key<Int>("lastSeenSplashVersion", default: 0)
static let hidePikaWhilePicking = Key<Bool>("hidePikaWhilePicking", default: false)
static let windowShadow = Key<WindowShadow>("windowShadow", default: .always)
static let pickContrastingColor = Key<Bool>("pickContrastingColor", default: false)
static let pickerStyle = Key<PickerStyle>("pickerStyle", default: .system)
static let loupeTheme = Key<LoupeTheme>("loupeTheme", default: .lens)
static let copyColorOnPick = Key<Bool>("copyColorOnPick", default: false)
static let hideMenuBarIcon = Key<Bool>("hideMenuBarIcon", default: false)
static let betaUpdates = Key<Bool>("betaUpdates", default: false)
Expand Down
39 changes: 37 additions & 2 deletions Pika/Constants/PikaShortcuts.swift
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,29 @@ struct PikaShortcut {
let modifiers: NSEvent.ModifierFlags
let action: Selector
let notificationName: Notification.Name
/// True when this shortcut is also registered as a global `KeyboardShortcuts`
/// hotkey (see `AppDelegate.registerTogglePikaShortcut`). Such shortcuts fire
/// regardless of focus, so the popover local monitor must NOT dispatch them
/// again — otherwise the action double-fires. Still shown in the Help grid.
let hasGlobalBinding: Bool

init(
title: String,
displayKeys: [String],
character: String,
modifiers: NSEvent.ModifierFlags,
action: Selector,
notificationName: Notification.Name,
hasGlobalBinding: Bool = false
) {
self.title = title
self.displayKeys = displayKeys
self.character = character
self.modifiers = modifiers
self.action = action
self.notificationName = notificationName
self.hasGlobalBinding = hasGlobalBinding
}
}

enum PikaShortcuts {
Expand All @@ -29,6 +52,16 @@ enum PikaShortcuts {
action: #selector(AppDelegate.triggerPickBackground),
notificationName: .triggerPickBackground
),
PikaShortcut(
title: PikaText.textPickPair,
displayKeys: ["⌥", "⌘", "D"], character: "d", modifiers: [.command, .option],
action: #selector(AppDelegate.triggerPickContrast),
notificationName: .triggerPickPair,
// Already bound globally as `KeyboardShortcuts.Name.pickPair`, which
// fires regardless of focus — skip local-monitor dispatch to avoid a
// double pick (mirrors the menu-accelerator omission in PikaApp.swift).
hasGlobalBinding: true
),
PikaShortcut(
title: PikaText.textCopyForeground,
displayKeys: ["⌘", "C"], character: "c", modifiers: .command,
Expand Down Expand Up @@ -133,10 +166,12 @@ enum PikaShortcuts {
),
]

/// Returns the shortcut matching the given key event, if any.
/// Returns the shortcut matching the given key event, if any. Shortcuts that
/// are also globally bound are excluded — the global hotkey already dispatches
/// them regardless of focus, so matching here would double-fire the action.
static func match(_ event: NSEvent) -> PikaShortcut? {
let chars = event.charactersIgnoringModifiers?.lowercased() ?? ""
let mods = event.modifierFlags.intersection(.deviceIndependentFlagsMask)
return all.first { $0.character == chars && $0.modifiers == mods }
return all.first { $0.character == chars && $0.modifiers == mods && !$0.hasGlobalBinding }
}
}
Loading
Loading