diff --git a/docs/adr/0010-spm-build-with-shell-script-app-bundling.md b/docs/adr/0010-spm-build-with-shell-script-app-bundling.md new file mode 100644 index 0000000..e449f80 --- /dev/null +++ b/docs/adr/0010-spm-build-with-shell-script-app-bundling.md @@ -0,0 +1,36 @@ +# ADR 0010 — Swift Package Manager build with shell-script app bundling + +**Date:** 2026-04-13 +**Status:** Accepted + +## Context + +Snippy is a macOS GUI application with an `NSPanel`, menu bar icon, and a full SwiftUI view hierarchy. macOS GUI apps are conventionally built with Xcode projects (`.xcodeproj` or `.xcworkspace`), which provide automatic app bundle creation, code signing, asset catalog compilation, and Interface Builder integration. + +However, Snippy has no external dependencies, no asset catalogs, no storyboards, and no test target. The entire app is ~1,100 lines of Swift across 10 source files. An Xcode project for this scope is mostly boilerplate — the `.xcodeproj` directory alone would be larger than several source files combined. + +Alternative approaches considered: + +- **Xcode project** — full IDE integration and automatic `.app` bundle generation, but adds ~20 config files, merge-conflict-prone `pbxproj`, and requires Xcode (not just Command Line Tools) to build. +- **Swift Package Manager only** — builds an executable binary via `swift build`, but SPM has no concept of `.app` bundles, `Info.plist`, or macOS application packaging. +- **SPM + shell script** — SPM compiles the Swift code; a short shell script assembles the `.app` bundle structure, copies the binary and `Info.plist`, and optionally installs to `/Applications`. + +## Decision + +Use `Package.swift` (swift-tools-version 6.0, Swift language mode 5) as the build system and `build.sh` as the app-bundling step. The package defines a single executable target with all sources in `Sources/`. The shell script: + +1. Runs `swift build -c release`. +2. Creates the `.app/Contents/MacOS` and `.app/Contents/Resources` directory structure. +3. Copies the compiled binary from `.build/release/Snippy` into the bundle. +4. Copies `Info.plist` into `.app/Contents/`. +5. Writes a `PkgInfo` file (`APPL????`). +6. Optionally kills any running instance and installs to `/Applications`. + +## Consequences + +- Building requires only Xcode Command Line Tools (`xcode-select --install`), not the full Xcode IDE. `swift build` works from any terminal. +- There is no `.xcodeproj` to maintain or merge. The `Package.swift` is 14 lines. +- Code signing is not handled. The app runs unsigned, which is fine for local development but means Gatekeeper will block it for other users unless they right-click → Open. Distribution via DMG would require a separate signing step. +- Adding resources (images, asset catalogs, localisation bundles) would require extending `build.sh` to copy them into `.app/Contents/Resources`. SPM's resource bundling (`Bundle.module`) does not produce a macOS app bundle layout. +- `Info.plist` is maintained manually at the repo root. Changes to bundle metadata (version, `LSUIElement`, ATS settings) require editing the plist directly rather than through Xcode's GUI. +- Debug builds via `swift build` (without `build.sh`) produce a bare executable that works but has no app bundle identity — it cannot display a menu bar icon correctly in all macOS versions. `build.sh` is needed for a proper install. diff --git a/docs/adr/0011-zstack-overlay-for-inline-label-coloring.md b/docs/adr/0011-zstack-overlay-for-inline-label-coloring.md new file mode 100644 index 0000000..4087451 --- /dev/null +++ b/docs/adr/0011-zstack-overlay-for-inline-label-coloring.md @@ -0,0 +1,45 @@ +# ADR 0011 — ZStack overlay for real-time inline label coloring + +**Date:** 2026-04-13 +**Status:** Accepted + +## Context + +Snippy's add and edit rows show a real-time color highlight as the user types: the label portion (before `": "`) appears in an accent color while the value portion stays in the primary color. This gives immediate visual feedback that the parser has detected a label, without requiring a separate label field. + +SwiftUI's `TextField` does not support attributed strings or partial text coloring. The entire text field can have one `foregroundColor`, but there is no built-in way to color a substring differently. + +Approaches considered: + +- **NSTextField with NSAttributedString** — drop down to AppKit, use `NSMutableAttributedString` to set per-range colors. Works, but breaks SwiftUI's declarative data flow — the binding between `@State` and the attributed string must be managed manually, and the colored text must be re-applied on every keystroke. +- **Multiple adjacent TextFields** — split input into a label `TextField` and a value `TextField` side by side. Semantically clean but disrupts the single-field typing experience; the user would need to tab between fields and could not type `Label: value` in one continuous flow. +- **ZStack with transparent TextField + colored Text overlay** — keep a single `TextField` bound to the full input string, but set its `foregroundColor` to `.clear` when a label is detected. Layer a `Text` view on top (via `ZStack(alignment: .leading)`) that renders the label in accent color, the separator in secondary color, and the value in primary color. Disable hit-testing on the overlay so clicks and selections still reach the `TextField`. + +## Decision + +Use the ZStack overlay pattern in both `AddSnippetRow` and `EditSnippetRow`: + +```swift +ZStack(alignment: .leading) { + TextField("value or label: value", text: $input) + .foregroundColor(parsed != nil ? .clear : .primary) + + if let p = parsed { + (Text(p.label).foregroundColor(.accentColor) + + Text(": ").foregroundColor(.secondary) + + Text(p.value).foregroundColor(.primary)) + .allowsHitTesting(false) + } +} +``` + +The overlay is conditionally shown only when `SnippyParser.parse(input)` returns a non-nil result. When no label is detected, the `TextField` renders normally with `.primary` color. + +## Consequences + +- The user types in a single field with no mode switches. Label detection is instant and non-disruptive — the colors update on every keystroke as SwiftUI re-evaluates the `parsed` computed property. +- `.allowsHitTesting(false)` on the overlay ensures the `TextField` remains fully interactive: cursor placement, text selection, and drag all work as expected because input events pass through the overlay. +- The `Text` overlay and the `TextField` must use identical font metrics (`.system(size: 13, weight: .medium, design: .monospaced)`) so the colored text aligns exactly with the invisible original text. A mismatch would cause visible misalignment. +- `AddSnippetRow` uses `.accentColor` for the label; `EditSnippetRow` uses `.orange`. This visual distinction helps the user tell at a glance whether they are adding or editing. +- The pattern works only for single-line text. If multi-line snippet editing were added, the overlay alignment would break because `Text` and `TextField` handle line wrapping differently. +- The cursor and selection highlight are rendered by the underlying `TextField` in its `.clear` foreground color — they remain visible because the cursor and selection use system colors, not the text foreground color.