Thanks for considering contributing. This guide covers everything you need to get started.
# 1. Fork and clone
git clone https://github.com/YOUR_USERNAME/better-shot.git
cd better-shot
# 2. Build and run
make runThat's it. The Makefile handles everything. No extra tools needed.
Alternative: Open
BetterShot.xcodeprojin Xcode and press⌘R.
The Xcode project is generated from project.yml using XcodeGen. If you change project.yml, regenerate the project:
brew install xcodegen # one-time
xcodegen generateMost contributions won't need this.
- macOS 14.0+
- Xcode 16.0+ (Swift 6)
On first launch, grant both:
- Screen Recording — System Settings > Privacy & Security > Screen Recording
- Accessibility — System Settings > Privacy & Security > Accessibility
Sources/
App/ App entry point and delegate
Capture/ Screenshot capture, region selection, color picker, countdown
Editor/ Annotation editor: canvas, inspector panel, tools, rendering
Models/ Data types: annotations, backgrounds, preferences, config
Preview/ Floating preview overlay and pinned screenshots
History/ Capture history (JSON in Application Support)
Recording/ Screen/window recording, video editor, trim timeline, effects export
Services/ Beautifier renderer, keyboard shortcuts, app updater
Settings/ Preferences window (sidebar navigation) and settings window controller
Views/ Menu bar popover, toast notifications
Resources/
Assets.xcassets/ App icon, menu bar icon
Backgrounds/ Bundled wallpaper and gradient images
Info.plist
BetterShot.entitlements
| File | What it does |
|---|---|
EditorModel.swift |
All editor state: annotation interactions, undo/redo, config |
EditorCanvasView.swift |
Live canvas rendering with drag gestures |
EditorInspectorView.swift |
Left panel: tools, colors, text, effects, backgrounds |
AnnotationDrawing.swift |
CoreGraphics renderer for final image export (Y-down flipped context) |
BeautifierRenderer.swift |
Composites background + shadow + radius + annotations (flips CG context for annotations) |
AnnotationItem.swift |
Annotation data model: rect, points, tools, hit-testing, remapped(from:) for crop |
CaptureOrchestrator.swift |
Coordinates capture pipeline: capture > sound > history > preview |
ShortcutService.swift |
Global keyboard shortcuts via CGEvent tap |
PreviewOverlay.swift |
Floating preview card after capture (screenshots and recordings) |
ScreenRecordingManager.swift |
Screen and window recording via ScreenCaptureKit |
VideoEditorModel.swift |
Video editor state: trim, effects config, export with AVMutableVideoComposition |
VideoEditorView.swift |
Video editor UI: inspector sidebar, preview, timeline, transport controls |
RecordingStatusBar.swift |
Floating recording status bar with timer, pause, stop, discard |
PreferencesView.swift |
Settings window with sidebar navigation (General, Capture, Recording, History, Videos, About) |
SettingsWindowController.swift |
Creates and manages the settings NSWindow (mirrors EditorWindowController) |
MenuBarPopoverController.swift |
Custom NSPanel-based menu bar popover with arrow, click-outside dismiss, and originScreen for window targeting |
ToastWindow.swift |
Floating toast notifications (save confirmation, OCR/color picker feedback) |
AppPreferences.swift |
All UserDefaults-backed preferences |
User presses ⌘⇧4
→ ShortcutService (CGEvent tap intercepts the keypress, captures origin screen)
→ CaptureOrchestrator.performCapture(.region, on: screen)
→ ScreenCapture.captureRegion() (native screencapture CLI)
→ HistoryStore.importCapture() (saves to Application Support)
→ PreviewOverlay.show(on: screen) (floating card on same screen)
→ User clicks preview → EditorWindowController.open(on: screen)
All windows (editor, video editor, settings, preview, toast, pinned screenshots) open on the screen where the action originated, not the primary display.
User presses ⌘⇧2 (or clicks Record / Record Window)
→ ShortcutService (CGEvent tap intercepts the keypress)
→ ScreenRecordingManager.startRecording() (full screen)
or .startWindowRecording() (hover-and-click window picker)
→ RecordingStatusBarController.show() (floating status bar, excluded from capture)
→ User clicks stop → HistoryStore.importCapture(kind: .recording, deleteSource: false)
→ PreviewOverlay.show() (floating card with raw video)
→ User clicks preview → VideoEditorWindowController.open()
→ User edits effects → VideoEditorModel.exportWithEffects()
(AVMutableVideoComposition + Core Animation layers)
EditorModel (all state)
├── EditorInspectorView Left panel: tools, style, text, effects, layout, background
├── EditorCanvasView Renders image + live annotation views, handles gestures
│ └── AnnotationItemView One per annotation (shapes, text, redaction)
└── AnnotationKeyboard Keyboard shortcuts for tools and actions
Annotations use normalized coordinates (0.0 to 1.0) so they're resolution-independent. The canvas renders them as SwiftUI views for interactive editing. AnnotationDrawing re-renders them with CoreGraphics for the final export.
Coordinate system: The canvas preview uses SwiftUI's Y-down coordinates. The export renderer flips its CG context to Y-down before drawing annotations, so both code paths use identical coordinate math (imageRect.minY + point.y * imageRect.height). When crop is active, AnnotationItem.remapped(from:) transforms annotations from original-image to cropped-image coordinate space before export.
The settings window is managed by SettingsWindowController, which creates an NSWindow hosting PreferencesView in an NSHostingView. This mirrors the EditorWindowController pattern. The view uses a sidebar list (General, Capture, Recording, History, Videos, About) and a content panel. Preferences are stored via @AppStorage and the centralized AppPreferences enum. Screenshots and recordings have separate history tabs — History shows only screenshots, Videos shows only recordings.
The menu bar popover is managed by MenuBarPopoverController, which creates a custom NSPanel (not SwiftUI's MenuBarExtra) for full control over appearance and animation. The panel hosts MenuBarPanelView with an arrow indicator and spring animation.
- Add the case to
AnnotationToolinModels/AnnotationItem.swift - Set its
systemImageandtitle - Add live rendering in
Editor/AnnotationItemView.swift(usesviewRect/viewPoint— Y-down) - Add export rendering in
Editor/AnnotationDrawing.swift(usesrenderedRect/renderedPointwithflipped: true— same Y-down math as the canvas) - Handle creation in
EditorModel.beginDraftItem - Handle updates in
EditorModel.updateDraftItem - Add keyboard shortcut in
Editor/AnnotationKeyboard.swift - If the tool uses
rectorpoints, ensureAnnotationItem.remapped(from:)handles it correctly for crop support
- Add the case to
BackgroundStyleinModels/BackgroundStyle.swift - Handle rendering in
BeautifierRenderer.drawBackground - Add picker UI in
BackgroundPickerSectioninEditorInspectorView.swift - Add picker UI in
DefaultBackgroundPickerinPreferencesView.swift
- Add the
@AppStoragekey and property inModels/AppPreferences.swift - Add the UI control in the appropriate section of
Settings/PreferencesView.swift
- Swift 6 strict concurrency — all code must compile without concurrency warnings
@Observablefor model classes,@Bindablein views- No comments unless explaining something non-obvious (a hidden constraint, a workaround)
- No abstractions for their own sake — three similar lines is better than a premature helper
- System colors — use
NSColor.controlBackgroundColor,.separatorColor, etc. for native look - Main actor — all UI types are
@MainActor
- Create a branch:
git checkout -b feat/what-it-doesorfix/what-it-fixes - Keep changes focused — one feature or fix per PR
- Make sure it builds:
make build - Test manually in the app:
make run - Write a clear PR title and description
Use short, descriptive messages:
feat: add blur strength slider to redaction tools
fix: window capture failing on secondary monitors
chore: update dependencies
Version is tracked in three places (keep them in sync):
| File | Fields |
|---|---|
version.json |
version, build |
project.yml |
MARKETING_VERSION, CURRENT_PROJECT_VERSION |
BetterShot.xcodeproj/project.pbxproj |
MARKETING_VERSION, CURRENT_PROJECT_VERSION (both Debug and Release) |
The CHANGELOG.md documents what changed in each version.
By contributing, you agree that your contributions will be licensed under the project's BSD 3-Clause License.