Use this alongside CLAUDE.md (bundle size rules, brand voice, project context).
# Build debug
xcodebuild -project Binky.xcodeproj -scheme Binky -configuration Debug build
# Build + run unit tests (the primary verification command)
xcodebuild -project Binky.xcodeproj -scheme Binky -configuration Debug -destination 'platform=macOS' test
# Release build (used by release.sh)
xcodebuild -scheme Binky -configuration Release -derivedDataPath build clean buildCI runs on macOS 26 + Xcode 26 (see .github/workflows/ci.yml). There is no linter, formatter, or typecheck step beyond what Xcode/Swift provides — xcodebuild test is the full gate.
Tests live in BinkyTests/ (Xcode test target) and import @testable import Binky + @testable import BinkyCoreSort.
Binky/— the Xcode app target (SwiftUI + AppKit). Entry point:BinkyApp.swift(@main).BinkyCore/— SwiftPM package with four targets:BinkyCoreShared— models, prefs keys, batch/history types (no dependencies)BinkyCoreSort— sort pipeline, content inspection, rules evaluation, Finder tags, archive/DMG services (depends onBinkyCoreShared)BinkyCLILib— CLI command implementations (depends on both above)BinkyCLI— executable entry point for thebinkyCLI binary
- The app target uses
@_exported import BinkyCoreSharedand@_exported import BinkyCoreSort(BinkyExportedDependencies.swift) so other app files don't need per-file imports. BinkyTests/— XCTest unit tests (classification, tag composition, routines, WhereFroms). No UI tests.site/— marketing site deployed via Netlify (netlify.tomlpublishessite/). Compare pages, screenshots,llms.txt,openapi.yaml.Casks/— Homebrew cask definition. Updated automatically byrelease.sh.tools/— Python helper scripts for localization. All four currently hardcode Dinky's path — adjust before running on Binky.docs/local-cli.md— how to build and use thebinkyCLI fromBinkyCore/.
- Preferences:
BinkyPreferences(ObservableObject) is the single source of truth, shared via@StateObjectat the app root and.environmentObject(). - Notifications: Cross-component communication uses
NotificationCenterwith names defined inStrings.swift(e.g..binkyStartSort,.binkyShowMainWindow,.binkyOpenMacPreferences). - Settings window: Uses
Windowscene (notSettingsscene) with.windowToolbarStyle(.unified(showsTitle: true))so the unified title bar matches the main window. - macOS version branching:
View+AdaptiveGlass.swiftprovidesadaptiveGlass()andadaptiveVisibleWindowToolbarBackground()that use#available(macOS 26, *)/#available(macOS 15, *)checks. Use these instead of inline availability checks. - Liquid Glass: On macOS 26+ use
.glassEffect(); on macOS 14–25 fall back to.ultraThinMaterial. TheadaptiveGlass(in:)extension handles this.
./release.sh <version> handles the full flow: bump MARKETING_VERSION + CURRENT_PROJECT_VERSION in project.pbxproj, update site version tokens, build Release, create DMG + zip, update Casks/binky.rb (version + sha256), commit, tag, push, and publish GitHub release via gh.
Prerequisites: create-dmg (brew install create-dmg), gh (brew install gh). Working tree must be clean.
Use --bump-only for steps 1–2 only (version strings + site, no build/git/gh).
- Segmented controls (
Picker+.pickerStyle(.segmented)): Use for 2–5 mutually exclusive panes in a single settings surface (e.g. Image / Video / PDF under Presets). This matches System Settings patterns and avoids long vertical scrolling for parallel option groups. - Settings window navigation: Host preferences in a dedicated
Windowscene withwindowToolbarStyle(.unified(showsTitle: true))(same pattern as Dinky — not the SwiftUISettingsscene, which splits the navigation chrome). Inside, useNavigationSplitViewwith a sidebarList(grouped sections: General, Sorting, Interface). The detail column usesNavigationStackwith per-pane titles and optional back/forward history (seePreferencesView). - Grouped
Form: Use section headers to name groups; use footers sparingly for secondary explanation (progressive disclosure). Prefer concise captions over long instructional blocks. - Accessibility: If
.labelsHidden()is used on a control for layout, set.accessibilityLabel(and related inputs when needed) so VoiceOver still describes the control. - Hierarchy: Place the control that changes scope (segmented picker) above the content it affects, with a clear section header (e.g. "Media").
- Cross-cutting settings: If an option affects multiple panes (e.g. Smart quality and PDF/video tiers), keep it in a dedicated section above the segmented control so it stays visible when switching panes.
- See
CLAUDE.mdfor bundle size and dependency rules.
- User-facing strings use
String(localized:comment:)withLocalizable.xcstrings(12 locales).InfoPlist.xcstringscovers Info.plist strings. Binky/Resources/has per-locale.lprojdirectories for Help markdown.- Python tools in
tools/— all hardcode Dinky's path; adjustPATH/ROOTbefore running on Binky:fill_xcstrings_translations.py— batch MT fill for missing xcstrings keys (Google Translate)fix_brand_in_xcstrings.py— fix brand-name MT leaks (e.g. 丁基→Dinky)correct_zh_translations.py— hand-curated zh-Hans/zh-Hant corrections (run after the above two)correct_ru_ja_translations.py— hand-curated Russian/Japanese correctionstranslate_help_md.py— translate Help.md to all locales via Google Translate